Compare commits

..

4 Commits

Author SHA1 Message Date
Paolo Barbolini
efee4b5d72 Fix warnings 2024-03-19 16:21:10 +01:00
Paolo Barbolini
57d7bf25cc Replace try_smtp macro with more resilient method 2024-03-19 16:21:10 +01:00
Paolo Barbolini
c52c596458 Drop ref syntax 2024-03-10 15:28:54 +01:00
Paolo Barbolini
53dee3e31f Drop need for NetworkStream::None variant 2024-03-10 15:28:54 +01:00
51 changed files with 1761 additions and 3604 deletions

View File

@@ -13,16 +13,16 @@ env:
jobs: jobs:
rustfmt: rustfmt:
name: rustfmt / nightly-2024-09-01 name: rustfmt / nightly-2023-06-22
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Install rust - name: Install rust
run: | run: |
rustup default nightly-2024-09-01 rustup default nightly-2023-06-22
rustup component add rustfmt rustup component add rustfmt
- name: cargo fmt - name: cargo fmt
@@ -34,7 +34,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Install rust - name: Install rust
run: | run: |
@@ -50,19 +50,19 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Install rust - name: Install rust
run: rustup update --no-self-update stable run: rustup update --no-self-update stable
- name: Setup cache - name: Setup cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Install cargo hack - name: Install cargo hack
run: cargo install cargo-hack --debug run: cargo install cargo-hack --debug
- name: Check with cargo hack - name: Check with cargo hack
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 run: cargo hack check --feature-powerset --depth 3
test: test:
name: test / ${{ matrix.name }} name: test / ${{ matrix.name }}
@@ -75,12 +75,12 @@ jobs:
rust: stable rust: stable
- name: beta - name: beta
rust: beta rust: beta
- name: '1.74' - name: '1.70'
rust: '1.74' rust: '1.70'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Install rust - name: Install rust
run: | run: |
@@ -90,7 +90,7 @@ jobs:
- name: Setup cache - name: Setup cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: Install postfix - name: Install postfix
run: | run: |
DEBIAN_FRONTEND=noninteractive sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix
@@ -112,6 +112,12 @@ jobs:
- name: Install dkimverify - name: Install dkimverify
run: sudo apt -y install python3-dkim run: sudo apt -y install python3-dkim
- name: Work around early dependencies MSRV bump
run: |
cargo update -p anstyle --precise 1.0.2
cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
- name: Test with no default features - name: Test with no default features
run: cargo test --no-default-features run: cargo test --no-default-features
@@ -119,16 +125,16 @@ jobs:
run: cargo test run: cargo test
- name: Test with all features (-native-tls) - name: Test with all features (-native-tls)
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 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
- name: Test with all features (-boring-tls) - name: Test with all features (-boring-tls)
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 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
# coverage: # coverage:
# name: Coverage # name: Coverage
# runs-on: ubuntu-latest # runs-on: ubuntu-latest
# steps: # steps:
# - uses: actions/checkout@v4 # - uses: actions/checkout@v2
# - uses: actions-rs/toolchain@v1 # - uses: actions-rs/toolchain@v1
# with: # with:
# toolchain: nightly # toolchain: nightly

View File

@@ -1,337 +1,3 @@
<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)
#### Bug fixes
* Fix feature gate for `accept_invalid_hostnames` for rustls ([#988])
* Fix parsing `Mailbox` with trailing spaces ([#986])
#### Misc
* Bump `rustls-native-certs` to v0.8 ([#992])
* Make getting started example in readme complete ([#990])
[#988]: https://github.com/lettre/lettre/pull/988
[#986]: https://github.com/lettre/lettre/pull/986
[#990]: https://github.com/lettre/lettre/pull/990
[#992]: https://github.com/lettre/lettre/pull/992
<a name="v0.11.8"></a>
### v0.11.8 (2024-09-03)
#### Features
* Add mTLS support ([#974])
* Implement `accept_invalid_hostnames` for rustls ([#977])
* Provide certificate chain for peer certificates when using `rustls` or `boring-tls` ([#976])
#### Changes
* Make `HeaderName` comparisons via `PartialEq` case insensitive ([#980])
#### Misc
* Fix clippy warnings ([#979])
* Replace manual impl of `#[non_exhaustive]` for `InvalidHeaderName` ([#981])
[#974]: https://github.com/lettre/lettre/pull/974
[#976]: https://github.com/lettre/lettre/pull/976
[#977]: https://github.com/lettre/lettre/pull/977
[#980]: https://github.com/lettre/lettre/pull/980
[#981]: https://github.com/lettre/lettre/pull/981
<a name="v0.11.7"></a>
### v0.11.7 (2024-04-23)
#### Misc
* Bump `hostname` to v0.4 ([#956])
* Fix `tracing` message consistency ([#960])
* Bump minimum required `rustls` to v0.23.5 ([#958])
* Dropped use of `ref` syntax in the entire project ([#959])
[#956]: https://github.com/lettre/lettre/pull/956
[#958]: https://github.com/lettre/lettre/pull/958
[#959]: https://github.com/lettre/lettre/pull/959
[#960]: https://github.com/lettre/lettre/pull/960
<a name="v0.11.6"></a>
### v0.11.6 (2024-03-28)
#### Bug fixes
* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952])
#### Misc
* Updated copyright year in license ([#954])
[#952]: https://github.com/lettre/lettre/pull/952
[#954]: https://github.com/lettre/lettre/pull/954
<a name="v0.11.5"></a>
### v0.11.5 (2024-03-25)
#### Features
* Support SMTP SASL draft login challenge ([#911])
* Add conversion from SMTP response code to integer ([#941])
#### Misc
* Upgrade `rustls` to v0.23 ([#950])
* Bump `base64` to v0.22 ([#945])
* Fix typos in documentation ([#943], [#944])
* Add `Cargo.lock` ([#942])
[#911]: https://github.com/lettre/lettre/pull/911
[#941]: https://github.com/lettre/lettre/pull/941
[#942]: https://github.com/lettre/lettre/pull/942
[#943]: https://github.com/lettre/lettre/pull/943
[#944]: https://github.com/lettre/lettre/pull/944
[#945]: https://github.com/lettre/lettre/pull/945
[#950]: https://github.com/lettre/lettre/pull/950
<a name="v0.11.4"></a> <a name="v0.11.4"></a>
### v0.11.4 (2024-01-28) ### v0.11.4 (2024-01-28)
@@ -748,6 +414,6 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
* multipart support * multipart support
* add non-consuming methods for Email builders * 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` for consistency with other methods. Use the `header`
method instead method instead

1828
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge) # remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.19" version = "0.11.4"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "https://lettre.rs" homepage = "https://lettre.rs"
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
categories = ["email", "network-programming"] categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"] keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021" edition = "2021"
rust-version = "1.74" rust-version = "1.70"
[badges] [badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" } is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -19,12 +19,9 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
email_address = { version = "0.2.1", default-features = false }
chumsky = "0.9" chumsky = "0.9"
idna = "1" idna = "0.5"
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 # builder
httpdate = { version = "1", optional = true } httpdate = { version = "1", optional = true }
@@ -32,26 +29,26 @@ mime = { version = "0.3.4", optional = true }
fastrand = { version = "2.0", optional = true } fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.5", optional = true } quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.22", optional = true } base64 = { version = "0.22", optional = true }
email-encoding = { version = "0.4", optional = true } email-encoding = { version = "0.2", optional = true }
# file transport # file transport
uuid = { version = "1", features = ["v4"], optional = true } uuid = { version = "1", features = ["v4"], optional = true }
serde = { version = "1.0.110", features = ["derive"], optional = true } serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
# smtp-transport # smtp-transport
nom = { version = "8", optional = true } nom = { version = "7", optional = true }
hostname = { version = "0.4", optional = true } # feature hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.6", optional = true } socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true } url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true } percent-encoding = { version = "2.3", optional = true }
## tls ## tls
native-tls = { version = "0.2.9", optional = true } # feature native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.23.18", default-features = false, features = ["logging", "std", "tls12"], optional = true } rustls = { version = "0.22.1", optional = true }
rustls-platform-verifier = { version = "0.6.0", optional = true } rustls-pemfile = { version = "2", optional = true }
rustls-native-certs = { version = "0.8", optional = true } rustls-native-certs = { version = "0.7", optional = true }
webpki-roots = { version = "1.0.0", optional = true } webpki-roots = { version = "0.26", optional = true }
boring = { version = "4", optional = true } boring = { version = "4", optional = true }
# async # async
@@ -61,22 +58,22 @@ async-trait = { version = "0.1", optional = true }
## async-std ## async-std
async-std = { version = "1.8", optional = true } async-std = { version = "1.8", optional = true }
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true } #async-native-tls = { version = "0.3.3", optional = true }
futures-rustls = { version = "0.25", optional = true }
## tokio ## tokio
tokio1_crate = { package = "tokio", version = "1", optional = true } tokio1_crate = { package = "tokio", version = "1", optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true } tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true } tokio1_rustls = { package = "tokio-rustls", version = "0.25", optional = true }
tokio1_boring = { package = "tokio-boring", version = "4", optional = true } tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
## dkim ## dkim
sha2 = { version = "0.10", features = ["oid"], optional = true } sha2 = { version = "0.10", optional = true, features = ["oid"] }
rsa = { version = "0.9", optional = true } rsa = { version = "0.9", optional = true }
ed25519-dalek = { version = "2", optional = true } ed25519-dalek = { version = "2", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies] # email formats
## web-time for wasm support email_address = { version = "0.2.1", default-features = false }
web-time = { version = "1.1.0", optional = true }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1" pretty_assertions = "1"
@@ -111,36 +108,21 @@ smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percen
pool = ["dep:futures-util"] pool = ["dep:futures-util"]
rustls = ["dep:rustls"] rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile"]
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"] boring-tls = ["dep:boring"]
# async # async
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"] async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
async-std1-rustls = ["async-std1", "rustls", "dep:futures-rustls"] #async-std1-native-tls = ["async-std1", "native-tls", "dep:async-native-tls"]
# deprecated async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
async-std1-rustls-tls = ["async-std1-rustls", "rustls-tls"]
tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"] tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"] tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
tokio1-rustls = ["tokio1", "rustls", "dep:tokio1_rustls"] tokio1-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
# deprecated
tokio1-rustls-tls = ["tokio1-rustls", "rustls-tls"]
tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"] tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"] dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
# wasm support
web = ["dep:web-time"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(lettre_ignore_tls_mismatch)'] }
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"] rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]

View File

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

View File

@@ -28,8 +28,8 @@
</div> </div>
<div align="center"> <div align="center">
<a href="https://deps.rs/crate/lettre/0.11.19"> <a href="https://deps.rs/crate/lettre/0.11.4">
<img src="https://deps.rs/crate/lettre/0.11.19/status.svg" <img src="https://deps.rs/crate/lettre/0.11.4/status.svg"
alt="dependency status" /> alt="dependency status" />
</a> </a>
</div> </div>
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
## Supported Rust Versions ## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.74, but this could change at any time either from the minimum supported Rust version is 1.70, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre. one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example ## Example
This library requires Rust 1.74 or newer. This library requires Rust 1.70 or newer.
To use this library, add the following to your `Cargo.toml`: To use this library, add the following to your `Cargo.toml`:
```toml ```toml
@@ -67,33 +67,31 @@ lettre = "0.11"
``` ```
```rust,no_run ```rust,no_run
use lettre::message::{Mailbox, header::ContentType}; use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport}; use lettre::{Message, SmtpTransport, Transport};
fn main() { let email = Message::builder()
let email = Message::builder() .from("NoBody <nobody@domain.tld>".parse().unwrap())
.from(Mailbox::new(Some("NoBody".to_owned()), "nobody@domain.tld".parse().unwrap())) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.reply_to(Mailbox::new(Some("Yuin".to_owned()), "yuin@domain.tld".parse().unwrap())) .to("Hei <hei@domain.tld>".parse().unwrap())
.to(Mailbox::new(Some("Hei".to_owned()), "hei@domain.tld".parse().unwrap())) .subject("Happy new year")
.subject("Happy new year") .header(ContentType::TEXT_PLAIN)
.header(ContentType::TEXT_PLAIN) .body(String::from("Be happy!"))
.body(String::from("Be happy!")) .unwrap();
.unwrap();
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com") let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap() .unwrap()
.credentials(creds) .credentials(creds)
.build(); .build();
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"), Err(e) => panic!("Could not send email: {e:?}"),
}
} }
``` ```
@@ -107,7 +105,7 @@ cargo run --example autoconfigure SMTP_HOST
## Testing ## 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` 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`. Alternatively only unit tests can be run by doing `cargo test --lib`.

View File

@@ -1,5 +1,5 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport}; use lettre::{Message, SmtpTransport, Transport};
fn bench_simple_send(c: &mut Criterion) { fn bench_simple_send(c: &mut Criterion) {
let sender = SmtpTransport::builder_dangerous("127.0.0.1") let sender = SmtpTransport::builder_dangerous("127.0.0.1")
@@ -13,7 +13,6 @@ fn bench_simple_send(c: &mut Criterion) {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let result = black_box(sender.send(&email)); let result = black_box(sender.send(&email));
@@ -33,7 +32,6 @@ fn bench_reuse_send(c: &mut Criterion) {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let result = black_box(sender.send(&email)); let result = black_box(sender.send(&email));

View File

@@ -1,3 +0,0 @@
disallowed-methods = [
{ "path" = "std::time::SystemTime::now", reason = "does not work on WASM environments", replacement = "crate::time::now" }
]

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Address {
{ {
struct FieldVisitor; struct FieldVisitor;
impl Visitor<'_> for FieldVisitor { impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field; type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {

View File

@@ -45,7 +45,6 @@ use crate::transport::smtp::Error;
#[async_trait] #[async_trait]
pub trait Executor: Debug + Send + Sync + 'static + private::Sealed { pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
#[allow(private_bounds)]
type Handle: SpawnHandle; type Handle: SpawnHandle;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
type Sleep: Future<Output = ()> + Send + 'static; type Sleep: Future<Output = ()> + Send + 'static;
@@ -83,8 +82,8 @@ pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
#[doc(hidden)] #[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
#[async_trait] #[async_trait]
pub(crate) trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed { pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
async fn shutdown(&self); async fn shutdown(self);
} }
/// Async [`Executor`] using `tokio` `1.x` /// Async [`Executor`] using `tokio` `1.x`
@@ -134,7 +133,7 @@ impl Executor for Tokio1Executor {
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match tls { let tls_parameters = match tls {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls"))] #[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()), Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None, _ => None,
}; };
@@ -148,15 +147,15 @@ impl Executor for Tokio1Executor {
) )
.await?; .await?;
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls"))] #[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
match tls { match tls {
Tls::Opportunistic(tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
} }
Tls::Required(tls_parameters) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
_ => (), _ => (),
} }
@@ -178,7 +177,7 @@ impl Executor for Tokio1Executor {
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))] #[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
#[async_trait] #[async_trait]
impl SpawnHandle for tokio1_crate::task::JoinHandle<()> { impl SpawnHandle for tokio1_crate::task::JoinHandle<()> {
async fn shutdown(&self) { async fn shutdown(self) {
self.abort(); self.abort();
} }
} }
@@ -202,7 +201,7 @@ pub struct AsyncStd1Executor;
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
impl Executor for AsyncStd1Executor { impl Executor for AsyncStd1Executor {
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
type Handle = futures_util::future::AbortHandle; type Handle = async_std::task::JoinHandle<()>;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
type Sleep = BoxFuture<'static, ()>; type Sleep = BoxFuture<'static, ()>;
@@ -212,9 +211,7 @@ impl Executor for AsyncStd1Executor {
F: Future<Output = ()> + Send + 'static, F: Future<Output = ()> + Send + 'static,
F::Output: Send + 'static, F::Output: Send + 'static,
{ {
let (handle, registration) = futures_util::future::AbortHandle::new_pair(); async_std::task::spawn(fut)
async_std::task::spawn(futures_util::future::Abortable::new(fut, registration));
handle
} }
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
@@ -233,7 +230,7 @@ impl Executor for AsyncStd1Executor {
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match tls { let tls_parameters = match tls {
#[cfg(feature = "async-std1-rustls")] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()), Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None, _ => None,
}; };
@@ -246,15 +243,15 @@ impl Executor for AsyncStd1Executor {
) )
.await?; .await?;
#[cfg(feature = "async-std1-rustls")] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
match tls { match tls {
Tls::Opportunistic(tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
} }
Tls::Required(tls_parameters) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?; conn = conn.starttls(tls_parameters.clone(), hello_name).await?;
} }
_ => (), _ => (),
} }
@@ -275,9 +272,9 @@ impl Executor for AsyncStd1Executor {
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))] #[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
#[async_trait] #[async_trait]
impl SpawnHandle for futures_util::future::AbortHandle { impl SpawnHandle for async_std::task::JoinHandle<()> {
async fn shutdown(&self) { async fn shutdown(self) {
self.abort(); self.cancel().await;
} }
} }
@@ -294,5 +291,5 @@ mod private {
impl Sealed for tokio1_crate::task::JoinHandle<()> {} impl Sealed for tokio1_crate::task::JoinHandle<()> {}
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))] #[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
impl Sealed for futures_util::future::AbortHandle {} impl Sealed for async_std::task::JoinHandle<()> {}
} }

View File

@@ -6,7 +6,7 @@
//! * Secure defaults //! * Secure defaults
//! * Async support //! * Async support
//! //!
//! Lettre requires Rust 1.74 or newer. //! Lettre requires Rust 1.70 or newer.
//! //!
//! ## Features //! ## Features
//! //!
@@ -34,25 +34,13 @@
//! //!
//! _Secure SMTP connections using TLS from the `native-tls` crate_ //! _Secure SMTP connections using TLS from the `native-tls` crate_
//! //!
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL //! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
//! on all other platforms.
//! //!
//! * **native-tls** 📫: TLS support for the synchronous version of the API //! * **native-tls** 📫: TLS support for the synchronous version of the API
//! * **tokio1-native-tls**: TLS support for the `tokio1` async 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` //! 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) //! #### SMTP over TLS via the boring crate (Boring TLS)
//! //!
//! _Secure SMTP connections using TLS from the `boring-tls` crate_ //! _Secure SMTP connections using TLS from the `boring-tls` crate_
@@ -64,49 +52,13 @@
//! //!
//! #### SMTP over TLS via the rustls crate //! #### SMTP over TLS via the rustls crate
//! //!
//! _Secure SMTP connections using TLS from the `rustls` crate_ //! _Secure SMTP connections using TLS from the `rustls-tls` crate_
//! //!
//! * **rustls**: TLS support for the synchronous version of the API //! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
//! * **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 crypto backends //! * **rustls-tls**: TLS support for the synchronous version of the API
//! //! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
//! _The crypto implementation to use with rustls_ //! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
//!
//! 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 //! ### Sendmail transport
//! //!
@@ -143,7 +95,6 @@
//! * **tracing**: Logging using the `tracing` crate //! * **tracing**: Logging using the `tracing` crate
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct //! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
//! * **dkim**: Add support for signing email with DKIM //! * **dkim**: Add support for signing email with DKIM
//! * **web**: WebAssembly support using the `web-time` crate for time operations
//! //!
//! [`SMTP`]: crate::transport::smtp //! [`SMTP`]: crate::transport::smtp
//! [`sendmail`]: crate::transport::sendmail //! [`sendmail`]: crate::transport::sendmail
@@ -151,23 +102,18 @@
//! [`ContentType`]: crate::message::header::ContentType //! [`ContentType`]: crate::message::header::ContentType
//! [tokio]: https://docs.rs/tokio/1 //! [tokio]: https://docs.rs/tokio/1
//! [async-std]: https://docs.rs/async-std/1 //! [async-std]: https://docs.rs/async-std/1
//! [AWS-LC]: https://github.com/aws/aws-lc //! [ring]: https://github.com/briansmith/ring#ring
//! [`aws-lc-rs`]: https://crates.io/crates/aws-lc-rs //! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
//! [`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 //! [Tokio 1.x]: https://docs.rs/tokio/1
//! [async-std 1.x]: https://docs.rs/async-std/1 //! [async-std 1.x]: https://docs.rs/async-std/1
//! [mime 0.3]: https://docs.rs/mime/0.3 //! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376 //! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.19")] #![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.4")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")] #![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")] #![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny( #![deny(
unreachable_pub,
missing_copy_implementations, missing_copy_implementations,
trivial_casts, trivial_casts,
trivial_numeric_casts, trivial_numeric_casts,
@@ -191,36 +137,12 @@
clippy::wildcard_imports, clippy::wildcard_imports,
clippy::str_to_string, clippy::str_to_string,
clippy::empty_structs_with_brackets, 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_attr(docsrs, feature(doc_cfg))]
#[cfg(not(lettre_ignore_tls_mismatch))] #[cfg(not(lettre_ignore_tls_mismatch))]
mod compiletime_checks { 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"))] #[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 compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
the executable will fail to link."); the executable will fail to link.");
@@ -231,12 +153,16 @@ mod compiletime_checks {
not(feature = "tokio1-native-tls") 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. 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` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features. If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(feature = "tokio1", feature = "rustls", not(feature = "tokio1-rustls")))] #[cfg(all(
compile_error!("Lettre is being built with the `tokio1` and the `rustls` features, but the `tokio1-rustls` feature hasn't been turned on. feature = "tokio1",
If you'd like to use `native-tls` make sure that the `rustls` feature hasn't been enabled by mistake. 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.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all( #[cfg(all(
@@ -245,22 +171,36 @@ mod compiletime_checks {
not(feature = "tokio1-boring-tls") 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. 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` feature hasn't been enabled by mistake. If you'd like to use `boring-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(feature = "async-std1", feature = "native-tls"))] /*
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
not(feature = "async-std1-native-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the `async-std1-native-tls` feature hasn't been turned on.
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
*/
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
not(feature = "async-std1-native-tls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet. compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576. If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
If you were trying to opt into `rustls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features. If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all( #[cfg(all(
feature = "async-std1", feature = "async-std1",
feature = "rustls", feature = "rustls-tls",
not(feature = "async-std1-rustls") not(feature = "async-std1-rustls-tls")
))] ))]
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. 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` hasn't been enabled by mistake. If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
} }
@@ -273,9 +213,6 @@ mod executor;
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))] #[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub mod message; pub mod message;
#[cfg(feature = "rustls")]
mod rustls_crypto;
mod time;
pub mod transport; pub mod transport;
use std::error::Error as StdError; use std::error::Error as StdError;

View File

@@ -16,10 +16,7 @@ enum Disposition {
/// File name /// File name
Attached(String), Attached(String),
/// Content id /// Content id
Inline { Inline(String),
content_id: String,
name: Option<String>,
},
} }
impl Attachment { impl Attachment {
@@ -84,50 +81,7 @@ impl Attachment {
/// ``` /// ```
pub fn new_inline(content_id: String) -> Self { pub fn new_inline(content_id: String) -> Self {
Attachment { Attachment {
disposition: Disposition::Inline { disposition: Disposition::Inline(content_id),
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),
},
} }
} }
@@ -141,18 +95,9 @@ impl Attachment {
Disposition::Attached(filename) => { Disposition::Attached(filename) => {
builder.header(header::ContentDisposition::attachment(&filename)) builder.header(header::ContentDisposition::attachment(&filename))
} }
Disposition::Inline { Disposition::Inline(content_id) => builder
content_id,
name: None,
} => builder
.header(header::ContentId::from(format!("<{content_id}>"))) .header(header::ContentId::from(format!("<{content_id}>")))
.header(header::ContentDisposition::inline()), .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 = builder.header(content_type);
builder.body(content) builder.body(content)
@@ -197,24 +142,4 @@ 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"
)
);
}
} }

View File

@@ -69,7 +69,7 @@ impl Display for DkimSigningAlgorithm {
} }
} }
/// Describe [`DkimSigningKey`] key error /// Describe DkimSigning key error
#[derive(Debug)] #[derive(Debug)]
pub struct DkimSigningKeyError(InnerDkimSigningKeyError); 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)] #[derive(Debug)]
pub struct DkimSigningKey(InnerDkimSigningKey); pub struct DkimSigningKey(InnerDkimSigningKey);
@@ -183,7 +183,7 @@ impl DkimConfig {
} }
} }
/// Create a [`DkimConfig`] /// Create a DkimConfig
pub fn new( pub fn new(
selector: String, selector: String,
domain: String, domain: String,
@@ -283,19 +283,19 @@ fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
// End of header. // End of header.
[b'\r', b'\n', ..] => { [b'\r', b'\n', ..] => {
*out += "\r\n"; *out += "\r\n";
name(&h[2..], out); name(&h[2..], out)
} }
// Sequential whitespace. // Sequential whitespace.
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out), [b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
// All whitespace becomes spaces. // All whitespace becomes spaces.
[b'\t', ..] => { [b'\t', ..] => {
out.push(' '); out.push(' ');
value(&h[1..], out); value(&h[1..], out)
} }
[_, ..] => { [_, ..] => {
let mut chars = h.chars(); let mut chars = h.chars();
out.push(chars.next().unwrap()); 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>( fn dkim_canonicalize_headers<'a>(
headers_list: impl IntoIterator<Item = &'a str>, headers_list: impl IntoIterator<Item = &'a str>,
mail_headers: &Headers, mail_headers: &Headers,
@@ -344,9 +344,10 @@ fn dkim_canonicalize_headers<'a>(
} }
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by /// 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) { pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
dkim_sign_fixed_time(message, dkim_config, crate::time::now()); dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
} }
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) { fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
@@ -377,7 +378,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 dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
let signed_headers = dkim_canonicalize_headers( let signed_headers = dkim_canonicalize_headers(
dkim_config.headers.iter().map(AsRef::as_ref), dkim_config.headers.iter().map(|h| h.as_ref()),
headers, headers,
dkim_config.canonicalization.header, dkim_config.canonicalization.header,
); );
@@ -487,14 +488,14 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
fn test_headers_simple_canonicalize() { fn test_headers_simple_canonicalize() {
let message = test_message(); let message = test_message();
dbg!(message.headers.to_string()); 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] #[test]
fn test_headers_relaxed_canonicalize() { fn test_headers_relaxed_canonicalize() {
let message = test_message(); let message = test_message();
dbg!(message.headers.to_string()); 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] #[test]

View File

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

View File

@@ -119,7 +119,7 @@ mod serde {
{ {
struct ContentTypeVisitor; struct ContentTypeVisitor;
impl Visitor<'_> for ContentTypeVisitor { impl<'de> Visitor<'de> for ContentTypeVisitor {
type Value = ContentType; type Value = ContentType;
// The error message which states what the Visitor expects to // The error message which states what the Visitor expects to

View File

@@ -21,7 +21,7 @@ impl Date {
/// ///
/// Shortcut for `Date::new(SystemTime::now())` /// Shortcut for `Date::new(SystemTime::now())`
pub fn now() -> Self { pub fn now() -> Self {
Self::new(crate::time::now()) Self::new(SystemTime::now())
} }
} }

View File

@@ -1,4 +1,4 @@
use email_encoding::headers::writer::EmailWriter; use email_encoding::headers::EmailWriter;
use super::{Header, HeaderName, HeaderValue}; use super::{Header, HeaderName, HeaderValue};
use crate::{ use crate::{
@@ -31,7 +31,7 @@ macro_rules! mailbox_header {
let mut encoded_value = String::new(); let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len(); let line_len = $header_name.len() + ": ".len();
{ {
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false); let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
self.0.encode(&mut w).expect("writing `Mailbox` returned an error"); self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
} }
@@ -81,7 +81,7 @@ macro_rules! mailboxes_header {
let mut encoded_value = String::new(); let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len(); let line_len = $header_name.len() + ": ".len();
{ {
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false); let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error"); self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
} }
@@ -110,7 +110,7 @@ mailbox_header! {
`Sender` header `Sender` header
This header contains [`Mailbox`] associated with sender. This header contains [`Mailbox`][self::Mailbox] associated with sender.
```no_test ```no_test
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap()) header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
@@ -124,7 +124,7 @@ mailboxes_header! {
`From` header `From` header
This header contains [`Mailboxes`]. This header contains [`Mailboxes`][self::Mailboxes].
*/ */
(From, "From") (From, "From")
@@ -135,7 +135,7 @@ mailboxes_header! {
`Reply-To` header `Reply-To` header
This header contains [`Mailboxes`]. This header contains [`Mailboxes`][self::Mailboxes].
*/ */
(ReplyTo, "Reply-To") (ReplyTo, "Reply-To")
@@ -146,7 +146,7 @@ mailboxes_header! {
`To` header `To` header
This header contains [`Mailboxes`]. This header contains [`Mailboxes`][self::Mailboxes].
*/ */
(To, "To") (To, "To")
@@ -157,7 +157,7 @@ mailboxes_header! {
`Cc` header `Cc` header
This header contains [`Mailboxes`]. This header contains [`Mailboxes`][self::Mailboxes].
*/ */
(Cc, "Cc") (Cc, "Cc")
@@ -168,7 +168,7 @@ mailboxes_header! {
`Bcc` header `Bcc` header
This header contains [`Mailboxes`]. This header contains [`Mailboxes`][self::Mailboxes].
*/ */
(Bcc, "Bcc") (Bcc, "Bcc")

View File

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

View File

@@ -41,14 +41,14 @@ fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space // FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
// obs-FWS // obs-FWS
pub(super) fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> { pub fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
rfc2234::wsp() rfc2234::wsp()
.or_not() .or_not()
.then_ignore(rfc2234::wsp().ignored().repeated()) .then_ignore(rfc2234::wsp().ignored().repeated())
} }
// CFWS = *([FWS] comment) (([FWS] comment) / FWS) // CFWS = *([FWS] comment) (([FWS] comment) / FWS)
pub(super) fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> { pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
// TODO: comment are not currently supported, so for now a cfws is // TODO: comment are not currently supported, so for now a cfws is
// the same as a fws. // the same as a fws.
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] // dot-atom = [CFWS] dot-atom-text [CFWS]
pub(super) fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(dot_atom_text()) cfws().chain(dot_atom_text())
} }
// dot-atom-text = 1*atext *("." 1*atext) // dot-atom-text = 1*atext *("." 1*atext)
pub(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atext().repeated().at_least(1).chain( atext().repeated().at_least(1).chain(
just('.') just('.')
.chain(atext().repeated().at_least(1)) .chain(atext().repeated().at_least(1))
@@ -170,9 +170,7 @@ fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// mailbox = name-addr / addr-spec // mailbox = name-addr / addr-spec
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>>
{ {
choice((name_addr(), addr_spec().map(|addr| (None, addr)))) choice((name_addr(), addr_spec().map(|addr| (None, addr)))).then_ignore(end())
.padded()
.then_ignore(end())
} }
// name-addr = [display-name] angle-addr // name-addr = [display-name] angle-addr
@@ -204,7 +202,7 @@ pub(crate) fn mailbox_list(
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1 // https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
// addr-spec = local-part "@" domain // addr-spec = local-part "@" domain
pub(super) fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> { pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
local_part() local_part()
.collect() .collect()
.then_ignore(just('@')) .then_ignore(just('@'))
@@ -212,12 +210,12 @@ pub(super) fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<c
} }
// local-part = dot-atom / quoted-string / obs-local-part // local-part = dot-atom / quoted-string / obs-local-part
pub(super) fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((dot_atom(), quoted_string(), obs_local_part())) choice((dot_atom(), quoted_string(), obs_local_part()))
} }
// domain = dot-atom / domain-literal / obs-domain // domain = dot-atom / domain-literal / obs-domain
pub(super) fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// NOTE: omitting domain-literal since it may never be used // NOTE: omitting domain-literal since it may never be used
choice((dot_atom(), obs_domain())) choice((dot_atom(), obs_domain()))
} }
@@ -240,11 +238,11 @@ fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4 // https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
// obs-local-part = word *("." word) // obs-local-part = word *("." word)
pub(super) fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
word().chain(just('.').chain(word()).repeated().flatten()) word().chain(just('.').chain(word()).repeated().flatten())
} }
// obs-domain = atom *("." atom) // obs-domain = atom *("." atom)
pub(super) fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> { pub fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atom().chain(just('.').chain(atom()).repeated().flatten()) atom().chain(just('.').chain(atom()).repeated().flatten())
} }

View File

@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Mailbox {
{ {
struct FieldVisitor; struct FieldVisitor;
impl Visitor<'_> for FieldVisitor { impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field; type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {

View File

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

View File

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

View File

@@ -217,7 +217,7 @@ mod mimebody;
use crate::{ use crate::{
address::Envelope, address::Envelope,
message::header::{ContentTransferEncoding, Header, HeaderValue, Headers, MailboxesHeader}, message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
Error as EmailError, Error as EmailError,
}; };
@@ -277,7 +277,7 @@ impl MessageBuilder {
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted /// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
/// if no date has been provided. /// if no date has been provided.
pub fn date_now(self) -> Self { pub fn date_now(self) -> Self {
self.date(crate::time::now()) self.date(SystemTime::now())
} }
/// Set or add mailbox to `ReplyTo` header /// Set or add mailbox to `ReplyTo` header
@@ -345,7 +345,7 @@ impl MessageBuilder {
let hostname = hostname::get() let hostname = hostname::get()
.map_err(|_| ()) .map_err(|_| ())
.and_then(|s| s.into_string().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"))] #[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned(); let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
@@ -369,12 +369,6 @@ impl MessageBuilder {
self 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 /// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self { pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
match self.headers.get::<H>() { match self.headers.get::<H>() {
@@ -463,12 +457,12 @@ impl MessageBuilder {
self.build(MessageBody::Raw(body.into_vec())) self.build(MessageBody::Raw(body.into_vec()))
} }
/// Create message using mime body ([`MultiPart`]) /// Create message using mime body ([`MultiPart`][self::MultiPart])
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> { pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Multi(part))) self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
} }
/// Create message using mime body ([`SinglePart`]) /// Create message using mime body ([`SinglePart`][self::SinglePart])
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> { pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Single(part))) self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
} }
@@ -533,7 +527,7 @@ impl Message {
match &self.body { match &self.body {
MessageBody::Mime(p) => p.format_body(&mut out), MessageBody::Mime(p) => p.format_body(&mut out),
MessageBody::Raw(r) => out.extend_from_slice(r), MessageBody::Raw(r) => out.extend_from_slice(r),
} };
out.extend_from_slice(b"\r\n"); out.extend_from_slice(b"\r\n");
out out
} }
@@ -543,10 +537,7 @@ impl Message {
/// Example: /// Example:
/// ```rust /// ```rust
/// use lettre::{ /// use lettre::{
/// message::{ /// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
/// dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
/// header::ContentType,
/// },
/// Message, /// Message,
/// }; /// };
/// ///
@@ -555,7 +546,6 @@ impl Message {
/// .reply_to("Bob <bob@example.org>".parse().unwrap()) /// .reply_to("Bob <bob@example.org>".parse().unwrap())
/// .to("Carla <carla@example.net>".parse().unwrap()) /// .to("Carla <carla@example.net>".parse().unwrap())
/// .subject("Hello") /// .subject("Hello")
/// .header(ContentType::TEXT_PLAIN)
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned()) /// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
/// .unwrap(); /// .unwrap();
/// let key = "-----BEGIN RSA PRIVATE KEY----- /// let key = "-----BEGIN RSA PRIVATE KEY-----
@@ -611,7 +601,7 @@ impl EmailFormat for Message {
MessageBody::Mime(p) => p.format(out), MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => { MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n"); out.extend_from_slice(b"\r\n");
out.extend_from_slice(r); out.extend_from_slice(r)
} }
} }
} }
@@ -717,10 +707,7 @@ mod test {
.header(header::To( .header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(), vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
)) ))
.raw_header(header::HeaderValue::new( .header(header::Subject::from(String::from("яңа ел белән!")))
header::HeaderName::new_from_ascii_str("Subject"),
"яңа ел белән!".to_owned(),
))
.body(String::from("Happy new year!")) .body(String::from("Happy new year!"))
.unwrap(); .unwrap();
@@ -778,7 +765,7 @@ mod test {
continue; continue;
} }
assert_eq!(line.0, line.1); assert_eq!(line.0, line.1)
} }
} }

View File

@@ -1,14 +0,0 @@
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)
})
}

View File

@@ -1,26 +0,0 @@
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()
}

View File

@@ -34,7 +34,6 @@ impl Error {
/// Returns true if the error is an envelope serialization or deserialization error /// Returns true if the error is an envelope serialization or deserialization error
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
pub fn is_envelope(&self) -> bool { pub fn is_envelope(&self) -> bool {
matches!(self.inner.kind, Kind::Envelope) matches!(self.inner.kind, Kind::Envelope)
} }
@@ -69,7 +68,7 @@ impl fmt::Display for Error {
Kind::Io => f.write_str("response error")?, Kind::Io => f.write_str("response error")?,
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
Kind::Envelope => f.write_str("internal client error")?, Kind::Envelope => f.write_str("internal client error")?,
} };
if let Some(e) = &self.inner.source { if let Some(e) = &self.inner.source {
write!(f, ": {e}")?; write!(f, ": {e}")?;

View File

@@ -11,7 +11,7 @@
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! //!
//! use lettre::{message::header::ContentType, FileTransport, Message, Transport}; //! use lettre::{FileTransport, Message, Transport};
//! //!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir()); //! let sender = FileTransport::new(temp_dir());
@@ -20,10 +20,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! sender.send(&email)?; //! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! //!
@@ -44,7 +44,7 @@
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! //!
//! use lettre::{message::header::ContentType, FileTransport, Message, Transport}; //! use lettre::{FileTransport, Message, Transport};
//! //!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = FileTransport::with_envelope(temp_dir()); //! let sender = FileTransport::with_envelope(temp_dir());
@@ -53,10 +53,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! sender.send(&email)?; //! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! //!
@@ -73,9 +73,7 @@
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! //!
//! use lettre::{ //! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
//! message::header::ContentType, AsyncFileTransport, AsyncTransport, Message, Tokio1Executor,
//! };
//! //!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir()); //! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
@@ -84,10 +82,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! sender.send(email).await?; //! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```
@@ -101,10 +99,7 @@
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir; //! use std::env::temp_dir;
//! //!
//! use lettre::{ //! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
//! message::header::ContentType, AsyncFileTransport, AsyncStd1Executor, AsyncTransport,
//! Message,
//! };
//! //!
//! // Write to the local temp directory //! // Write to the local temp directory
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir()); //! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
@@ -113,10 +108,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! sender.send(email).await?; //! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```
@@ -130,7 +125,6 @@
//! Reply-To: Yuin <yuin@domain.tld> //! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld> //! To: Hei <hei@domain.tld>
//! Subject: Happy new year //! Subject: Happy new year
//! Content-Type: text/plain; charset=utf-8
//! Date: Tue, 18 Aug 2020 22:50:17 GMT //! Date: Tue, 18 Aug 2020 22:50:17 GMT
//! //!
//! Be happy! //! Be happy!
@@ -173,7 +167,7 @@ pub struct FileTransport {
} }
/// Asynchronously writes the content and the envelope information to a file /// Asynchronously writes the content and the envelope information to a file
#[derive(Debug)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))] #[cfg(any(feature = "async-std1", feature = "tokio1"))]
@@ -199,7 +193,6 @@ impl FileTransport {
/// Writes the email content in eml format and the envelope /// Writes the email content in eml format and the envelope
/// in json format. /// in json format.
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
pub fn with_envelope<P: AsRef<Path>>(path: P) -> FileTransport { pub fn with_envelope<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport { FileTransport {
path: PathBuf::from(path.as_ref()), path: PathBuf::from(path.as_ref()),
@@ -212,7 +205,6 @@ impl FileTransport {
/// ///
/// Reads the envelope and the raw message content. /// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")] #[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> { pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
use std::fs; use std::fs;
@@ -251,7 +243,6 @@ where
/// Writes the email content in eml format and the envelope /// Writes the email content in eml format and the envelope
/// in json format. /// in json format.
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
pub fn with_envelope<P: AsRef<Path>>(path: P) -> Self { pub fn with_envelope<P: AsRef<Path>>(path: P) -> Self {
Self { Self {
inner: FileTransport::with_envelope(path), inner: FileTransport::with_envelope(path),
@@ -263,7 +254,6 @@ where
/// ///
/// Reads the envelope and the raw message content. /// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")] #[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> { 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_file = self.inner.path.join(format!("{email_id}.eml"));
let eml = E::fs_read(&eml_file).await.map_err(error::io)?; let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
@@ -276,16 +266,6 @@ 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 { impl Transport for FileTransport {
type Ok = Id; type Ok = Id;
type Error = Error; type Error = Error;

View File

@@ -12,7 +12,7 @@
//! //!
//! * a service from your Cloud or hosting provider //! * a service from your Cloud or hosting provider
//! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either //! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either
//! locally on your servers or accessible over the network //! locally on your servers or accessible over the network
//! * a dedicated external service, like Mailchimp, Mailgun, etc. //! * a dedicated external service, like Mailchimp, Mailgun, etc.
//! //!
//! In most cases, the best option is to: //! In most cases, the best option is to:
@@ -56,17 +56,13 @@
//! # //! #
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))] //! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{ //! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
//! message::header::ContentType, transport::smtp::authentication::Credentials, Message,
//! SmtpTransport, Transport,
//! };
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned()); //! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
@@ -140,10 +136,6 @@ pub trait Transport {
} }
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>; 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 /// Async Transport method for emails
@@ -170,8 +162,4 @@ pub trait AsyncTransport {
} }
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>; 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) {}
} }

View File

@@ -65,7 +65,7 @@ impl fmt::Display for Error {
match self.inner.kind { match self.inner.kind {
Kind::Response => f.write_str("response error")?, Kind::Response => f.write_str("response error")?,
Kind::Client => f.write_str("internal client error")?, Kind::Client => f.write_str("internal client error")?,
} };
if let Some(e) = &self.inner.source { if let Some(e) = &self.inner.source {
write!(f, ": {e}")?; write!(f, ": {e}")?;

View File

@@ -7,18 +7,18 @@
//! # //! #
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))] //! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{message::header::ContentType, Message, SendmailTransport, Transport}; //! use lettre::{Message, SendmailTransport, Transport};
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! let sender = SendmailTransport::new(); //! let sender = SendmailTransport::new();
//! sender.send(&email)?; //! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! //!
@@ -34,8 +34,7 @@
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))] //! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{ //! use lettre::{
//! message::header::ContentType, AsyncSendmailTransport, AsyncTransport, Message, //! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
//! SendmailTransport, Tokio1Executor,
//! }; //! };
//! //!
//! let email = Message::builder() //! let email = Message::builder()
@@ -43,11 +42,11 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! let sender = AsyncSendmailTransport::<Tokio1Executor>::new(); //! let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
//! sender.send(email).await?; //! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```
@@ -59,17 +58,18 @@
//! # //! #
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))] //! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> { //! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor,message::header::ContentType, AsyncSendmailTransport}; //! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
//! //!
//! let email = Message::builder() //! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?) //! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?) //! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year").header(ContentType::TEXT_PLAIN) //! .subject("Happy new year")
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new(); //! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
//! sender.send(email).await?; //! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```
@@ -120,7 +120,7 @@ impl SendmailTransport {
/// Creates a new transport with the `sendmail` command /// Creates a new transport with the `sendmail` command
/// ///
/// Note: This uses the `sendmail` command in the current `PATH`. To use another 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 { pub fn new() -> SendmailTransport {
SendmailTransport { SendmailTransport {
command: DEFAULT_SENDMAIL.into(), command: DEFAULT_SENDMAIL.into(),
@@ -157,7 +157,7 @@ where
/// Creates a new transport with the `sendmail` command /// Creates a new transport with the `sendmail` command
/// ///
/// Note: This uses the `sendmail` command in the current `PATH`. To use another 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 { pub fn new() -> Self {
Self { Self {
inner: SendmailTransport::new(), inner: SendmailTransport::new(),

View File

@@ -12,12 +12,6 @@ use async_trait::async_trait;
use super::pool::async_impl::Pool; use super::pool::async_impl::Pool;
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
use super::PoolConfig; use super::PoolConfig;
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls",
feature = "async-std1-rustls"
))]
use super::Tls;
use super::{ use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo, client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
}; };
@@ -30,30 +24,6 @@ use crate::Tokio1Executor;
use crate::{Envelope, Executor}; use crate::{Envelope, Executor};
/// Asynchronously sends emails using the SMTP protocol /// 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"))))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransport<E: Executor> { pub struct AsyncSmtpTransport<E: Executor> {
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
@@ -75,15 +45,10 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?; let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))] #[cfg(not(feature = "pool"))]
conn.abort().await; conn.quit().await?;
Ok(result) Ok(result)
} }
async fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown().await;
}
} }
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
@@ -102,11 +67,6 @@ impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
Ok(result) Ok(result)
} }
async fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown().await;
}
} }
impl<E> AsyncSmtpTransport<E> impl<E> AsyncSmtpTransport<E>
@@ -121,15 +81,16 @@ where
/// to validate TLS certificates. /// to validate TLS certificates.
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "async-std1-rustls" feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any( doc(cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "async-std1-rustls" feature = "async-std1-rustls-tls"
))) )))
)] )]
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> { pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
@@ -155,15 +116,16 @@ where
/// or emails will be sent to the server, protecting from downgrade attacks. /// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "async-std1-rustls" feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any( doc(cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "async-std1-rustls" feature = "async-std1-rustls-tls"
))) )))
)] )]
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> { pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
@@ -201,72 +163,54 @@ where
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL /// Creates a `AsyncSmtpTransportBuilder` from a connection URL
/// ///
/// The protocol, credentials, host, port and EHLO name can be provided /// The protocol, credentials, host and port can be provided in a single URL.
/// in a single URL. This may be simpler than having to configure SMTP /// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
/// through multiple configuration parameters and then having to pass /// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
/// those options to lettre. /// 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 URL is created in the following way: /// <table>
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`. /// <thead>
/// /// <tr>
/// `user` (Username) and `pass` (Password) are optional in case the /// <th>scheme</th>
/// SMTP relay doesn't require authentication. When `port` is not /// <th>tls parameter</th>
/// configured it is automatically determined based on the `scheme`. /// <th>example</th>
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO /// <th>remarks</th>
/// command. `TLS` controls whether STARTTLS is simply enabled /// </tr>
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks) /// </thead>
/// or `required` (require the server to upgrade the connection to /// <tbody>
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt). /// <tr>
/// /// <td>smtps</td>
/// Use the following table to construct your SMTP url: /// <td>-</td>
/// /// <td>smtps://smtp.example.com</td>
/// | scheme | `tls` query parameter | example | default port | remarks | /// <td>SMTP over TLS, recommended method</td>
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | /// </tr>
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method | /// <tr>
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available | /// <td>smtp</td>
/// | `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 | /// <td>required</td>
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted | /// <td>smtp://smtp.example.com?tls=required</td>
/// /// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
/// IMPORTANT: some parameters like `user` and `pass` cannot simply /// </tr>
/// be concatenated to construct the final URL because special characters /// <tr>
/// contained within the parameter may confuse the URL decoder. /// <td>smtp</td>
/// Manually URL encode the parameters before concatenating them or use /// <td>opportunistic</td>
/// a proper URL encoder, like the following cargo script: /// <td>smtp://smtp.example.com?tls=opportunistic</td>
/// /// <td>
/// ```rust /// SMTP with optionally STARTTLS when supported by the server.
/// # const TOML: &str = r#" /// Caution: this method is vulnerable to a man-in-the-middle attack.
/// #!/usr/bin/env cargo /// Not recommended for production use.
/// /// </td>
/// //! ```cargo /// </tr>
/// //! [dependencies] /// <tr>
/// //! url = "2" /// <td>smtp</td>
/// //! ``` /// <td>-</td>
/// # "#; /// <td>smtp://smtp.example.com</td>
/// /// <td>Unencrypted SMTP, not recommended for production use.</td>
/// use url::Url; /// </tr>
/// /// </tbody>
/// fn main() { /// </table>
/// // 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 /// ```rust,no_run
/// use lettre::{ /// use lettre::{
@@ -290,18 +234,22 @@ where
/// let mailer: AsyncSmtpTransport<Tokio1Executor> = /// let mailer: AsyncSmtpTransport<Tokio1Executor> =
/// AsyncSmtpTransport::<Tokio1Executor>::from_url( /// AsyncSmtpTransport::<Tokio1Executor>::from_url(
/// "smtps://username:password@smtp.example.com:465", /// "smtps://username:password@smtp.example.com:465",
/// )? /// )
/// .unwrap()
/// .build(); /// .build();
/// ///
/// // Send the email /// // Send the email
/// mailer.send(email).await?; /// match mailer.send(email).await {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
pub fn from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> { pub fn from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
super::connection_url::from_connection_url(connection_url) super::connection_url::from_connection_url(connection_url)
@@ -377,7 +325,7 @@ impl AsyncSmtpTransportBuilder {
self self
} }
/// Set the authentication credentials to use /// Set the authentication mechanism to use
pub fn credentials(mut self, credentials: Credentials) -> Self { pub fn credentials(mut self, credentials: Credentials) -> Self {
self.info.credentials = Some(credentials); self.info.credentials = Some(credentials);
self self
@@ -390,17 +338,6 @@ impl AsyncSmtpTransportBuilder {
} }
/// Set the port to use /// 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 { pub fn port(mut self, port: u16) -> Self {
self.info.port = port; self.info.port = port;
self self
@@ -413,31 +350,21 @@ impl AsyncSmtpTransportBuilder {
} }
/// Set the TLS settings to use /// 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( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "async-std1-rustls" feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))] ))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any( doc(cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "async-std1-rustls" feature = "async-std1-rustls-tls"
))) )))
)] )]
pub fn tls(mut self, tls: Tls) -> Self { pub fn tls(mut self, tls: super::Tls) -> Self {
self.info.tls = tls; self.info.tls = tls;
self self
} }
@@ -470,7 +397,7 @@ impl AsyncSmtpTransportBuilder {
} }
/// Build client /// Build client
pub(super) struct AsyncSmtpClient<E> { pub struct AsyncSmtpClient<E> {
info: SmtpInfo, info: SmtpInfo,
marker_: PhantomData<E>, marker_: PhantomData<E>,
} }
@@ -482,7 +409,7 @@ where
/// Creates a new connection directly usable to send emails /// Creates a new connection directly usable to send emails
/// ///
/// Handles encryption and authentication /// Handles encryption and authentication
pub(super) async fn connection(&self) -> Result<AsyncSmtpConnection, Error> { pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
let mut conn = E::connect( let mut conn = E::connect(
&self.info.server, &self.info.server,
self.info.port, self.info.port,

View File

@@ -98,17 +98,13 @@ impl Mechanism {
let decoded_challenge = challenge let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?; .ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if contains_ignore_ascii_case( if ["User Name", "Username:", "Username", "User Name\0"]
decoded_challenge, .contains(&decoded_challenge)
["User Name", "Username:", "Username", "User Name\0"], {
) {
return Ok(credentials.authentication_identity.clone()); return Ok(credentials.authentication_identity.clone());
} }
if contains_ignore_ascii_case( if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
decoded_challenge,
["Password", "Password:", "Password\0"],
) {
return Ok(credentials.secret.clone()); return Ok(credentials.secret.clone());
} }
@@ -125,15 +121,6 @@ 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)] #[cfg(test)]
mod test { mod test {
use super::{Credentials, Mechanism}; use super::{Credentials, Mechanism};
@@ -168,23 +155,6 @@ mod test {
assert!(mechanism.response(&credentials, None).is_err()); 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] #[test]
fn test_xoauth2() { fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2; let mechanism = Mechanism::Xoauth2;

View File

@@ -6,8 +6,7 @@ use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use super::async_net::AsyncTokioStream; use super::async_net::AsyncTokioStream;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use super::escape_crlf; use super::escape_crlf;
#[allow(deprecated)] use super::{AsyncNetworkStream, ClientCodec, ConnectionState, TlsParameters};
use super::{async_net::AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{ use crate::{
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
@@ -20,26 +19,11 @@ use crate::{
Envelope, Envelope,
}; };
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort().await;
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
pub struct AsyncSmtpConnection { pub struct AsyncSmtpConnection {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
#[allow(deprecated)]
stream: BufReader<AsyncNetworkStream>, stream: BufReader<AsyncNetworkStream>,
/// Panic state
panic: bool,
/// Information about the server /// Information about the server
server_info: ServerInfo, server_info: ServerInfo,
} }
@@ -54,12 +38,10 @@ impl AsyncSmtpConnection {
/// ///
/// Sends EHLO and parses server information /// Sends EHLO and parses server information
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_with_transport( pub async fn connect_with_transport(
stream: Box<dyn AsyncTokioStream>, stream: Box<dyn AsyncTokioStream>,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
#[allow(deprecated)]
let stream = AsyncNetworkStream::use_existing_tokio1(stream); let stream = AsyncNetworkStream::use_existing_tokio1(stream);
Self::connect_impl(stream, hello_name).await Self::connect_impl(stream, hello_name).await
} }
@@ -90,12 +72,12 @@ impl AsyncSmtpConnection {
/// Some(TlsParameters::new("example.com".to_owned())?), /// Some(TlsParameters::new("example.com".to_owned())?),
/// None, /// None,
/// ) /// )
/// .await?; /// .await
/// .unwrap();
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>( pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
@@ -103,7 +85,6 @@ impl AsyncSmtpConnection {
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
local_address: Option<IpAddr>, local_address: Option<IpAddr>,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
#[allow(deprecated)]
let stream = let stream =
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address) AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
.await?; .await?;
@@ -114,19 +95,16 @@ impl AsyncSmtpConnection {
/// ///
/// Sends EHLO and parses server information /// Sends EHLO and parses server information
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>( pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
#[allow(deprecated)]
let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?; let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await Self::connect_impl(stream, hello_name).await
} }
#[allow(deprecated)]
async fn connect_impl( async fn connect_impl(
stream: AsyncNetworkStream, stream: AsyncNetworkStream,
hello_name: &ClientId, hello_name: &ClientId,
@@ -134,7 +112,6 @@ impl AsyncSmtpConnection {
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = AsyncSmtpConnection { let mut conn = AsyncSmtpConnection {
stream, stream,
panic: false,
server_info: ServerInfo::default(), server_info: ServerInfo::default(),
}; };
// TODO log // TODO log
@@ -178,30 +155,26 @@ impl AsyncSmtpConnection {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
try_smtp!( self.command(Mail::new(envelope.from().cloned(), mail_options))
self.command(Mail::new(envelope.from().cloned(), mail_options)) .await?;
.await,
self
);
// Recipient // Recipient
for to_address in envelope.to() { for to_address in envelope.to() {
try_smtp!( self.command(Rcpt::new(to_address.clone(), vec![])).await?;
self.command(Rcpt::new(to_address.clone(), vec![])).await,
self
);
} }
// Data // Data
try_smtp!(self.command(Data).await, self); self.command(Data).await?;
// Message content // Message content
let result = try_smtp!(self.message(email).await, self); self.message(email).await
Ok(result)
} }
pub fn has_broken(&self) -> bool { pub fn has_broken(&self) -> bool {
self.panic match self.stream.get_ref().state() {
ConnectionState::Ok => false,
ConnectionState::Broken | ConnectionState::Closed => true,
}
} }
pub fn can_starttls(&self) -> bool { pub fn can_starttls(&self) -> bool {
@@ -216,18 +189,20 @@ impl AsyncSmtpConnection {
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314 /// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
#[allow(unused_variables)] #[allow(unused_variables)]
pub async fn starttls( pub async fn starttls(
&mut self, mut self,
tls_parameters: TlsParameters, tls_parameters: TlsParameters,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<(), Error> { ) -> Result<Self, Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
try_smtp!(self.command(Starttls).await, self); self.command(Starttls).await?;
self.stream.get_mut().upgrade_tls(tls_parameters).await?; let stream = self.stream.into_inner();
let stream = stream.upgrade_tls(tls_parameters).await?;
self.stream = BufReader::new(stream);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
try_smtp!(self.ehlo(hello_name).await, self); self.ehlo(hello_name).await?;
Ok(()) Ok(self)
} else { } else {
Err(error::client("STARTTLS is not supported on this server")) Err(error::client("STARTTLS is not supported on this server"))
} }
@@ -235,26 +210,27 @@ impl AsyncSmtpConnection {
/// Send EHLO and update server info /// Send EHLO and update server info
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> { async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self); let ehlo_response = self.command(Ehlo::new(hello_name.clone())).await?;
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self); self.server_info = ServerInfo::from_response(&ehlo_response)?;
Ok(()) Ok(())
} }
pub async fn quit(&mut self) -> Result<Response, Error> { pub async fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit).await, self)) self.command(Quit).await
} }
pub async fn abort(&mut self) { pub async fn abort(&mut self) {
// Only try to quit if we are not already broken match self.stream.get_ref().state() {
if !self.panic { ConnectionState::Ok | ConnectionState::Broken => {
self.panic = true; let _ = self.command(Quit).await;
let _ = self.command(Quit).await; let _ = self.stream.close().await;
self.stream.get_mut().set_state(ConnectionState::Closed);
}
ConnectionState::Closed => {}
} }
let _ = self.stream.close().await;
} }
/// Sets the underlying stream /// Sets the underlying stream
#[allow(deprecated)]
pub fn set_stream(&mut self, stream: AsyncNetworkStream) { pub fn set_stream(&mut self, stream: AsyncNetworkStream) {
self.stream = BufReader::new(stream); self.stream = BufReader::new(stream);
} }
@@ -288,15 +264,13 @@ impl AsyncSmtpConnection {
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = try_smtp!( response = self
self.command(Auth::new_from_response( .command(Auth::new_from_response(
mechanism, mechanism,
credentials.clone(), credentials.clone(),
&response, &response,
)?) )?)
.await, .await?;
self
);
} }
if challenges == 0 { if challenges == 0 {
@@ -324,6 +298,9 @@ impl AsyncSmtpConnection {
/// Writes a string to the server /// Writes a string to the server
async fn write(&mut self, string: &[u8]) -> Result<(), Error> { async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
self.stream self.stream
.get_mut() .get_mut()
.write_all(string) .write_all(string)
@@ -335,6 +312,8 @@ impl AsyncSmtpConnection {
.await .await
.map_err(error::network)?; .map_err(error::network)?;
self.stream.get_mut().set_state(ConnectionState::Ok);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string))); tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
Ok(()) Ok(())
@@ -342,6 +321,9 @@ impl AsyncSmtpConnection {
/// Gets the SMTP response /// Gets the SMTP response
pub async fn read_response(&mut self) -> Result<Response, Error> { pub async fn read_response(&mut self) -> Result<Response, Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
let mut buffer = String::with_capacity(100); let mut buffer = String::with_capacity(100);
while self while self
@@ -355,6 +337,8 @@ impl AsyncSmtpConnection {
tracing::debug!("<< {}", escape_crlf(&buffer)); tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) { match parse_response(&buffer) {
Ok((_remaining, response)) => { Ok((_remaining, response)) => {
self.stream.get_mut().set_state(ConnectionState::Ok);
return if response.is_positive() { return if response.is_positive() {
Ok(response) Ok(response)
} else { } else {
@@ -362,7 +346,7 @@ impl AsyncSmtpConnection {
response.code(), response.code(),
Some(response.message().collect()), Some(response.message().collect()),
)) ))
} };
} }
Err(nom::Err::Failure(e)) => { Err(nom::Err::Failure(e)) => {
return Err(error::response(e.to_string())); return Err(error::response(e.to_string()));
@@ -378,36 +362,8 @@ impl AsyncSmtpConnection {
} }
/// The X509 certificate of the server (DER encoded) /// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate() self.stream.get_ref().peer_certificate()
} }
/// 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", 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()
}
} }

View File

@@ -6,15 +6,16 @@ use std::{
time::Duration, time::Duration,
}; };
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs}; use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
use futures_io::{ use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Result as IoResult,
Result as IoResult,
}; };
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsStream; use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(any(feature = "tokio1-rustls", feature = "async-std1-rustls"))] #[cfg(any(feature = "tokio1-rustls-tls", feature = "async-std1-rustls-tls"))]
use rustls::pki_types::ServerName; use rustls::pki_types::ServerName;
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
use tokio1_boring::SslStream as Tokio1SslStream; use tokio1_boring::SslStream as Tokio1SslStream;
@@ -27,29 +28,27 @@ use tokio1_crate::net::{
}; };
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream; use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsStream; use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls", feature = "tokio1-boring-tls",
feature = "async-std1-rustls" feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))] ))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::{ConnectionState, TlsParameters};
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
use crate::transport::smtp::client::net::resolved_address_filter; use crate::transport::smtp::client::net::resolved_address_filter;
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// A network stream /// A network stream
#[derive(Debug)] #[derive(Debug)]
#[deprecated(
since = "0.11.14",
note = "This struct was not meant to be made public"
)]
pub struct AsyncNetworkStream { pub struct AsyncNetworkStream {
inner: InnerAsyncNetworkStream, inner: InnerAsyncNetworkStream,
state: ConnectionState,
} }
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
@@ -78,8 +77,8 @@ enum InnerAsyncNetworkStream {
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>), Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
Tokio1Rustls(Tokio1RustlsStream<Box<dyn AsyncTokioStream>>), Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>), Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
@@ -87,20 +86,27 @@ enum InnerAsyncNetworkStream {
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
AsyncStd1Tcp(AsyncStd1TcpStream), AsyncStd1Tcp(AsyncStd1TcpStream),
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-native-tls")]
AsyncStd1Rustls(AsyncStd1RustlsStream<AsyncStd1TcpStream>), AsyncStd1NativeTls(AsyncStd1TlsStream<AsyncStd1TcpStream>),
/// Can't be built /// Encrypted Tokio 1.x TCP stream
None, #[cfg(feature = "async-std1-rustls-tls")]
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
} }
#[allow(deprecated)]
impl AsyncNetworkStream { impl AsyncNetworkStream {
fn new(inner: InnerAsyncNetworkStream) -> Self { fn new(inner: InnerAsyncNetworkStream) -> Self {
if let InnerAsyncNetworkStream::None = inner { AsyncNetworkStream {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); inner,
state: ConnectionState::Ok,
} }
}
AsyncNetworkStream { inner } pub(super) fn state(&self) -> ConnectionState {
self.state
}
pub(super) fn set_state(&mut self, state: ConnectionState) {
self.state = state;
} }
/// Returns peer's address /// Returns peer's address
@@ -112,31 +118,25 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(s) => { InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
s.get_ref().get_ref().get_ref().peer_addr() s.get_ref().get_ref().get_ref().peer_addr()
} }
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => s.get_ref().0.peer_addr(), InnerAsyncNetworkStream::Tokio1RustlsTls(s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(), InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(), InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => s.get_ref().0.peer_addr(), InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => s.get_ref().peer_addr(),
InnerAsyncNetworkStream::None => { #[cfg(feature = "async-std1-rustls-tls")]
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
Err(IoError::other(
"InnerAsyncNetworkStream::None must never be built",
))
}
} }
} }
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream { pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream)) AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
} }
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>( pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
@@ -176,7 +176,7 @@ impl AsyncNetworkStream {
last_err = Some(io::Error::new( last_err = Some(io::Error::new(
io::ErrorKind::TimedOut, io::ErrorKind::TimedOut,
"connection timed out", "connection timed out",
)); ))
} }
} }
} else { } else {
@@ -197,13 +197,12 @@ impl AsyncNetworkStream {
let mut stream = let mut stream =
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream))); AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?; stream = stream.upgrade_tls(tls_parameters).await?;
} }
Ok(stream) Ok(stream)
} }
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>( pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
@@ -229,7 +228,7 @@ impl AsyncNetworkStream {
last_err = Some(io::Error::new( last_err = Some(io::Error::new(
io::ErrorKind::TimedOut, io::ErrorKind::TimedOut,
"connection timed out", "connection timed out",
)); ))
} }
} }
} }
@@ -249,70 +248,67 @@ impl AsyncNetworkStream {
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream)); let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?; stream = stream.upgrade_tls(tls_parameters).await?;
} }
Ok(stream) Ok(stream)
} }
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> { pub async fn upgrade_tls(self, tls_parameters: TlsParameters) -> Result<Self, Error> {
match &self.inner { match self.inner {
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
not(any( not(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls" feature = "tokio1-boring-tls"
)) ))
))] ))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => { InnerAsyncNetworkStream::Tokio1Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls feature"); panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
} }
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls" feature = "tokio1-boring-tls"
))] ))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => { InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => {
// get owned TcpStream let inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) = tcp_stream else {
unreachable!()
};
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(()) Ok(Self {
inner,
state: ConnectionState::Ok,
})
} }
#[cfg(all(feature = "async-std1", not(feature = "async-std1-rustls")))] #[cfg(all(
feature = "async-std1",
not(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))
))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled the async-std1-rustls feature"); panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the async-std1-native-tls or the async-std1-rustls-tls feature");
} }
#[cfg(feature = "async-std1-rustls")] #[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => {
// get owned TcpStream let inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) = tcp_stream else {
unreachable!()
};
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(()) Ok(Self {
inner,
state: ConnectionState::Ok,
})
} }
_ => Ok(()), _ => Ok(self),
} }
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls" feature = "tokio1-boring-tls"
))] ))]
async fn upgrade_tokio1_tls( async fn upgrade_tokio1_tls(
@@ -323,7 +319,7 @@ impl AsyncNetworkStream {
match tls_parameters.connector { match tls_parameters.connector {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls { connector } => { InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio1-native-tls"))] #[cfg(not(feature = "tokio1-native-tls"))]
panic!("built without the tokio1-native-tls feature"); panic!("built without the tokio1-native-tls feature");
@@ -339,12 +335,12 @@ impl AsyncNetworkStream {
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream)) Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
}; };
} }
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerTlsParameters::Rustls { config } => { InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio1-rustls"))] #[cfg(not(feature = "tokio1-rustls-tls"))]
panic!("built without the tokio1-rustls feature"); panic!("built without the tokio1-rustls-tls feature");
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
return { return {
use tokio1_rustls::TlsConnector; use tokio1_rustls::TlsConnector;
@@ -356,21 +352,18 @@ impl AsyncNetworkStream {
.connect(domain.to_owned(), tcp_stream) .connect(domain.to_owned(), tcp_stream)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1Rustls(stream)) Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
}; };
} }
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls { InnerTlsParameters::BoringTls(connector) => {
connector,
accept_invalid_hostnames,
} => {
#[cfg(not(feature = "tokio1-boring-tls"))] #[cfg(not(feature = "tokio1-boring-tls"))]
panic!("built without the tokio1-boring-tls feature"); panic!("built without the tokio1-boring-tls feature");
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
return { return {
let mut config = connector.configure().map_err(error::connection)?; let mut config = connector.configure().map_err(error::connection)?;
config.set_verify_hostname(accept_invalid_hostnames); config.set_verify_hostname(tls_parameters.accept_invalid_hostnames);
let stream = tokio1_boring::connect(config, &domain, tcp_stream) let stream = tokio1_boring::connect(config, &domain, tcp_stream)
.await .await
@@ -382,7 +375,11 @@ impl AsyncNetworkStream {
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(feature = "async-std1-rustls")] #[cfg(any(
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls",
feature = "async-std1-boring-tls"
))]
async fn upgrade_asyncstd1_tls( async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream, tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters, mut tls_parameters: TlsParameters,
@@ -391,15 +388,31 @@ impl AsyncNetworkStream {
match tls_parameters.connector { match tls_parameters.connector {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls { connector } => { InnerTlsParameters::NativeTls(connector) => {
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531"); panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
}
#[cfg(feature = "rustls")]
InnerTlsParameters::Rustls { config } => {
#[cfg(not(feature = "async-std1-rustls"))]
panic!("built without the async-std1-rustls feature");
#[cfg(feature = "async-std1-rustls")] /*
#[cfg(not(feature = "async-std1-native-tls"))]
panic!("built without the async-std1-native-tls feature");
#[cfg(feature = "async-std1-native-tls")]
return {
use async_native_tls::TlsConnector;
// TODO: fix
let connector: TlsConnector = todo!();
// let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::AsyncStd1NativeTls(stream))
};
*/
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "async-std1-rustls-tls"))]
panic!("built without the async-std1-rustls-tls feature");
#[cfg(feature = "async-std1-rustls-tls")]
return { return {
use futures_rustls::TlsConnector; use futures_rustls::TlsConnector;
@@ -411,11 +424,11 @@ impl AsyncNetworkStream {
.connect(domain.to_owned(), tcp_stream) .connect(domain.to_owned(), tcp_stream)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1Rustls(stream)) Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
}; };
} }
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls { .. } => { InnerTlsParameters::BoringTls(connector) => {
panic!("boring-tls isn't supported with async-std yet."); panic!("boring-tls isn't supported with async-std yet.");
} }
} }
@@ -427,83 +440,16 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1Tcp(_) => false, InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true, InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1Rustls(_) => true, InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true, InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false, InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => true, InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
InnerAsyncNetworkStream::None => false, #[cfg(feature = "async-std1-rustls-tls")]
} InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
}
#[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")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
.peer_cert_chain()
.unwrap()
.iter()
.map(|c| c.to_der().map_err(error::tls))
.collect::<Result<Vec<_>, _>>()?),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
@@ -521,8 +467,8 @@ impl AsyncNetworkStream {
.unwrap() .unwrap()
.to_der() .to_der()
.map_err(error::tls)?), .map_err(error::tls)?),
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
.get_ref() .get_ref()
.1 .1
.peer_certificates() .peer_certificates()
@@ -541,8 +487,10 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted")) Err(error::client("Connection is not encrypted"))
} }
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream InnerAsyncNetworkStream::AsyncStd1NativeTls(t) => panic!("Unsupported"),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
.get_ref() .get_ref()
.1 .1
.peer_certificates() .peer_certificates()
@@ -550,12 +498,10 @@ impl AsyncNetworkStream {
.first() .first()
.unwrap() .unwrap()
.to_vec()), .to_vec()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
} }
#[allow(deprecated)]
impl FuturesAsyncRead for AsyncNetworkStream { impl FuturesAsyncRead for AsyncNetworkStream {
fn poll_read( fn poll_read(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
@@ -581,8 +527,8 @@ impl FuturesAsyncRead for AsyncNetworkStream {
Poll::Pending => Poll::Pending, Poll::Pending => Poll::Pending,
} }
} }
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => { InnerAsyncNetworkStream::Tokio1RustlsTls(s) => {
let mut b = Tokio1ReadBuf::new(buf); let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) { match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())), Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -601,17 +547,14 @@ impl FuturesAsyncRead for AsyncNetworkStream {
} }
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf), InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_read(cx, buf), InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_read(cx, buf),
InnerAsyncNetworkStream::None => { #[cfg(feature = "async-std1-rustls-tls")]
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
Poll::Ready(Ok(0))
}
} }
} }
} }
#[allow(deprecated)]
impl FuturesAsyncWrite for AsyncNetworkStream { impl FuturesAsyncWrite for AsyncNetworkStream {
fn poll_write( fn poll_write(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
@@ -623,18 +566,16 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::None => { #[cfg(feature = "async-std1-rustls-tls")]
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
Poll::Ready(Ok(0))
}
} }
} }
@@ -644,39 +585,37 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => { #[cfg(feature = "async-std1-rustls-tls")]
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
Poll::Ready(Ok(()))
}
} }
} }
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> { fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
self.state = ConnectionState::Closed;
match &mut self.inner { match &mut self.inner {
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-rustls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-boring-tls")] #[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx), InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-rustls")] #[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_close(cx), InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => { #[cfg(feature = "async-std1-rustls-tls")]
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built"); InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
Poll::Ready(Ok(()))
}
} }
} }
} }

View File

@@ -7,7 +7,7 @@ use std::{
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use super::escape_crlf; use super::escape_crlf;
use super::{ClientCodec, NetworkStream, TlsParameters}; use super::{ClientCodec, ConnectionState, NetworkStream, TlsParameters};
use crate::{ use crate::{
address::Envelope, address::Envelope,
transport::smtp::{ transport::smtp::{
@@ -20,25 +20,11 @@ use crate::{
}, },
}; };
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort();
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
pub struct SmtpConnection { pub struct SmtpConnection {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: BufReader<NetworkStream>, stream: BufReader<NetworkStream>,
/// Panic state
panic: bool,
/// Information about the server /// Information about the server
server_info: ServerInfo, server_info: ServerInfo,
} }
@@ -65,7 +51,6 @@ impl SmtpConnection {
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = SmtpConnection { let mut conn = SmtpConnection {
stream, stream,
panic: false,
server_info: ServerInfo::default(), server_info: ServerInfo::default(),
}; };
conn.set_timeout(timeout).map_err(error::network)?; conn.set_timeout(timeout).map_err(error::network)?;
@@ -110,26 +95,25 @@ impl SmtpConnection {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
try_smtp!( self.command(Mail::new(envelope.from().cloned(), mail_options))?;
self.command(Mail::new(envelope.from().cloned(), mail_options)),
self
);
// Recipient // Recipient
for to_address in envelope.to() { for to_address in envelope.to() {
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self); self.command(Rcpt::new(to_address.clone(), vec![]))?;
} }
// Data // Data
try_smtp!(self.command(Data), self); self.command(Data)?;
// Message content // Message content
let result = try_smtp!(self.message(email), self); self.message(email)
Ok(result)
} }
pub fn has_broken(&self) -> bool { pub fn has_broken(&self) -> bool {
self.panic match self.stream.get_ref().state() {
ConnectionState::Ok => false,
ConnectionState::Broken | ConnectionState::Closed => true,
}
} }
pub fn can_starttls(&self) -> bool { pub fn can_starttls(&self) -> bool {
@@ -138,22 +122,28 @@ impl SmtpConnection {
#[allow(unused_variables)] #[allow(unused_variables)]
pub fn starttls( pub fn starttls(
&mut self, mut self,
tls_parameters: &TlsParameters, tls_parameters: &TlsParameters,
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<(), Error> { ) -> Result<Self, Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
{ {
try_smtp!(self.command(Starttls), self); self.command(Starttls)?;
self.stream.get_mut().upgrade_tls(tls_parameters)?; let stream = self.stream.into_inner();
let stream = stream.upgrade_tls(tls_parameters)?;
self.stream = BufReader::new(stream);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("connection encrypted"); tracing::debug!("connection encrypted");
// Send EHLO again // Send EHLO again
try_smtp!(self.ehlo(hello_name), self); self.ehlo(hello_name)?;
Ok(()) Ok(self)
} }
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))] #[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
// This should never happen as `Tls` can only be created // This should never happen as `Tls` can only be created
// when a TLS library is enabled // when a TLS library is enabled
unreachable!("TLS support required but not supported"); unreachable!("TLS support required but not supported");
@@ -164,22 +154,24 @@ impl SmtpConnection {
/// Send EHLO and update server info /// Send EHLO and update server info
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> { fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self); let ehlo_response = self.command(Ehlo::new(hello_name.clone()))?;
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self); self.server_info = ServerInfo::from_response(&ehlo_response)?;
Ok(()) Ok(())
} }
pub fn quit(&mut self) -> Result<Response, Error> { pub fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit), self)) self.command(Quit)
} }
pub fn abort(&mut self) { pub fn abort(&mut self) {
// Only try to quit if we are not already broken match self.stream.get_ref().state() {
if !self.panic { ConnectionState::Ok | ConnectionState::Broken => {
self.panic = true; let _ = self.command(Quit);
let _ = self.command(Quit); let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
self.stream.get_mut().set_state(ConnectionState::Closed);
}
ConnectionState::Closed => {}
} }
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -220,14 +212,11 @@ impl SmtpConnection {
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = try_smtp!( response = self.command(Auth::new_from_response(
self.command(Auth::new_from_response( mechanism,
mechanism, credentials.clone(),
credentials.clone(), &response,
&response, )?)?;
)?),
self
);
} }
if challenges == 0 { if challenges == 0 {
@@ -256,12 +245,17 @@ impl SmtpConnection {
/// Writes a string to the server /// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> { fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
self.stream self.stream
.get_mut() .get_mut()
.write_all(string) .write_all(string)
.map_err(error::network)?; .map_err(error::network)?;
self.stream.get_mut().flush().map_err(error::network)?; self.stream.get_mut().flush().map_err(error::network)?;
self.stream.get_mut().set_state(ConnectionState::Ok);
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string))); tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
Ok(()) Ok(())
@@ -269,6 +263,9 @@ impl SmtpConnection {
/// Gets the SMTP response /// Gets the SMTP response
pub fn read_response(&mut self) -> Result<Response, Error> { pub fn read_response(&mut self) -> Result<Response, Error> {
self.stream.get_ref().state().verify()?;
self.stream.get_mut().set_state(ConnectionState::Broken);
let mut buffer = String::with_capacity(100); let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 { while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
@@ -276,6 +273,8 @@ impl SmtpConnection {
tracing::debug!("<< {}", escape_crlf(&buffer)); tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) { match parse_response(&buffer) {
Ok((_remaining, response)) => { Ok((_remaining, response)) => {
self.stream.get_mut().set_state(ConnectionState::Ok);
return if response.is_positive() { return if response.is_positive() {
Ok(response) Ok(response)
} else { } else {
@@ -299,36 +298,8 @@ impl SmtpConnection {
} }
/// The X509 certificate of the server (DER encoded) /// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate() self.stream.get_ref().peer_certificate()
} }
/// 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", 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()
}
} }

View File

@@ -28,19 +28,19 @@ use std::fmt::Debug;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_connection::AsyncSmtpConnection; pub use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[allow(deprecated)]
pub use self::async_net::AsyncNetworkStream; pub use self::async_net::AsyncNetworkStream;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub use self::async_net::AsyncTokioStream; pub use self::async_net::AsyncTokioStream;
use self::net::NetworkStream; use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub(super) use self::tls::InnerTlsParameters; pub(super) use self::tls::InnerTlsParameters;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub use self::tls::TlsVersion; pub use self::tls::TlsVersion;
pub use self::{ pub use self::{
connection::SmtpConnection, connection::SmtpConnection,
tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder}, tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
}; };
use super::{error, Error};
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_connection; mod async_connection;
@@ -50,6 +50,23 @@ mod connection;
mod net; mod net;
mod tls; mod tls;
#[derive(Debug, Copy, Clone)]
enum ConnectionState {
Ok,
Broken,
Closed,
}
impl ConnectionState {
fn verify(&mut self) -> Result<(), Error> {
match self {
Self::Ok => Ok(()),
Self::Broken => Err(error::connection("connection broken")),
Self::Closed => Err(error::connection("connection closed")),
}
}
}
/// The codec used for transparency /// The codec used for transparency
#[derive(Debug)] #[derive(Debug)]
struct ClientCodec { struct ClientCodec {
@@ -58,7 +75,7 @@ struct ClientCodec {
impl ClientCodec { impl ClientCodec {
/// Creates a new client codec /// Creates a new client codec
pub(crate) fn new() -> Self { pub fn new() -> Self {
Self { Self {
status: CodecStatus::StartOfNewLine, status: CodecStatus::StartOfNewLine,
} }
@@ -140,7 +157,7 @@ mod test {
} }
#[test] #[test]
#[cfg(feature = "tracing")] #[cfg(feature = "log")]
fn test_escape_crlf() { fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CRLF>"); assert_eq!(escape_crlf("\r\n"), "<CRLF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>"); assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");

View File

@@ -1,9 +1,8 @@
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
use std::sync::Arc; use std::sync::Arc;
use std::{ use std::{
io::{self, Read, Write}, io::{self, Read, Write},
mem, net::{IpAddr, Shutdown, SocketAddr, TcpStream, ToSocketAddrs},
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
time::Duration, time::Duration,
}; };
@@ -11,18 +10,19 @@ use std::{
use boring::ssl::SslStream; use boring::ssl::SslStream;
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::TlsStream; use native_tls::TlsStream;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
use rustls::{pki_types::ServerName, ClientConnection, StreamOwned}; use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
use socket2::{Domain, Protocol, Type}; use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::{ConnectionState, TlsParameters};
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// A network stream /// A network stream
pub struct NetworkStream { pub struct NetworkStream {
inner: InnerNetworkStream, inner: InnerNetworkStream,
state: ConnectionState,
} }
/// Represents the different types of underlying network streams /// Represents the different types of underlying network streams
@@ -36,21 +36,26 @@ enum InnerNetworkStream {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
NativeTls(TlsStream<TcpStream>), NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream /// Encrypted TCP stream
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
Rustls(StreamOwned<ClientConnection, TcpStream>), RustlsTls(StreamOwned<ClientConnection, TcpStream>),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
BoringTls(SslStream<TcpStream>), BoringTls(SslStream<TcpStream>),
/// Can't be built
None,
} }
impl NetworkStream { impl NetworkStream {
fn new(inner: InnerNetworkStream) -> Self { fn new(inner: InnerNetworkStream) -> Self {
if let InnerNetworkStream::None = inner { NetworkStream {
debug_assert!(false, "InnerNetworkStream::None must never be built"); inner,
state: ConnectionState::Ok,
} }
}
NetworkStream { inner } pub(super) fn state(&self) -> ConnectionState {
self.state
}
pub(super) fn set_state(&mut self, state: ConnectionState) {
self.state = state;
} }
/// Returns peer's address /// Returns peer's address
@@ -59,34 +64,25 @@ impl NetworkStream {
InnerNetworkStream::Tcp(s) => s.peer_addr(), InnerNetworkStream::Tcp(s) => s.peer_addr(),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(), InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(s) => s.get_ref().peer_addr(), InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(), InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
)))
}
} }
} }
/// Shutdowns the connection /// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> { pub fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
self.state = ConnectionState::Closed;
match &self.inner { match &self.inner {
InnerNetworkStream::Tcp(s) => s.shutdown(how), InnerNetworkStream::Tcp(s) => s.shutdown(how),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how), InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(s) => s.get_ref().shutdown(how), InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how), InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
@@ -119,12 +115,12 @@ impl NetworkStream {
if let Some(timeout) = timeout { if let Some(timeout) = timeout {
match socket.connect_timeout(&addr.into(), timeout) { match socket.connect_timeout(&addr.into(), timeout) {
Ok(()) => return Ok(socket.into()), Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err), Err(err) => last_err = Some(err),
} }
} else { } else {
match socket.connect(&addr.into()) { match socket.connect(&addr.into()) {
Ok(()) => return Ok(socket.into()), Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err), Err(err) => last_err = Some(err),
} }
} }
@@ -139,65 +135,63 @@ impl NetworkStream {
let tcp_stream = try_connect(server, timeout, local_addr)?; let tcp_stream = try_connect(server, timeout, local_addr)?;
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream)); let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters)?; stream = stream.upgrade_tls(tls_parameters)?;
} }
Ok(stream) Ok(stream)
} }
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> { pub fn upgrade_tls(self, tls_parameters: &TlsParameters) -> Result<Self, Error> {
match &self.inner { match self.inner {
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))] #[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an NetworkStream without having enabled either the `native-tls` or the `rustls` feature"); panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
} }
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(tcp_stream) => {
// get owned TcpStream let inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None); Ok(Self {
let InnerNetworkStream::Tcp(tcp_stream) = tcp_stream else { inner,
unreachable!() state: ConnectionState::Ok,
}; })
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
Ok(())
} }
_ => Ok(()), _ => Ok(self),
} }
} }
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
fn upgrade_tls_impl( fn upgrade_tls_impl(
tcp_stream: TcpStream, tcp_stream: TcpStream,
tls_parameters: &TlsParameters, tls_parameters: &TlsParameters,
) -> Result<InnerNetworkStream, Error> { ) -> Result<InnerNetworkStream, Error> {
Ok(match &tls_parameters.connector { Ok(match &tls_parameters.connector {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls { connector } => { InnerTlsParameters::NativeTls(connector) => {
let stream = connector let stream = connector
.connect(tls_parameters.domain(), tcp_stream) .connect(tls_parameters.domain(), tcp_stream)
.map_err(error::connection)?; .map_err(error::connection)?;
InnerNetworkStream::NativeTls(stream) InnerNetworkStream::NativeTls(stream)
} }
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerTlsParameters::Rustls { config } => { InnerTlsParameters::RustlsTls(connector) => {
let domain = ServerName::try_from(tls_parameters.domain()) let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?; .map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connection = ClientConnection::new(Arc::clone(config), domain.to_owned()) let connection = ClientConnection::new(Arc::clone(connector), domain.to_owned())
.map_err(error::connection)?; .map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream); let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::Rustls(stream) InnerNetworkStream::RustlsTls(stream)
} }
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls { InnerTlsParameters::BoringTls(connector) => {
connector,
accept_invalid_hostnames,
} => {
let stream = connector let stream = connector
.configure() .configure()
.map_err(error::connection)? .map_err(error::connection)?
.verify_hostname(*accept_invalid_hostnames) .verify_hostname(tls_parameters.accept_invalid_hostnames)
.connect(tls_parameters.domain(), tcp_stream) .connect(tls_parameters.domain(), tcp_stream)
.map_err(error::connection)?; .map_err(error::connection)?;
InnerNetworkStream::BoringTls(stream) InnerNetworkStream::BoringTls(stream)
@@ -210,66 +204,14 @@ impl NetworkStream {
InnerNetworkStream::Tcp(_) => false, InnerNetworkStream::Tcp(_) => false,
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true, InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(_) => true, InnerNetworkStream::RustlsTls(_) => true,
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(_) => true, InnerNetworkStream::BoringTls(_) => true,
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
false
}
} }
} }
#[cfg(feature = "boring-tls")] #[cfg(any(feature = "native-tls", feature = "rustls-tls", 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")]
InnerNetworkStream::Rustls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
.peer_cert_chain()
.unwrap()
.iter()
.map(|c| c.to_der().map_err(error::tls))
.collect::<Result<Vec<_>, _>>()?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
#[cfg(any(feature = "native-tls", feature = "rustls", 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> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner { match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")), InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
@@ -280,8 +222,8 @@ impl NetworkStream {
.unwrap() .unwrap()
.to_der() .to_der()
.map_err(error::tls)?), .map_err(error::tls)?),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(stream) => Ok(stream InnerNetworkStream::RustlsTls(stream) => Ok(stream
.conn .conn
.peer_certificates() .peer_certificates()
.unwrap() .unwrap()
@@ -295,7 +237,6 @@ impl NetworkStream {
.unwrap() .unwrap()
.to_der() .to_der()
.map_err(error::tls)?), .map_err(error::tls)?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
@@ -304,14 +245,10 @@ impl NetworkStream {
InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration), InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration), InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_read_timeout(duration), InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration), InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
@@ -322,14 +259,10 @@ impl NetworkStream {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration), InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_write_timeout(duration), InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration), InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
} }
@@ -340,14 +273,10 @@ impl Read for NetworkStream {
InnerNetworkStream::Tcp(s) => s.read(buf), InnerNetworkStream::Tcp(s) => s.read(buf),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.read(buf), InnerNetworkStream::NativeTls(s) => s.read(buf),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(s) => s.read(buf), InnerNetworkStream::RustlsTls(s) => s.read(buf),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.read(buf), InnerNetworkStream::BoringTls(s) => s.read(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
} }
} }
} }
@@ -358,14 +287,10 @@ impl Write for NetworkStream {
InnerNetworkStream::Tcp(s) => s.write(buf), InnerNetworkStream::Tcp(s) => s.write(buf),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.write(buf), InnerNetworkStream::NativeTls(s) => s.write(buf),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(s) => s.write(buf), InnerNetworkStream::RustlsTls(s) => s.write(buf),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.write(buf), InnerNetworkStream::BoringTls(s) => s.write(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
} }
} }
@@ -374,14 +299,10 @@ impl Write for NetworkStream {
InnerNetworkStream::Tcp(s) => s.flush(), InnerNetworkStream::Tcp(s) => s.flush(),
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(s) => s.flush(), InnerNetworkStream::NativeTls(s) => s.flush(),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::Rustls(s) => s.flush(), InnerNetworkStream::RustlsTls(s) => s.flush(),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(s) => s.flush(), InnerNetworkStream::BoringTls(s) => s.flush(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
} }
} }
} }
@@ -389,7 +310,7 @@ impl Write for NetworkStream {
/// If the local address is set, binds the socket to this address. /// 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 /// If local address is not set, then destination address is required to determine the default
/// local address on some platforms. /// 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( fn bind_local_address(
socket: &socket2::Socket, socket: &socket2::Socket,
dst_addr: &SocketAddr, dst_addr: &SocketAddr,

View File

@@ -1,31 +1,29 @@
use std::fmt::{self, Debug}; use std::fmt::{self, Debug};
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
use std::sync::Arc; use std::{io, sync::Arc};
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
use boring::{ use boring::{
pkey::PKey,
ssl::{SslConnector, SslVersion}, ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder, x509::store::X509StoreBuilder,
}; };
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
use rustls::{ use rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}, crypto::{verify_tls12_signature, verify_tls13_signature},
pki_types::{self, pem::PemObject, CertificateDer, PrivateKeyDer, ServerName, UnixTime}, pki_types::{CertificateDer, ServerName, UnixTime},
server::ParsedCertificate,
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme, ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
}; };
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// TLS protocol versions. /// TLS protocol versions.
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
#[non_exhaustive] #[non_exhaustive]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub enum TlsVersion { pub enum TlsVersion {
/// TLS 1.0 /// TLS 1.0
/// ///
@@ -58,75 +56,31 @@ pub enum TlsVersion {
Tlsv13, Tlsv13,
} }
/// Specifies how to establish a TLS connection /// How to apply TLS to a client connection
///
/// TLDR: Use [`Tls::Wrapper`] or [`Tls::Required`] when
/// connecting to a remote server, [`Tls::None`] when
/// connecting to a local server.
#[derive(Clone)] #[derive(Clone)]
#[allow(missing_copy_implementations)] #[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 { pub enum Tls {
/// Insecure (plaintext) connection only. /// Insecure connection only (for testing purposes)
///
/// 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, None,
/// Begin with a plaintext connection and attempt to use `STARTTLS` if available. /// Start with insecure connection and use `STARTTLS` when available
/// #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
/// 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( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
Opportunistic(TlsParameters), Opportunistic(TlsParameters),
/// Begin with a plaintext connection and require `STARTTLS` for security. /// Start with insecure connection and require `STARTTLS`
/// #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
/// 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( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
Required(TlsParameters), Required(TlsParameters),
/// Establish a connection wrapped in TLS from the start. /// Use TLS wrapped connection
/// #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
/// 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( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
Wrapper(TlsParameters), Wrapper(TlsParameters),
} }
@@ -135,11 +89,11 @@ impl Debug for Tls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self { match &self {
Self::None => f.pad("None"), Self::None => f.pad("None"),
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"), Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Required(_) => f.pad("Required"), Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"), Self::Wrapper(_) => f.pad("Wrapper"),
} }
} }
@@ -148,25 +102,14 @@ impl Debug for Tls {
/// Source for the base set of root certificates to trust. /// Source for the base set of root certificates to trust.
#[allow(missing_copy_implementations)] #[allow(missing_copy_implementations)]
#[derive(Clone, Debug, Default)] #[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 { pub enum CertificateStore {
/// Use the default for the TLS backend. /// Use the default for the TLS backend.
/// ///
/// For native-tls, this will use the system certificate store on Windows, the keychain on /// For native-tls, this will use the system certificate store on Windows, the keychain on
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`). /// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
/// ///
/// For rustls, this will use the system certificate verifier if the `rustls-platform-verifier` /// For rustls, this will also use the the system store if the `rustls-native-certs` feature is
/// feature is enabled. If the `rustls-native-certs` feature is enabled, system certificate /// enabled, or will fall back to `webpki-roots`.
/// 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. /// The boring-tls backend uses the same logic as OpenSSL on all platforms.
#[default] #[default]
@@ -174,7 +117,7 @@ pub enum CertificateStore {
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate. /// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
/// ///
/// This option is only available in the rustls backend. /// This option is only available in the rustls backend.
#[cfg(all(feature = "rustls", feature = "webpki-roots"))] #[cfg(feature = "rustls-tls")]
WebpkiRoots, WebpkiRoots,
/// Don't use any system certificates. /// Don't use any system certificates.
None, None,
@@ -182,42 +125,23 @@ pub enum CertificateStore {
/// Parameters to use for secure clients /// Parameters to use for secure clients
#[derive(Clone)] #[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 struct TlsParameters {
pub(crate) connector: InnerTlsParameters, pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server /// The domain name which is expected in the TLS certificate from the server
pub(super) domain: String, pub(super) domain: String,
#[cfg(feature = "boring-tls")]
pub(super) accept_invalid_hostnames: bool,
} }
/// Builder for `TlsParameters` /// Builder for `TlsParameters`
#[derive(Debug, Clone)] #[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 { pub struct TlsParametersBuilder {
domain: String, domain: String,
cert_store: CertificateStore, cert_store: CertificateStore,
root_certs: Vec<Certificate>, root_certs: Vec<Certificate>,
identity: Option<Identity>,
accept_invalid_hostnames: bool, accept_invalid_hostnames: bool,
accept_invalid_certs: bool, accept_invalid_certs: bool,
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
min_tls_version: TlsVersion, min_tls_version: TlsVersion,
} }
@@ -228,10 +152,9 @@ impl TlsParametersBuilder {
domain, domain,
cert_store: CertificateStore::Default, cert_store: CertificateStore::Default,
root_certs: Vec::new(), root_certs: Vec::new(),
identity: None,
accept_invalid_hostnames: false, accept_invalid_hostnames: false,
accept_invalid_certs: false, accept_invalid_certs: false,
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
min_tls_version: TlsVersion::Tlsv12, min_tls_version: TlsVersion::Tlsv12,
} }
} }
@@ -244,24 +167,14 @@ impl TlsParametersBuilder {
/// Add a custom root certificate /// Add a custom root certificate
/// ///
/// Can be used to safely connect to a server using a self-signed certificate, for example. /// Can be used to safely connect to a server using a self signed certificate, for example.
pub fn add_root_certificate(mut self, cert: Certificate) -> Self { pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
self.root_certs.push(cert); self.root_certs.push(cert);
self self
} }
/// Add a client certificate
///
/// Can be used to configure a client certificate to present to the server.
pub fn identify_with(mut self, identity: Identity) -> Self {
self.identity = Some(identity);
self
}
/// Controls whether certificates with an invalid hostname are accepted /// Controls whether certificates with an invalid hostname are accepted
/// ///
/// This option is silently disabled when using `rustls-platform-verifier`.
///
/// Defaults to `false`. /// Defaults to `false`.
/// ///
/// # Warning /// # Warning
@@ -271,11 +184,10 @@ impl TlsParametersBuilder {
/// including those from other sites, are trusted. /// including those from other sites, are trusted.
/// ///
/// This method introduces significant vulnerabilities to man-in-the-middle attacks. /// This method introduces significant vulnerabilities to man-in-the-middle attacks.
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] ///
#[cfg_attr( /// Hostname verification can only be disabled with the `native-tls` TLS backend.
docsrs, #[cfg(any(feature = "native-tls", feature = "boring-tls"))]
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) #[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
)]
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self { pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames; self.accept_invalid_hostnames = accept_invalid_hostnames;
self self
@@ -284,11 +196,7 @@ impl TlsParametersBuilder {
/// Controls which minimum TLS version is allowed /// Controls which minimum TLS version is allowed
/// ///
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12]. /// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self { pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
self.min_tls_version = min_tls_version; self.min_tls_version = min_tls_version;
self self
@@ -317,17 +225,17 @@ impl TlsParametersBuilder {
/// Creates a new `TlsParameters` using native-tls, boring-tls or rustls /// Creates a new `TlsParameters` using native-tls, boring-tls or rustls
/// depending on which one is available /// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
pub fn build(self) -> Result<TlsParameters, Error> { pub fn build(self) -> Result<TlsParameters, Error> {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
return self.build_rustls(); return self.build_rustls();
#[cfg(all(not(feature = "rustls"), feature = "native-tls"))] #[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
return self.build_native(); return self.build_native();
#[cfg(all(not(feature = "rustls"), feature = "boring-tls"))] #[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
return self.build_boring(); return self.build_boring();
} }
@@ -367,14 +275,12 @@ impl TlsParametersBuilder {
}; };
tls_builder.min_protocol_version(Some(min_tls_version)); tls_builder.min_protocol_version(Some(min_tls_version));
if let Some(identity) = self.identity {
tls_builder.identity(identity.native_tls);
}
let connector = tls_builder.build().map_err(error::tls)?; let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls { connector }, connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain, domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
@@ -411,15 +317,6 @@ impl TlsParametersBuilder {
} }
} }
if let Some(identity) = self.identity {
tls_builder
.set_certificate(identity.boring_tls.0.as_ref())
.map_err(error::tls)?;
tls_builder
.set_private_key(identity.boring_tls.1.as_ref())
.map_err(error::tls)?;
}
let min_tls_version = match self.min_tls_version { let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => SslVersion::TLS1, TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1, TlsVersion::Tlsv11 => SslVersion::TLS1_1,
@@ -432,17 +329,15 @@ impl TlsParametersBuilder {
.map_err(error::tls)?; .map_err(error::tls)?;
let connector = tls_builder.build(); let connector = tls_builder.build();
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::BoringTls { connector: InnerTlsParameters::BoringTls(connector),
connector,
accept_invalid_hostnames: self.accept_invalid_hostnames,
},
domain: self.domain, domain: self.domain,
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
/// Creates a new `TlsParameters` using rustls with the provided configuration /// Creates a new `TlsParameters` using rustls with the provided configuration
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))] #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> { pub fn build_rustls(self) -> Result<TlsParameters, Error> {
let just_version3 = &[&rustls::version::TLS13]; let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version { let supported_versions = match self.min_tls_version {
@@ -456,144 +351,80 @@ impl TlsParametersBuilder {
TlsVersion::Tlsv13 => just_version3, TlsVersion::Tlsv13 => just_version3,
}; };
let crypto_provider = crate::rustls_crypto::crypto_provider(); let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
// Build TLS config let tls = if self.accept_invalid_certs {
let mut root_cert_store = RootCertStore::empty();
#[cfg(all(
not(feature = "rustls-platform-verifier"),
feature = "rustls-native-certs"
))]
fn load_native_roots(store: &mut RootCertStore) {
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
let errors_len = errors.len();
let (added, ignored) = store.add_parsable_certificates(certs);
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
);
#[cfg(not(feature = "tracing"))]
let _ = (errors_len, added, ignored);
}
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
fn load_webpki_roots(store: &mut RootCertStore) {
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
#[cfg_attr(not(feature = "rustls-platform-verifier"), allow(unused_mut))]
let mut extra_roots = None::<Vec<CertificateDer<'static>>>;
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-platform-verifier")]
{
extra_roots = Some(Vec::new());
}
#[cfg(all(
not(feature = "rustls-platform-verifier"),
feature = "rustls-native-certs"
))]
load_native_roots(&mut root_cert_store);
#[cfg(all(
not(feature = "rustls-platform-verifier"),
not(feature = "rustls-native-certs"),
feature = "webpki-roots"
))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
#[cfg(feature = "rustls-platform-verifier")]
if let Some(extra_roots) = &mut extra_roots {
extra_roots.push(rustls_cert.clone());
}
root_cert_store.add(rustls_cert).map_err(error::tls)?;
}
}
let tls = if self.accept_invalid_certs
|| (extra_roots.is_none() && self.accept_invalid_hostnames)
{
let verifier = InvalidCertsVerifier {
ignore_invalid_hostnames: self.accept_invalid_hostnames,
ignore_invalid_certs: self.accept_invalid_certs,
roots: root_cert_store,
crypto_provider,
};
tls.dangerous() tls.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier)) .with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
} else { } else {
#[cfg(feature = "rustls-platform-verifier")] let mut root_cert_store = RootCertStore::empty();
if let Some(extra_roots) = extra_roots {
tls.dangerous().with_custom_certificate_verifier(Arc::new( #[cfg(feature = "rustls-native-certs")]
rustls_platform_verifier::Verifier::new_with_extra_roots( fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
extra_roots, let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
crypto_provider, let (added, ignored) = store.add_parsable_certificates(native_certs);
) #[cfg(feature = "tracing")]
.map_err(error::tls)?, tracing::debug!(
)) "loaded platform certs with {added} valid and {ignored} ignored (invalid) certs"
} else { );
tls.with_root_certificates(root_cert_store) Ok(())
} }
#[cfg(not(feature = "rustls-platform-verifier"))] #[cfg(feature = "rustls-tls")]
{ fn load_webpki_roots(store: &mut RootCertStore) {
tls.with_root_certificates(root_cert_store) store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
} }
};
let tls = if let Some(identity) = self.identity { match self.cert_store {
let (client_certificates, private_key) = identity.rustls_tls; CertificateStore::Default => {
tls.with_client_auth_cert(client_certificates, private_key) #[cfg(feature = "rustls-native-certs")]
.map_err(error::tls)? load_native_roots(&mut root_cert_store)?;
} else { #[cfg(not(feature = "rustls-native-certs"))]
tls.with_no_client_auth() load_webpki_roots(&mut root_cert_store);
}
#[cfg(feature = "rustls-tls")]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(rustls_cert).map_err(error::tls)?;
}
}
tls.with_root_certificates(root_cert_store)
}; };
let tls = tls.with_no_client_auth();
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::Rustls { connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
config: Arc::new(tls),
},
domain: self.domain, domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
} }
#[derive(Clone)] #[derive(Clone)]
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
pub(crate) enum InnerTlsParameters { pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
NativeTls { connector: TlsConnector }, NativeTls(TlsConnector),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
Rustls { config: Arc<ClientConfig> }, RustlsTls(Arc<ClientConfig>),
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
BoringTls { BoringTls(SslConnector),
connector: SslConnector,
accept_invalid_hostnames: bool,
},
} }
impl TlsParameters { impl TlsParameters {
/// Creates a new `TlsParameters` using native-tls or rustls /// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available /// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
pub fn new(domain: String) -> Result<Self, Error> { pub fn new(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build() TlsParametersBuilder::new(domain).build()
@@ -612,8 +443,8 @@ impl TlsParameters {
} }
/// Creates a new `TlsParameters` using rustls /// Creates a new `TlsParameters` using rustls
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))] #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn new_rustls(domain: String) -> Result<Self, Error> { pub fn new_rustls(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_rustls() TlsParametersBuilder::new(domain).build_rustls()
} }
@@ -630,29 +461,19 @@ impl TlsParameters {
} }
} }
/// A certificate that can be used with [`TlsParametersBuilder::add_root_certificate`] /// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
#[derive(Clone)] #[derive(Clone)]
#[allow(missing_copy_implementations)] #[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 { pub struct Certificate {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate, native_tls: native_tls::Certificate,
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
rustls: Vec<CertificateDer<'static>>, rustls: Vec<CertificateDer<'static>>,
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509, boring_tls: boring::x509::X509,
} }
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
impl Certificate { impl Certificate {
/// Create a `Certificate` from a DER encoded certificate /// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> { pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
@@ -665,7 +486,7 @@ impl Certificate {
Ok(Self { Ok(Self {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
rustls: vec![der.into()], rustls: vec![der.into()],
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert, boring_tls: boring_tls_cert,
@@ -680,17 +501,20 @@ impl Certificate {
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?; let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
let rustls_cert = { let rustls_cert = {
CertificateDer::pem_slice_iter(pem) use std::io::Cursor;
.collect::<Result<Vec<_>, pki_types::pem::Error>>()
let mut pem = Cursor::new(pem);
rustls_pemfile::certs(&mut pem)
.collect::<io::Result<Vec<_>>>()
.map_err(|_| error::tls("invalid certificates"))? .map_err(|_| error::tls("invalid certificates"))?
}; };
Ok(Self { Ok(Self {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
rustls: rustls_cert, rustls: rustls_cert,
#[cfg(feature = "boring-tls")] #[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert, boring_tls: boring_tls_cert,
@@ -704,125 +528,20 @@ impl Debug for Certificate {
} }
} }
/// An identity that can be used with [`TlsParametersBuilder::identify_with`] #[cfg(feature = "rustls-tls")]
#[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")]
rustls_tls: (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>),
#[cfg(feature = "boring-tls")]
boring_tls: (boring::x509::X509, PKey<boring::pkey::Private>),
}
impl Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity").finish()
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
Identity {
#[cfg(feature = "native-tls")]
native_tls: self.native_tls.clone(),
#[cfg(feature = "rustls")]
rustls_tls: (self.rustls_tls.0.clone(), self.rustls_tls.1.clone_key()),
#[cfg(feature = "boring-tls")]
boring_tls: (self.boring_tls.0.clone(), self.boring_tls.1.clone()),
}
}
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
impl Identity {
pub fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: Identity::from_pem_native_tls(pem, key)?,
#[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)?,
})
}
#[cfg(feature = "native-tls")]
fn from_pem_native_tls(pem: &[u8], key: &[u8]) -> Result<native_tls::Identity, Error> {
native_tls::Identity::from_pkcs8(pem, key).map_err(error::tls)
}
#[cfg(feature = "rustls")]
fn from_pem_rustls_tls(
pem: &[u8],
key: &[u8],
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
let key = match PrivateKeyDer::from_pem_slice(key) {
Ok(key) => key,
Err(pki_types::pem::Error::NoItemsFound) => {
return Err(error::tls("no private key found"))
}
Err(err) => return Err(error::tls(err)),
};
Ok((vec![pem.to_owned().into()], key))
}
#[cfg(feature = "boring-tls")]
fn from_pem_boring_tls(
pem: &[u8],
key: &[u8],
) -> Result<(boring::x509::X509, PKey<boring::pkey::Private>), Error> {
let cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
let key = boring::pkey::PKey::private_key_from_pem(key).map_err(error::tls)?;
Ok((cert, key))
}
}
#[cfg(feature = "rustls")]
#[derive(Debug)] #[derive(Debug)]
struct InvalidCertsVerifier { struct InvalidCertsVerifier;
ignore_invalid_hostnames: bool,
ignore_invalid_certs: bool,
roots: RootCertStore,
crypto_provider: Arc<CryptoProvider>,
}
#[cfg(feature = "rustls")] #[cfg(feature = "rustls-tls")]
impl ServerCertVerifier for InvalidCertsVerifier { impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert( fn verify_server_cert(
&self, &self,
end_entity: &CertificateDer<'_>, _end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>], _intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>, _server_name: &ServerName<'_>,
_ocsp_response: &[u8], _ocsp_response: &[u8],
now: UnixTime, _now: UnixTime,
) -> Result<ServerCertVerified, TlsError> { ) -> Result<ServerCertVerified, TlsError> {
let cert = ParsedCertificate::try_from(end_entity)?;
if !self.ignore_invalid_certs {
rustls::client::verify_server_cert_signed_by_trust_anchor(
&cert,
&self.roots,
intermediates,
now,
self.crypto_provider.signature_verification_algorithms.all,
)?;
}
if !self.ignore_invalid_hostnames {
rustls::client::verify_server_name(&cert, server_name)?;
}
Ok(ServerCertVerified::assertion()) Ok(ServerCertVerified::assertion())
} }
@@ -836,7 +555,7 @@ impl ServerCertVerifier for InvalidCertsVerifier {
message, message,
cert, cert,
dss, dss,
&self.crypto_provider.signature_verification_algorithms, &rustls::crypto::ring::default_provider().signature_verification_algorithms,
) )
} }
@@ -850,12 +569,12 @@ impl ServerCertVerifier for InvalidCertsVerifier {
message, message,
cert, cert,
dss, dss,
&self.crypto_provider.signature_verification_algorithms, &rustls::crypto::ring::default_provider().signature_verification_algorithms,
) )
} }
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> { fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.crypto_provider rustls::crypto::ring::default_provider()
.signature_verification_algorithms .signature_verification_algorithms
.supported_schemes() .supported_schemes()
} }

View File

@@ -1,8 +1,6 @@
use std::borrow::Cow;
use url::Url; use url::Url;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::client::{Tls, TlsParameters}; use super::client::{Tls, TlsParameters};
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
use super::AsyncSmtpTransportBuilder; use super::AsyncSmtpTransportBuilder;
@@ -13,7 +11,6 @@ use super::{
pub(crate) trait TransportBuilder { pub(crate) trait TransportBuilder {
fn new<T: Into<String>>(server: T) -> Self; 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 tls(self, tls: super::Tls) -> Self;
fn port(self, port: u16) -> Self; fn port(self, port: u16) -> Self;
fn credentials(self, credentials: Credentials) -> Self; fn credentials(self, credentials: Credentials) -> Self;
@@ -25,7 +22,6 @@ impl TransportBuilder for SmtpTransportBuilder {
Self::new(server) Self::new(server)
} }
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
fn tls(self, tls: super::Tls) -> Self { fn tls(self, tls: super::Tls) -> Self {
self.tls(tls) self.tls(tls)
} }
@@ -49,7 +45,6 @@ impl TransportBuilder for AsyncSmtpTransportBuilder {
Self::new(server) Self::new(server)
} }
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
fn tls(self, tls: super::Tls) -> Self { fn tls(self, tls: super::Tls) -> Self {
self.tls(tls) self.tls(tls)
} }
@@ -67,7 +62,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> { 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 connection_url = Url::parse(connection_url).map_err(error::connection)?;
let tls: Option<String> = connection_url let tls: Option<String> = connection_url
@@ -85,30 +80,30 @@ pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) ->
("smtp", None) => { ("smtp", None) => {
builder = builder.port(connection_url.port().unwrap_or(SMTP_PORT)); builder = builder.port(connection_url.port().unwrap_or(SMTP_PORT));
} }
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtp", Some("required")) => { ("smtp", Some("required")) => {
builder = builder builder = builder
.port(connection_url.port().unwrap_or(SUBMISSION_PORT)) .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", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtp", Some("opportunistic")) => { ("smtp", Some("opportunistic")) => {
builder = builder builder = builder
.port(connection_url.port().unwrap_or(SUBMISSION_PORT)) .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", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtps", _) => { ("smtps", _) => {
builder = builder builder = builder
.port(connection_url.port().unwrap_or(SUBMISSIONS_PORT)) .port(connection_url.port().unwrap_or(SUBMISSIONS_PORT))
.tls(Tls::Wrapper(TlsParameters::new(host.into())?)); .tls(Tls::Wrapper(TlsParameters::new(host.into())?))
} }
(scheme, tls) => { (scheme, tls) => {
return Err(error::connection(format!( return Err(error::connection(format!(
"Unknown scheme '{scheme}' or tls parameter '{tls:?}', note that a transport with TLS requires one of the TLS features" "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 // use the path segment of the URL as name in the name in the HELO / EHLO command
if connection_url.path().len() > 1 { if connection_url.path().len() > 1 {
@@ -120,7 +115,7 @@ pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) ->
let percent_decode = |s: &str| { let percent_decode = |s: &str| {
percent_encoding::percent_decode_str(s) percent_encoding::percent_decode_str(s)
.decode_utf8() .decode_utf8()
.map(Cow::into_owned) .map(|cow| cow.into_owned())
.map_err(error::connection) .map_err(error::connection)
}; };
let credentials = Credentials::new( let credentials = Credentials::new(

View File

@@ -68,20 +68,15 @@ impl Error {
} }
/// Returns true if the error is from TLS /// Returns true if the error is from TLS
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
pub fn is_tls(&self) -> bool { pub fn is_tls(&self) -> bool {
matches!(self.inner.kind, Kind::Tls) 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. /// Returns the status code, if the error was generated from a response.
pub fn status(&self) -> Option<Code> { pub fn status(&self) -> Option<Code> {
match self.inner.kind { match self.inner.kind {
@@ -112,12 +107,10 @@ pub(crate) enum Kind {
/// TLS error /// TLS error
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls, Tls,
/// Transport shutdown error
TransportShutdown,
} }
impl fmt::Debug for Error { impl fmt::Debug for Error {
@@ -141,16 +134,15 @@ impl fmt::Display for Error {
Kind::Client => f.write_str("internal client error")?, Kind::Client => f.write_str("internal client error")?,
Kind::Network => f.write_str("network error")?, Kind::Network => f.write_str("network error")?,
Kind::Connection => f.write_str("Connection error")?, Kind::Connection => f.write_str("Connection error")?,
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Kind::Tls => f.write_str("tls error")?, Kind::Tls => f.write_str("tls error")?,
Kind::TransportShutdown => f.write_str("transport has been shut down")?,
Kind::Transient(code) => { Kind::Transient(code) => {
write!(f, "transient error ({code})")?; write!(f, "transient error ({code})")?;
} }
Kind::Permanent(code) => { Kind::Permanent(code) => {
write!(f, "permanent error ({code})")?; write!(f, "permanent error ({code})")?;
} }
} };
if let Some(e) = &self.inner.source { if let Some(e) = &self.inner.source {
write!(f, ": {e}")?; write!(f, ": {e}")?;
@@ -193,11 +185,7 @@ pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Connection, Some(e)) Error::new(Kind::Connection, Some(e))
} }
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error { pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Tls, Some(e)) Error::new(Kind::Tls, Some(e))
} }
pub(crate) fn transport_shutdown() -> Error {
Error::new::<BoxError>(Kind::TransportShutdown, None)
}

View File

@@ -129,8 +129,9 @@ impl Display for ServerInfo {
impl ServerInfo { impl ServerInfo {
/// Parses a EHLO response to create a `ServerInfo` /// Parses a EHLO response to create a `ServerInfo`
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> { pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let Some(name) = response.first_word() else { let name = match response.first_word() {
return Err(error::response("Could not read server name")); Some(name) => name,
None => return Err(error::response("Could not read server name")),
}; };
let mut features: HashSet<Extension> = HashSet::new(); let mut features: HashSet<Extension> = HashSet::new();
@@ -168,7 +169,7 @@ impl ServerInfo {
} }
} }
_ => (), _ => (),
} };
} }
Ok(ServerInfo { Ok(ServerInfo {
@@ -290,8 +291,14 @@ impl Display for RcptParameter {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::collections::HashSet;
use super::*; use super::*;
use crate::transport::smtp::response::{Category, Code, Detail, Severity}; use crate::transport::smtp::{
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
};
#[test] #[test]
fn test_clientid_fmt() { fn test_clientid_fmt() {

View File

@@ -26,17 +26,43 @@
//! //!
//! The relay server can be the local email server, a specific host or a third-party service. //! The relay server can be the local email server, a specific host or a third-party service.
//! //!
//! #### Simple example with authentication //! #### Simple example
//! //!
//! A good starting point for sending emails via SMTP relay is to //! This is the most basic example of usage:
//! do the following:
//! //!
//! ```rust,no_run //! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))] //! # #[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")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> { //! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{ //! use lettre::{
//! message::header::ContentType, //! transport::smtp::{
//! transport::smtp::authentication::{Credentials, Mechanism}, //! authentication::{Credentials, Mechanism},
//! PoolConfig,
//! },
//! Message, SmtpTransport, Transport, //! Message, SmtpTransport, Transport,
//! }; //! };
//! //!
@@ -45,39 +71,35 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! // Create the SMTPS transport //! // Create TLS transport on port 587 with STARTTLS
//! let sender = SmtpTransport::relay("smtp.example.com")? //! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication //! // Add credentials for authentication
//! .credentials(Credentials::new( //! .credentials(Credentials::new(
//! "username".to_owned(), //! "username".to_owned(),
//! "password".to_owned(), //! "password".to_owned(),
//! )) //! ))
//! // Optionally configure expected authentication mechanism //! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain]) //! .authentication(vec![Mechanism::Plain])
//! // Connection pool settings
//! .pool_config(PoolConfig::new().max_size(20))
//! .build(); //! .build();
//! //!
//! // Send the email via remote relay //! // Send the email via remote relay
//! sender.send(&email)?; //! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(()) //! # Ok(())
//! # } //! # }
//! ``` //! ```
//! //!
//! #### Shortening configuration //! You can specify custom TLS settings:
//!
//! 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 //! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))] //! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> { //! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{ //! use lettre::{
//! message::header::ContentType, //! transport::smtp::client::{Tls, TlsParameters},
//! transport::smtp::authentication::{Credentials, Mechanism},
//! Message, SmtpTransport, Transport, //! Message, SmtpTransport, Transport,
//! }; //! };
//! //!
@@ -86,106 +108,25 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! // Create the SMTPS transport //! // Custom TLS configuration
//! let sender = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build(); //! let tls = TlsParameters::builder("smtp.example.com".to_owned())
//! //! .dangerous_accept_invalid_certs(true)
//! // 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()?; //! .build()?;
//! //!
//! // Create the SMTPS transport //! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")? //! let sender = SmtpTransport::relay("smtp.example.com")?
//! .tls(Tls::Wrapper(tls)) //! // Custom TLS configuration
//! .tls(Tls::Required(tls))
//! .build(); //! .build();
//! //!
//! // Send the email via remote relay //! // Send the email via remote relay
//! sender.send(&email)?; //! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(()) //! # 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; use std::time::Duration;
@@ -199,7 +140,7 @@ pub use self::{
error::Error, error::Error,
transport::{SmtpTransport, SmtpTransportBuilder}, transport::{SmtpTransport, SmtpTransportBuilder},
}; };
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use crate::transport::smtp::client::TlsParameters; use crate::transport::smtp::client::TlsParameters;
use crate::transport::smtp::{ use crate::transport::smtp::{
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS}, authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},

View File

@@ -1,5 +1,6 @@
use std::{ use std::{
fmt::{self, Debug}, fmt::{self, Debug},
mem,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
time::{Duration, Instant}, time::{Duration, Instant},
@@ -14,15 +15,11 @@ use super::{
super::{client::AsyncSmtpConnection, Error}, super::{client::AsyncSmtpConnection, Error},
PoolConfig, PoolConfig,
}; };
use crate::{ use crate::{executor::SpawnHandle, transport::smtp::async_transport::AsyncSmtpClient, Executor};
executor::SpawnHandle,
transport::smtp::{async_transport::AsyncSmtpClient, error},
Executor,
};
pub(crate) struct Pool<E: Executor> { pub struct Pool<E: Executor> {
config: PoolConfig, config: PoolConfig,
connections: Mutex<Option<Vec<ParkedConnection>>>, connections: Mutex<Vec<ParkedConnection>>,
client: AsyncSmtpClient<E>, client: AsyncSmtpClient<E>,
handle: OnceLock<E::Handle>, handle: OnceLock<E::Handle>,
} }
@@ -32,16 +29,16 @@ struct ParkedConnection {
since: Instant, since: Instant,
} }
pub(crate) struct PooledConnection<E: Executor> { pub struct PooledConnection<E: Executor> {
conn: Option<AsyncSmtpConnection>, conn: Option<AsyncSmtpConnection>,
pool: Arc<Pool<E>>, pool: Arc<Pool<E>>,
} }
impl<E: Executor> Pool<E> { impl<E: Executor> Pool<E> {
pub(crate) fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> { pub fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
let pool = Arc::new(Self { let pool = Arc::new(Self {
config, config,
connections: Mutex::new(Some(Vec::new())), connections: Mutex::new(Vec::new()),
client, client,
handle: OnceLock::new(), handle: OnceLock::new(),
}); });
@@ -63,10 +60,6 @@ impl<E: Executor> Pool<E> {
#[allow(clippy::needless_collect)] #[allow(clippy::needless_collect)]
let (count, dropped) = { let (count, dropped) = {
let mut connections = pool.connections.lock().await; let mut connections = pool.connections.lock().await;
let Some(connections) = connections.as_mut() else {
// The transport was shut down
return;
};
let to_drop = connections let to_drop = connections
.iter() .iter()
@@ -85,7 +78,7 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
let mut created = 0; 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 { let conn = match pool.client.connection().await {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => { Err(err) => {
@@ -99,11 +92,6 @@ impl<E: Executor> Pool<E> {
}; };
let mut connections = pool.connections.lock().await; 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)); connections.push(ParkedConnection::park(conn));
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@@ -121,7 +109,7 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("dropped {} idle connections", dropped.len()); tracing::debug!("dropped {} idle connections", dropped.len());
abort_concurrent(dropped.into_iter().map(ParkedConnection::unpark)) abort_concurrent(dropped.into_iter().map(|conn| conn.unpark()))
.await; .await;
} }
} }
@@ -146,25 +134,10 @@ impl<E: Executor> Pool<E> {
pool pool
} }
pub(crate) async fn shutdown(&self) { pub async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
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 { loop {
let conn = { let conn = {
let mut connections = self.connections.lock().await; 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() connections.pop()
}; };
@@ -208,20 +181,13 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("recycling connection"); tracing::debug!("recycling connection");
let mut connections_guard = self.connections.lock().await; let mut connections = self.connections.lock().await;
if connections.len() >= self.config.max_size as usize {
if let Some(connections) = connections_guard.as_mut() { drop(connections);
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 {
// The pool has already been shut down
drop(connections_guard);
conn.abort().await; conn.abort().await;
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
} }
} }
} }
@@ -234,13 +200,7 @@ impl<E: Executor> Debug for Pool<E> {
.field( .field(
"connections", "connections",
&match self.connections.try_lock() { &match self.connections.try_lock() {
Some(connections) => { Some(connections) => format!("{} connections", connections.len()),
if let Some(connections) = connections.as_ref() {
format!("{} connections", connections.len())
} else {
"SHUT DOWN".to_owned()
}
}
None => "LOCKED".to_owned(), None => "LOCKED".to_owned(),
}, },
@@ -262,16 +222,14 @@ impl<E: Executor> Drop for Pool<E> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("dropping Pool"); tracing::debug!("dropping Pool");
let connections = self.connections.get_mut().take(); let connections = mem::take(self.connections.get_mut());
let handle = self.handle.take(); let handle = self.handle.take();
E::spawn(async move { E::spawn(async move {
if let Some(handle) = handle { if let Some(handle) = handle {
handle.shutdown().await; handle.shutdown().await;
} }
if let Some(connections) = connections { abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
}
}); });
} }
} }

View File

@@ -1,8 +1,8 @@
use std::time::Duration; use std::time::Duration;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub(super) mod async_impl; pub mod async_impl;
pub(super) mod sync_impl; pub mod sync_impl;
/// Configuration for a connection pool /// Configuration for a connection pool
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -1,7 +1,8 @@
use std::{ use std::{
fmt::{self, Debug}, fmt::{self, Debug},
mem,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::{mpsc, Arc, Mutex, TryLockError}, sync::{Arc, Mutex, TryLockError},
thread, thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@@ -10,12 +11,11 @@ use super::{
super::{client::SmtpConnection, Error}, super::{client::SmtpConnection, Error},
PoolConfig, PoolConfig,
}; };
use crate::transport::smtp::{error, transport::SmtpClient}; use crate::transport::smtp::transport::SmtpClient;
pub(crate) struct Pool { pub struct Pool {
config: PoolConfig, config: PoolConfig,
connections: Mutex<Option<Vec<ParkedConnection>>>, connections: Mutex<Vec<ParkedConnection>>,
thread_terminator: mpsc::SyncSender<()>,
client: SmtpClient, client: SmtpClient,
} }
@@ -24,19 +24,16 @@ struct ParkedConnection {
since: Instant, since: Instant,
} }
pub(crate) struct PooledConnection { pub struct PooledConnection {
conn: Option<SmtpConnection>, conn: Option<SmtpConnection>,
pool: Arc<Pool>, pool: Arc<Pool>,
} }
impl Pool { impl Pool {
pub(crate) fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> { pub fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
let (thread_tx, thread_rx) = mpsc::sync_channel(1);
let pool = Arc::new(Self { let pool = Arc::new(Self {
config, config,
connections: Mutex::new(Some(Vec::new())), connections: Mutex::new(Vec::new()),
thread_terminator: thread_tx,
client, client,
}); });
@@ -57,10 +54,6 @@ impl Pool {
#[allow(clippy::needless_collect)] #[allow(clippy::needless_collect)]
let (count, dropped) = { let (count, dropped) = {
let mut connections = pool.connections.lock().unwrap(); let mut connections = pool.connections.lock().unwrap();
let Some(connections) = connections.as_mut() else {
// The transport was shut down
return;
};
let to_drop = connections let to_drop = connections
.iter() .iter()
@@ -79,7 +72,7 @@ impl Pool {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
let mut created = 0; let mut created = 0;
for _ in count..(min_idle as usize) { for _ in count..=(min_idle as usize) {
let conn = match pool.client.connection() { let conn = match pool.client.connection() {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => { Err(err) => {
@@ -93,11 +86,6 @@ impl Pool {
}; };
let mut connections = pool.connections.lock().unwrap(); 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)); connections.push(ParkedConnection::park(conn));
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@@ -121,15 +109,7 @@ impl Pool {
} }
} }
drop(pool); thread::sleep(idle_timeout);
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"); .expect("couldn't spawn the Pool thread");
@@ -138,25 +118,10 @@ impl Pool {
pool pool
} }
pub(crate) fn shutdown(&self) { pub fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
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 { loop {
let conn = { let conn = {
let mut connections = self.connections.lock().unwrap(); 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() connections.pop()
}; };
@@ -200,20 +165,13 @@ impl Pool {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("recycling connection"); tracing::debug!("recycling connection");
let mut connections_guard = self.connections.lock().unwrap(); let mut connections = self.connections.lock().unwrap();
if connections.len() >= self.config.max_size as usize {
if let Some(connections) = connections_guard.as_mut() { drop(connections);
if connections.len() >= self.config.max_size as usize {
drop(connections_guard);
conn.abort();
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
}
} else {
// The pool has already been shut down
drop(connections_guard);
conn.abort(); conn.abort();
} else {
let conn = ParkedConnection::park(conn);
connections.push(conn);
} }
} }
} }
@@ -226,13 +184,7 @@ impl Debug for Pool {
.field( .field(
"connections", "connections",
&match self.connections.try_lock() { &match self.connections.try_lock() {
Ok(connections) => { Ok(connections) => format!("{} connections", connections.len()),
if let Some(connections) = connections.as_ref() {
format!("{} connections", connections.len())
} else {
"SHUT DOWN".to_owned()
}
}
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(), Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(), Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
@@ -248,11 +200,10 @@ impl Drop for Pool {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("dropping Pool"); tracing::debug!("dropping Pool");
if let Some(connections) = self.connections.get_mut().unwrap().take() { let connections = mem::take(&mut *self.connections.get_mut().unwrap());
for conn in connections { for conn in connections {
let mut conn = conn.unpark(); let mut conn = conn.unpark();
conn.abort(); conn.abort();
}
} }
} }
} }

View File

@@ -12,8 +12,8 @@ use nom::{
bytes::streaming::{tag, take_until}, bytes::streaming::{tag, take_until},
combinator::{complete, map}, combinator::{complete, map},
multi::many0, multi::many0,
sequence::preceded, sequence::{preceded, tuple},
IResult, Parser, IResult,
}; };
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
@@ -221,8 +221,7 @@ fn parse_severity(i: &str) -> IResult<&str, Severity> {
map(tag("3"), |_| Severity::PositiveIntermediate), map(tag("3"), |_| Severity::PositiveIntermediate),
map(tag("4"), |_| Severity::TransientNegativeCompletion), map(tag("4"), |_| Severity::TransientNegativeCompletion),
map(tag("5"), |_| Severity::PermanentNegativeCompletion), map(tag("5"), |_| Severity::PermanentNegativeCompletion),
)) ))(i)
.parse(i)
} }
fn parse_category(i: &str) -> IResult<&str, Category> { fn parse_category(i: &str) -> IResult<&str, Category> {
@@ -233,8 +232,7 @@ fn parse_category(i: &str) -> IResult<&str, Category> {
map(tag("3"), |_| Category::Unspecified3), map(tag("3"), |_| Category::Unspecified3),
map(tag("4"), |_| Category::Unspecified4), map(tag("4"), |_| Category::Unspecified4),
map(tag("5"), |_| Category::MailSystem), map(tag("5"), |_| Category::MailSystem),
)) ))(i)
.parse(i)
} }
fn parse_detail(i: &str) -> IResult<&str, Detail> { fn parse_detail(i: &str) -> IResult<&str, Detail> {
@@ -249,20 +247,18 @@ fn parse_detail(i: &str) -> IResult<&str, Detail> {
map(tag("7"), |_| Detail::Seven), map(tag("7"), |_| Detail::Seven),
map(tag("8"), |_| Detail::Eight), map(tag("8"), |_| Detail::Eight),
map(tag("9"), |_| Detail::Nine), map(tag("9"), |_| Detail::Nine),
)) ))(i)
.parse(i)
} }
pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> { pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
let (i, lines) = many0(( let (i, lines) = many0(tuple((
parse_code, parse_code,
preceded(tag("-"), take_until("\r\n")), preceded(tag("-"), take_until("\r\n")),
tag("\r\n"), tag("\r\n"),
)) )))(i)?;
.parse(i)?;
let (i, (last_code, last_line)) = let (i, (last_code, last_line)) =
(parse_code, preceded(tag(" "), take_until("\r\n"))).parse(i)?; tuple((parse_code, preceded(tag(" "), take_until("\r\n"))))(i)?;
let (i, _) = complete(tag("\r\n")).parse(i)?; let (i, _) = complete(tag("\r\n"))(i)?;
// Check that all codes are equal. // Check that all codes are equal.
if !lines.iter().all(|&(code, _, _)| code == last_code) { if !lines.iter().all(|&(code, _, _)| code == last_code) {

View File

@@ -7,35 +7,11 @@ use super::pool::sync_impl::Pool;
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
use super::PoolConfig; use super::PoolConfig;
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo}; use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT}; use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::{address::Envelope, Transport}; use crate::{address::Envelope, Transport};
/// Synchronously send emails using the SMTP protocol /// Sends 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")))] #[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
#[derive(Clone)] #[derive(Clone)]
pub struct SmtpTransport { pub struct SmtpTransport {
@@ -56,15 +32,10 @@ impl Transport for SmtpTransport {
let result = conn.send(envelope, email)?; let result = conn.send(envelope, email)?;
#[cfg(not(feature = "pool"))] #[cfg(not(feature = "pool"))]
conn.abort(); conn.quit()?;
Ok(result) Ok(result)
} }
fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown();
}
} }
impl Debug for SmtpTransport { impl Debug for SmtpTransport {
@@ -82,10 +53,10 @@ impl SmtpTransport {
/// ///
/// Creates an encrypted transport over submissions port, using the provided domain /// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates. /// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> { pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?; let tls_parameters = TlsParameters::new(relay.into())?;
@@ -106,10 +77,10 @@ impl SmtpTransport {
/// ///
/// An error is returned if the connection can't be upgraded. No credentials /// 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. /// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> { pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?; let tls_parameters = TlsParameters::new(relay.into())?;
@@ -144,72 +115,54 @@ impl SmtpTransport {
/// Creates a `SmtpTransportBuilder` from a connection URL /// Creates a `SmtpTransportBuilder` from a connection URL
/// ///
/// The protocol, credentials, host, port and EHLO name can be provided /// The protocol, credentials, host and port can be provided in a single URL.
/// in a single URL. This may be simpler than having to configure SMTP /// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
/// through multiple configuration parameters and then having to pass /// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
/// those options to lettre. /// 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 URL is created in the following way: /// <table>
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`. /// <thead>
/// /// <tr>
/// `user` (Username) and `pass` (Password) are optional in case the /// <th>scheme</th>
/// SMTP relay doesn't require authentication. When `port` is not /// <th>tls parameter</th>
/// configured it is automatically determined based on the `scheme`. /// <th>example</th>
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO /// <th>remarks</th>
/// command. `TLS` controls whether STARTTLS is simply enabled /// </tr>
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks) /// </thead>
/// or `required` (require the server to upgrade the connection to /// <tbody>
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt). /// <tr>
/// /// <td>smtps</td>
/// Use the following table to construct your SMTP url: /// <td>-</td>
/// /// <td>smtps://smtp.example.com</td>
/// | scheme | `tls` query parameter | example | default port | remarks | /// <td>SMTP over TLS, recommended method</td>
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | /// </tr>
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method | /// <tr>
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available | /// <td>smtp</td>
/// | `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 | /// <td>required</td>
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted | /// <td>smtp://smtp.example.com?tls=required</td>
/// /// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
/// IMPORTANT: some parameters like `user` and `pass` cannot simply /// </tr>
/// be concatenated to construct the final URL because special characters /// <tr>
/// contained within the parameter may confuse the URL decoder. /// <td>smtp</td>
/// Manually URL encode the parameters before concatenating them or use /// <td>opportunistic</td>
/// a proper URL encoder, like the following cargo script: /// <td>smtp://smtp.example.com?tls=opportunistic</td>
/// /// <td>
/// ```rust /// SMTP with optionally STARTTLS when supported by the server.
/// # const TOML: &str = r#" /// Caution: this method is vulnerable to a man-in-the-middle attack.
/// #!/usr/bin/env cargo /// Not recommended for production use.
/// /// </td>
/// //! ```cargo /// </tr>
/// //! [dependencies] /// <tr>
/// //! url = "2" /// <td>smtp</td>
/// //! ``` /// <td>-</td>
/// # "#; /// <td>smtp://smtp.example.com</td>
/// /// <td>Unencrypted SMTP, not recommended for production use.</td>
/// use url::Url; /// </tr>
/// /// </tbody>
/// fn main() { /// </table>
/// // 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 /// ```rust,no_run
/// use lettre::{ /// use lettre::{
@@ -217,7 +170,6 @@ impl SmtpTransport {
/// SmtpTransport, Transport, /// SmtpTransport, Transport,
/// }; /// };
/// ///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let email = Message::builder() /// let email = Message::builder()
/// .from("NoBody <nobody@domain.tld>".parse().unwrap()) /// .from("NoBody <nobody@domain.tld>".parse().unwrap())
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) /// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
@@ -228,17 +180,20 @@ impl SmtpTransport {
/// .unwrap(); /// .unwrap();
/// ///
/// // Open a remote connection to example /// // Open a remote connection to example
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build(); /// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")
/// .unwrap()
/// .build();
/// ///
/// // Send the email /// // Send the email
/// mailer.send(&email)?; /// match mailer.send(&email) {
/// # Ok(()) /// Ok(_) => println!("Email sent successfully!"),
/// # } /// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// ``` /// ```
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> { pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
super::connection_url::from_connection_url(connection_url) super::connection_url::from_connection_url(connection_url)
@@ -291,7 +246,7 @@ impl SmtpTransportBuilder {
self self
} }
/// Set the authentication credentials to use /// Set the authentication mechanism to use
pub fn credentials(mut self, credentials: Credentials) -> Self { pub fn credentials(mut self, credentials: Credentials) -> Self {
self.info.credentials = Some(credentials); self.info.credentials = Some(credentials);
self self
@@ -310,38 +265,16 @@ impl SmtpTransportBuilder {
} }
/// Set the port to use /// 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 { pub fn port(mut self, port: u16) -> Self {
self.info.port = port; self.info.port = port;
self self
} }
/// Set the TLS settings to use /// 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( #[cfg_attr(
docsrs, docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))) doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)] )]
pub fn tls(mut self, tls: Tls) -> Self { pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls; self.info.tls = tls;
@@ -374,7 +307,7 @@ impl SmtpTransportBuilder {
/// Build client /// Build client
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(super) struct SmtpClient { pub struct SmtpClient {
info: SmtpInfo, info: SmtpInfo,
} }
@@ -382,10 +315,10 @@ impl SmtpClient {
/// Creates a new connection directly usable to send emails /// Creates a new connection directly usable to send emails
/// ///
/// Handles encryption and authentication /// Handles encryption and authentication
pub(super) fn connection(&self) -> Result<SmtpConnection, Error> { pub fn connection(&self) -> Result<SmtpConnection, Error> {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match &self.info.tls { let tls_parameters = match &self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls::Wrapper(tls_parameters) => Some(tls_parameters), Tls::Wrapper(tls_parameters) => Some(tls_parameters),
_ => None, _ => None,
}; };
@@ -399,15 +332,15 @@ impl SmtpClient {
None, None,
)?; )?;
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
match &self.info.tls { match &self.info.tls {
Tls::Opportunistic(tls_parameters) => { Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {
conn.starttls(tls_parameters, &self.info.hello_name)?; conn = conn.starttls(tls_parameters, &self.info.hello_name)?;
} }
} }
Tls::Required(tls_parameters) => { Tls::Required(tls_parameters) => {
conn.starttls(tls_parameters, &self.info.hello_name)?; conn = conn.starttls(tls_parameters, &self.info.hello_name)?;
} }
_ => (), _ => (),
} }

View File

@@ -4,9 +4,9 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext /// Encode a string as xtext
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct XText<'a>(pub(crate) &'a str); pub struct XText<'a>(pub &'a str);
impl Display for XText<'_> { impl<'a> Display for XText<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut rest = self.0; let mut rest = self.0;
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') { while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
@@ -38,7 +38,9 @@ mod tests {
("bjørn", "bjørn"), ("bjørn", "bjørn"),
("Ø+= ❤️‰", "Ø+2B+3D+20❤"), ("Ø+= ❤️‰", "Ø+2B+3D+20❤"),
("+", "+2B"), ("+", "+2B"),
] { ]
.iter()
{
assert_eq!(format!("{}", XText(input)), (*expect).to_owned()); assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
} }
} }

View File

@@ -11,9 +11,7 @@
//! ```rust //! ```rust
//! # #[cfg(feature = "builder")] //! # #[cfg(feature = "builder")]
//! # { //! # {
//! use lettre::{ //! use lettre::{transport::stub::StubTransport, Message, Transport};
//! message::header::ContentType, transport::stub::StubTransport, Message, Transport,
//! };
//! //!
//! # use std::error::Error; //! # use std::error::Error;
//! # fn try_main() -> Result<(), Box<dyn Error>> { //! # fn try_main() -> Result<(), Box<dyn Error>> {
@@ -22,11 +20,11 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@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!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! let mut sender = StubTransport::new_ok(); //! let mut sender = StubTransport::new_ok();
//! sender.send(&email)?; //! let result = sender.send(&email);
//! assert!(result.is_ok());
//! assert_eq!( //! assert_eq!(
//! sender.messages(), //! sender.messages(),
//! vec![( //! vec![(
@@ -43,11 +41,13 @@
use std::{ use std::{
error::Error as StdError, error::Error as StdError,
fmt, fmt,
sync::{Arc, Mutex}, sync::{Arc, Mutex as StdMutex},
}; };
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait; 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"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport; use crate::AsyncTransport;
@@ -70,7 +70,7 @@ impl StdError for Error {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StubTransport { pub struct StubTransport {
response: Result<(), Error>, response: Result<(), Error>,
message_log: Arc<Mutex<Vec<(Envelope, String)>>>, message_log: Arc<StdMutex<Vec<(Envelope, String)>>>,
} }
/// This transport logs messages and always returns the given response /// 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"))))] #[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncStubTransport { pub struct AsyncStubTransport {
response: Result<(), Error>, response: Result<(), Error>,
message_log: Arc<Mutex<Vec<(Envelope, String)>>>, message_log: Arc<FuturesMutex<Vec<(Envelope, String)>>>,
} }
impl StubTransport { impl StubTransport {
@@ -87,7 +87,7 @@ impl StubTransport {
pub fn new(response: Result<(), Error>) -> Self { pub fn new(response: Result<(), Error>) -> Self {
Self { Self {
response, response,
message_log: Arc::new(Mutex::new(vec![])), message_log: Arc::new(StdMutex::new(vec![])),
} }
} }
@@ -95,7 +95,7 @@ impl StubTransport {
pub fn new_ok() -> Self { pub fn new_ok() -> Self {
Self { Self {
response: Ok(()), response: Ok(()),
message_log: Arc::new(Mutex::new(vec![])), message_log: Arc::new(StdMutex::new(vec![])),
} }
} }
@@ -103,7 +103,7 @@ impl StubTransport {
pub fn new_error() -> Self { pub fn new_error() -> Self {
Self { Self {
response: Err(Error), response: Err(Error),
message_log: Arc::new(Mutex::new(vec![])), message_log: Arc::new(StdMutex::new(vec![])),
} }
} }
@@ -122,7 +122,7 @@ impl AsyncStubTransport {
pub fn new(response: Result<(), Error>) -> Self { pub fn new(response: Result<(), Error>) -> Self {
Self { Self {
response, response,
message_log: Arc::new(Mutex::new(vec![])), message_log: Arc::new(FuturesMutex::new(vec![])),
} }
} }
@@ -130,7 +130,7 @@ impl AsyncStubTransport {
pub fn new_ok() -> Self { pub fn new_ok() -> Self {
Self { Self {
response: Ok(()), response: Ok(()),
message_log: Arc::new(Mutex::new(vec![])), message_log: Arc::new(FuturesMutex::new(vec![])),
} }
} }
@@ -138,14 +138,14 @@ impl AsyncStubTransport {
pub fn new_error() -> Self { pub fn new_error() -> Self {
Self { Self {
response: Err(Error), response: Err(Error),
message_log: Arc::new(Mutex::new(vec![])), message_log: Arc::new(FuturesMutex::new(vec![])),
} }
} }
/// Return all logged messages sent using [`AsyncTransport::send_raw`] /// Return all logged messages sent using [`AsyncTransport::send_raw`]
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub async fn messages(&self) -> Vec<(Envelope, String)> { pub async fn messages(&self) -> Vec<(Envelope, String)> {
self.message_log.lock().unwrap().clone() self.message_log.lock().await.clone()
} }
} }
@@ -171,7 +171,7 @@ impl AsyncTransport for AsyncStubTransport {
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> { async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.message_log self.message_log
.lock() .lock()
.unwrap() .await
.push((envelope.clone(), String::from_utf8_lossy(email).into())); .push((envelope.clone(), String::from_utf8_lossy(email).into()));
self.response self.response
} }