Compare commits
131 Commits
v0.10.3
...
docs-crede
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5793a1b9a9 | ||
|
|
6c0be84817 | ||
|
|
6059cb04d6 | ||
|
|
fdf0346556 | ||
|
|
0f9455715c | ||
|
|
0b3a1ed278 | ||
|
|
76bf68268f | ||
|
|
99a86c0fac | ||
|
|
f0de9ef02c | ||
|
|
b4ddcbdcfc | ||
|
|
1e22bcd527 | ||
|
|
75716ca269 | ||
|
|
8a6f1dab0e | ||
|
|
621853e2e3 | ||
|
|
5e4cb2d1b5 | ||
|
|
b4abd40698 | ||
|
|
2d1ccda2ef | ||
|
|
54934e1492 | ||
|
|
cfa29743a8 | ||
|
|
4a4a96d805 | ||
|
|
f0b8052a52 | ||
|
|
655cd8a140 | ||
|
|
dabc88a053 | ||
|
|
9cdefcea09 | ||
|
|
5748af4c98 | ||
|
|
3e9b1876d9 | ||
|
|
795bedae76 | ||
|
|
891dd521ab | ||
|
|
0fb89e23ad | ||
|
|
097f7d5aaa | ||
|
|
32e066464a | ||
|
|
55c7f57f25 | ||
|
|
3f7a57a417 | ||
|
|
bb64baec67 | ||
|
|
5f13636b49 | ||
|
|
4513e602d6 | ||
|
|
382e15013a | ||
|
|
3ce31c5a6a | ||
|
|
a48cf92a5b | ||
|
|
43f6f139d2 | ||
|
|
fd1425666d | ||
|
|
de075153b0 | ||
|
|
02dfc7dd4a | ||
|
|
83ce5872d7 | ||
|
|
272efeca74 | ||
|
|
ec6f5f3920 | ||
|
|
b62d23bd87 | ||
|
|
51794aa912 | ||
|
|
eb42651401 | ||
|
|
99c6dc2a87 | ||
|
|
b6babbce00 | ||
|
|
c9895c52de | ||
|
|
575492b9ed | ||
|
|
ad665cd01e | ||
|
|
e2ac5dadfb | ||
|
|
1c6a348eb8 | ||
|
|
e8b2498ad7 | ||
|
|
bf48bd6b96 | ||
|
|
fa6191983a | ||
|
|
ca405040ae | ||
|
|
f7a1b790df | ||
|
|
caff354cbf | ||
|
|
a81401c4cb | ||
|
|
54df594d6c | ||
|
|
cada01d039 | ||
|
|
0132bee59d | ||
|
|
acdf189717 | ||
|
|
3aea65315f | ||
|
|
9d3ebfab1a | ||
|
|
6fb69086fb | ||
|
|
dfdf3a61d2 | ||
|
|
e30ac2dbff | ||
|
|
22dca340a7 | ||
|
|
c7d1f35676 | ||
|
|
eebea56f16 | ||
|
|
851d6ae164 | ||
|
|
6f38e6b9a9 | ||
|
|
c40af78809 | ||
|
|
6d2e0d5046 | ||
|
|
c64cb0ff2e | ||
|
|
10d7b197ed | ||
|
|
fb54855d5f | ||
|
|
157c4fb5ae | ||
|
|
1196e332ee | ||
|
|
75770f7bc6 | ||
|
|
76d0929c94 | ||
|
|
c3d00051b2 | ||
|
|
12580d82f4 | ||
|
|
f7849078b8 | ||
|
|
f2c94cdf4d | ||
|
|
74f64b81ab | ||
|
|
39c71dbfd2 | ||
|
|
c1bf5dfda1 | ||
|
|
1c1fef8055 | ||
|
|
1540f16015 | ||
|
|
330daa1173 | ||
|
|
47f2fe0750 | ||
|
|
8b6cee30ee | ||
|
|
62c16e90ef | ||
|
|
e0494a5f9d | ||
|
|
8c3bffa728 | ||
|
|
47eda90433 | ||
|
|
46ea8c48ac | ||
|
|
5f7063fdc3 | ||
|
|
61c1f6bc6f | ||
|
|
283e21f8d6 | ||
|
|
20c3701eb0 | ||
|
|
74117d5cc6 | ||
|
|
bb49e0a46b | ||
|
|
42365478c2 | ||
|
|
94769242d1 | ||
|
|
7e6ffe8aea | ||
|
|
16c35ef583 | ||
|
|
bbab86b484 | ||
|
|
b5652f18b7 | ||
|
|
c2f2b907a9 | ||
|
|
a1cc770613 | ||
|
|
57886c367d | ||
|
|
f3a469431e | ||
|
|
9b48ef355b | ||
|
|
7fee8dc5a8 | ||
|
|
7e9fff9bd0 | ||
|
|
92f5460132 | ||
|
|
cd0c032f71 | ||
|
|
f41c9c19ab | ||
|
|
cb6a7178d9 | ||
|
|
2bfc759aa3 | ||
|
|
89673d0eb2 | ||
|
|
8b588cf275 | ||
|
|
5f37b66352 | ||
|
|
69e5974024 |
33
.github/workflows/test.yml
vendored
33
.github/workflows/test.yml
vendored
@@ -13,16 +13,16 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rustfmt:
|
rustfmt:
|
||||||
name: rustfmt / nightly-2022-11-12
|
name: rustfmt / nightly-2024-09-01
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: |
|
run: |
|
||||||
rustup default nightly-2022-11-12
|
rustup default nightly-2024-09-01
|
||||||
rustup component add rustfmt
|
rustup component add rustfmt
|
||||||
|
|
||||||
- name: cargo fmt
|
- name: cargo fmt
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: |
|
run: |
|
||||||
@@ -50,19 +50,19 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: rustup update --no-self-update stable
|
run: rustup update --no-self-update stable
|
||||||
|
|
||||||
- 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
|
run: cargo hack check --feature-powerset --depth 3 --at-least-one-of aws-lc-rs,ring --at-least-one-of rustls-native-certs,webpki-roots
|
||||||
|
|
||||||
test:
|
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.60.0
|
- name: '1.74'
|
||||||
rust: 1.60.0
|
rust: '1.74'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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,9 +112,6 @@ jobs:
|
|||||||
- name: Install dkimverify
|
- name: Install dkimverify
|
||||||
run: sudo apt -y install python3-dkim
|
run: sudo apt -y install python3-dkim
|
||||||
|
|
||||||
- name: Work around early dependencies MSRV bump
|
|
||||||
run: cargo update -p async-global-executor --precise 2.0.4
|
|
||||||
|
|
||||||
- name: Test with no default features
|
- name: Test with no default features
|
||||||
run: cargo test --no-default-features
|
run: cargo test --no-default-features
|
||||||
|
|
||||||
@@ -122,16 +119,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-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-native-certs,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots
|
run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls,tracing
|
||||||
|
|
||||||
- name: Test with all features (-boring-tls)
|
- name: Test with all features (-boring-tls)
|
||||||
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-native-certs,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots
|
run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls,tracing
|
||||||
|
|
||||||
# coverage:
|
# coverage:
|
||||||
# name: Coverage
|
# name: Coverage
|
||||||
# runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
# steps:
|
# steps:
|
||||||
# - uses: actions/checkout@v2
|
# - uses: actions/checkout@v4
|
||||||
# - uses: actions-rs/toolchain@v1
|
# - uses: actions-rs/toolchain@v1
|
||||||
# with:
|
# with:
|
||||||
# toolchain: nightly
|
# toolchain: nightly
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,4 +4,3 @@
|
|||||||
lettre.sublime-*
|
lettre.sublime-*
|
||||||
lettre.iml
|
lettre.iml
|
||||||
target/
|
target/
|
||||||
/Cargo.lock
|
|
||||||
|
|||||||
355
CHANGELOG.md
355
CHANGELOG.md
@@ -1,3 +1,354 @@
|
|||||||
|
<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>
|
||||||
|
### v0.11.4 (2024-01-28)
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
|
||||||
|
* Percent decode credentials in SMTP connect URL ([#932], [#934])
|
||||||
|
* Fix mimebody DKIM body-hash computation ([#923])
|
||||||
|
|
||||||
|
[#923]: https://github.com/lettre/lettre/pull/923
|
||||||
|
[#932]: https://github.com/lettre/lettre/pull/932
|
||||||
|
[#934]: https://github.com/lettre/lettre/pull/934
|
||||||
|
|
||||||
|
<a name="v0.11.3"></a>
|
||||||
|
### v0.11.3 (2024-01-02)
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924])
|
||||||
|
* Derive `Debug` for `SmtpTransport` ([#925])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Upgrade `rustls` to v0.22 ([#921])
|
||||||
|
* Drop once_cell dependency in favor of OnceLock from std ([#928])
|
||||||
|
|
||||||
|
[#921]: https://github.com/lettre/lettre/pull/921
|
||||||
|
[#924]: https://github.com/lettre/lettre/pull/924
|
||||||
|
[#925]: https://github.com/lettre/lettre/pull/925
|
||||||
|
[#928]: https://github.com/lettre/lettre/pull/928
|
||||||
|
|
||||||
|
<a name="v0.11.2"></a>
|
||||||
|
### v0.11.2 (2023-11-23)
|
||||||
|
|
||||||
|
#### Upgrade notes
|
||||||
|
|
||||||
|
* MSRV is now 1.70 ([#916])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Bump `idna` to v0.5 ([#918])
|
||||||
|
* Bump `boring` and `tokio-boring` to v4 ([#915])
|
||||||
|
|
||||||
|
[#915]: https://github.com/lettre/lettre/pull/915
|
||||||
|
[#916]: https://github.com/lettre/lettre/pull/916
|
||||||
|
[#918]: https://github.com/lettre/lettre/pull/918
|
||||||
|
|
||||||
|
<a name="v0.11.1"></a>
|
||||||
|
### v0.11.1 (2023-10-24)
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
|
||||||
|
* Fix `webpki-roots` certificate store setup ([#909])
|
||||||
|
|
||||||
|
[#909]: https://github.com/lettre/lettre/pull/909
|
||||||
|
|
||||||
|
<a name="v0.11.0"></a>
|
||||||
|
### v0.11.0 (2023-10-15)
|
||||||
|
|
||||||
|
While this release technically contains breaking changes, we expect most projects
|
||||||
|
to be able to upgrade by only bumping the version in `Cargo.toml`.
|
||||||
|
|
||||||
|
#### Upgrade notes
|
||||||
|
|
||||||
|
* MSRV is now 1.65 ([#869] and [#881])
|
||||||
|
* `AddressError` is now marked as `#[non_exhaustive]` ([#839])
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* Improve mailbox parsing ([#839])
|
||||||
|
* Add construction of SMTP transport from URL ([#901])
|
||||||
|
* Add `From<Address>` implementation for `Mailbox` ([#879])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Bump `socket2` to v0.5 ([#868])
|
||||||
|
* Bump `idna` to v0.4, `fastrand` to v2, `quoted_printable` to v0.5, `rsa` to v0.9 ([#882])
|
||||||
|
* Bump `webpki-roots` to v0.25 ([#884] and [#890])
|
||||||
|
* Bump `ed25519-dalek` to v2 fixing RUSTSEC-2022-0093 ([#896])
|
||||||
|
* Bump `boring`ssl crates to v3 ([#897])
|
||||||
|
|
||||||
|
[#839]: https://github.com/lettre/lettre/pull/839
|
||||||
|
[#868]: https://github.com/lettre/lettre/pull/868
|
||||||
|
[#869]: https://github.com/lettre/lettre/pull/869
|
||||||
|
[#879]: https://github.com/lettre/lettre/pull/879
|
||||||
|
[#881]: https://github.com/lettre/lettre/pull/881
|
||||||
|
[#882]: https://github.com/lettre/lettre/pull/882
|
||||||
|
[#884]: https://github.com/lettre/lettre/pull/884
|
||||||
|
[#890]: https://github.com/lettre/lettre/pull/890
|
||||||
|
[#896]: https://github.com/lettre/lettre/pull/896
|
||||||
|
[#897]: https://github.com/lettre/lettre/pull/897
|
||||||
|
[#901]: https://github.com/lettre/lettre/pull/901
|
||||||
|
|
||||||
|
<a name="v0.10.4"></a>
|
||||||
|
### v0.10.4 (2023-04-02)
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Bumped rustls to 0.21 and all related dependencies ([#867])
|
||||||
|
|
||||||
|
[#867]: https://github.com/lettre/lettre/pull/867
|
||||||
|
|
||||||
<a name="v0.10.3"></a>
|
<a name="v0.10.3"></a>
|
||||||
### v0.10.3 (2023-02-20)
|
### v0.10.3 (2023-02-20)
|
||||||
|
|
||||||
@@ -143,7 +494,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
|||||||
* Update `hostname` to 0.3
|
* Update `hostname` to 0.3
|
||||||
* Update to `nom` 6
|
* Update to `nom` 6
|
||||||
* Replace `log` with `tracing`
|
* Replace `log` with `tracing`
|
||||||
* Move CI to Github Actions
|
* Move CI to GitHub Actions
|
||||||
* Use criterion for benchmarks
|
* Use criterion for benchmarks
|
||||||
|
|
||||||
<a name="v0.9.2"></a>
|
<a name="v0.9.2"></a>
|
||||||
@@ -313,6 +664,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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## Contributing to Lettre
|
## Contributing to Lettre
|
||||||
|
|
||||||
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
The following guidelines are inspired by the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
||||||
|
|
||||||
### Code formatting
|
### Code formatting
|
||||||
|
|
||||||
|
|||||||
2889
Cargo.lock
generated
Normal file
2889
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
114
Cargo.toml
114
Cargo.toml
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lettre"
|
name = "lettre"
|
||||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||||
version = "0.10.3"
|
version = "0.11.15"
|
||||||
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.60"
|
rust-version = "1.74"
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||||
@@ -19,35 +19,39 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
|||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idna = "0.3"
|
email_address = { version = "0.2.1", default-features = false }
|
||||||
once_cell = { version = "1", optional = true }
|
chumsky = "0.9"
|
||||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
idna = "1"
|
||||||
|
|
||||||
|
## 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 }
|
||||||
mime = { version = "0.3.4", optional = true }
|
mime = { version = "0.3.4", optional = true }
|
||||||
fastrand = { version = "1.4", optional = true }
|
fastrand = { version = "2.0", optional = true }
|
||||||
quoted_printable = { version = "0.4.6", optional = true }
|
quoted_printable = { version = "0.5", optional = true }
|
||||||
base64 = { version = "0.21", optional = true }
|
base64 = { version = "0.22", optional = true }
|
||||||
email-encoding = { version = "0.2", optional = true }
|
email-encoding = { version = "0.4", optional = true }
|
||||||
|
|
||||||
# file transport
|
# file transport
|
||||||
uuid = { version = "1", features = ["v4"], optional = true }
|
uuid = { version = "1", features = ["v4"], optional = true }
|
||||||
serde = { version = "1", optional = true, features = ["derive"] }
|
serde = { version = "1.0.110", features = ["derive"], optional = true }
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
|
|
||||||
# smtp-transport
|
# smtp-transport
|
||||||
nom = { version = "7", optional = true }
|
nom = { version = "8", optional = true }
|
||||||
hostname = { version = "0.3", optional = true } # feature
|
hostname = { version = "0.4", optional = true } # feature
|
||||||
socket2 = { version = "0.4.4", optional = true }
|
socket2 = { version = "0.5.1", optional = true }
|
||||||
|
url = { version = "2.4", optional = true }
|
||||||
|
percent-encoding = { version = "2.3", optional = true }
|
||||||
|
|
||||||
## tls
|
## tls
|
||||||
native-tls = { version = "0.2", optional = true } # feature
|
native-tls = { version = "0.2.9", optional = true } # feature
|
||||||
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
|
rustls = { version = "0.23.18", default-features = false, features = ["logging", "std", "tls12"], optional = true }
|
||||||
rustls-pemfile = { version = "1", optional = true }
|
rustls-native-certs = { version = "0.8", optional = true }
|
||||||
rustls-native-certs = { version = "0.6.2", optional = true }
|
webpki-roots = { version = "0.26", optional = true }
|
||||||
webpki-roots = { version = "0.22", optional = true }
|
boring = { version = "4", optional = true }
|
||||||
boring = { version = "2.0.0", optional = true }
|
|
||||||
|
|
||||||
# async
|
# async
|
||||||
futures-io = { version = "0.3.7", optional = true }
|
futures-io = { version = "0.3.7", optional = true }
|
||||||
@@ -56,26 +60,25 @@ async-trait = { version = "0.1", optional = true }
|
|||||||
|
|
||||||
## async-std
|
## async-std
|
||||||
async-std = { version = "1.8", optional = true }
|
async-std = { version = "1.8", optional = true }
|
||||||
#async-native-tls = { version = "0.3.3", optional = true }
|
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true }
|
||||||
futures-rustls = { version = "0.22", 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.23", optional = true }
|
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true }
|
||||||
tokio1_boring = { package = "tokio-boring", version = "2.1.4", optional = true }
|
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
|
||||||
|
|
||||||
## dkim
|
## dkim
|
||||||
sha2 = { version = "0.10", optional = true, features = ["oid"] }
|
sha2 = { version = "0.10", features = ["oid"], optional = true }
|
||||||
rsa = { version = "0.8", optional = true }
|
rsa = { version = "0.9", optional = true }
|
||||||
ed25519-dalek = { version = "1.0.1", optional = true }
|
ed25519-dalek = { version = "2", optional = true }
|
||||||
|
|
||||||
# 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"
|
||||||
criterion = "0.4"
|
criterion = "0.5"
|
||||||
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
|
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
@@ -83,39 +86,58 @@ walkdir = "2"
|
|||||||
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
async-std = { version = "1.8", features = ["attributes"] }
|
async-std = { version = "1.8", features = ["attributes"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
maud = "0.24"
|
maud = "0.26"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
harness = false
|
harness = false
|
||||||
name = "transport_smtp"
|
name = "transport_smtp"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
harness = false
|
||||||
|
name = "mailbox_parsing"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
||||||
builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"]
|
builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
|
||||||
mime03 = ["mime"]
|
mime03 = ["dep:mime"]
|
||||||
|
|
||||||
# transports
|
# transports
|
||||||
file-transport = ["uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||||
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
|
||||||
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
||||||
smtp-transport = ["base64", "nom", "socket2", "once_cell", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||||
|
|
||||||
pool = ["futures-util"]
|
pool = ["dep:futures-util"]
|
||||||
|
|
||||||
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"]
|
rustls = ["dep:rustls"]
|
||||||
|
aws-lc-rs = ["rustls?/aws-lc-rs"]
|
||||||
|
fips = ["aws-lc-rs", "rustls?/fips"]
|
||||||
|
ring = ["rustls?/ring"]
|
||||||
|
webpki-roots = ["dep:webpki-roots"]
|
||||||
|
# deprecated
|
||||||
|
rustls-tls = ["webpki-roots", "rustls", "ring"]
|
||||||
|
|
||||||
boring-tls = ["boring"]
|
boring-tls = ["dep:boring"]
|
||||||
|
|
||||||
# async
|
# async
|
||||||
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
|
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||||
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
|
async-std1-rustls = ["async-std1", "rustls", "dep:futures-rustls"]
|
||||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"]
|
# deprecated
|
||||||
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
|
async-std1-rustls-tls = ["async-std1-rustls", "rustls-tls"]
|
||||||
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
|
||||||
tokio1-boring-tls = ["tokio1", "boring-tls", "tokio1_boring"]
|
tokio1-rustls = ["tokio1", "rustls", "dep:tokio1_rustls"]
|
||||||
|
# deprecated
|
||||||
|
tokio1-rustls-tls = ["tokio1-rustls", "rustls-tls"]
|
||||||
|
tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
|
||||||
|
|
||||||
dkim = ["base64", "sha2", "rsa", "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
|
||||||
|
|||||||
4
LICENSE
4
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
Copyright (c) 2014-2022 Alexis Mousset <contact@amousset.me>
|
Copyright (c) 2014-2024 Alexis Mousset <contact@amousset.me>
|
||||||
Copyright (c) 2019-2022 Paolo Barbolini <paolo@paolo565.org>
|
Copyright (c) 2019-2025 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
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -28,8 +28,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://deps.rs/crate/lettre/0.10.3">
|
<a href="https://deps.rs/crate/lettre/0.11.15">
|
||||||
<img src="https://deps.rs/crate/lettre/0.10.3/status.svg"
|
<img src="https://deps.rs/crate/lettre/0.11.15/status.svg"
|
||||||
alt="dependency status" />
|
alt="dependency status" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,17 +53,17 @@ 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.60, but this could change at any time either from
|
the minimum supported Rust version is 1.74, but this could change at any time either from
|
||||||
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
This library requires Rust 1.60 or newer.
|
This library requires Rust 1.74 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
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lettre = "0.10"
|
lettre = "0.11"
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
@@ -71,27 +71,29 @@ use lettre::message::header::ContentType;
|
|||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
use lettre::{Message, SmtpTransport, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
|
|
||||||
let email = Message::builder()
|
fn main() {
|
||||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
let email = Message::builder()
|
||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new year")
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.header(ContentType::TEXT_PLAIN)
|
.subject("Happy new year")
|
||||||
.body(String::from("Be happy!"))
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.unwrap();
|
.body(String::from("Be happy!"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to gmail
|
// Open a remote connection to gmail
|
||||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.credentials(creds)
|
.credentials(creds)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -105,7 +107,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`.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
|
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
|
||||||
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
|
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
|
||||||
should not be reported via the public Github Issue tracker.
|
should not be reported via the public GitHub Issue tracker.
|
||||||
|
|
||||||
## Security advisories
|
## Security advisories
|
||||||
|
|
||||||
|
|||||||
27
benches/mailbox_parsing.rs
Normal file
27
benches/mailbox_parsing.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use lettre::message::{Mailbox, Mailboxes};
|
||||||
|
|
||||||
|
fn bench_parse_single(mailbox: &str) {
|
||||||
|
assert!(mailbox.parse::<Mailbox>().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_parse_multiple(mailboxes: &str) {
|
||||||
|
assert!(mailboxes.parse::<Mailboxes>().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
c.bench_function("parse single mailbox", |b| {
|
||||||
|
b.iter(|| bench_parse_single(black_box("\"Benchmark test\" <test@mail.local>")))
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("parse multiple mailboxes", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
bench_parse_multiple(black_box(
|
||||||
|
"\"Benchmark test\" <test@mail.local>, Test <test@mail.local>, <test@mail.local>, test@mail.local",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, criterion_benchmark);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -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, SmtpTransport, Transport};
|
use lettre::{message::header::ContentType, 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,6 +13,7 @@ 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));
|
||||||
@@ -32,6 +33,7 @@ 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));
|
||||||
|
|||||||
3
clippy.toml
Normal file
3
clippy.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
disallowed-methods = [
|
||||||
|
{ "path" = "std::time::SystemTime::now", reason = "does not work on WASM environments", replacement = "crate::time::now" }
|
||||||
|
]
|
||||||
@@ -41,7 +41,7 @@ fn main() {
|
|||||||
|
|
||||||
// Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS
|
// Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS
|
||||||
{
|
{
|
||||||
tracing::info!("Trying to establish a plaintext connection to {} and then updating it via the SMTP STARTTLS extension", smtp_host);
|
tracing::info!("Trying to establish a plaintext connection to {} and then upgrading it via the SMTP STARTTLS extension", smtp_host);
|
||||||
|
|
||||||
let transport = SmtpTransport::starttls_relay(&smtp_host)
|
let transport = SmtpTransport::starttls_relay(&smtp_host)
|
||||||
.expect("build SmtpTransport::starttls_relay")
|
.expect("build SmtpTransport::starttls_relay")
|
||||||
|
|||||||
@@ -14,11 +14,71 @@ pub struct Envelope {
|
|||||||
/// The envelope recipient's addresses
|
/// The envelope recipient's addresses
|
||||||
///
|
///
|
||||||
/// This can not be empty.
|
/// This can not be empty.
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
serde(deserialize_with = "serde_forward_path::deserialize")
|
||||||
|
)]
|
||||||
forward_path: Vec<Address>,
|
forward_path: Vec<Address>,
|
||||||
/// The envelope sender address
|
/// The envelope sender address
|
||||||
reverse_path: Option<Address>,
|
reverse_path: Option<Address>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// just like the default implementation to deserialize `Vec<Address>` but it
|
||||||
|
/// forbids **de**serializing empty lists
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
mod serde_forward_path {
|
||||||
|
use super::Address;
|
||||||
|
/// dummy type required for serde
|
||||||
|
/// see example: <https://serde.rs/deserialize-map.html>
|
||||||
|
struct CustomVisitor;
|
||||||
|
impl<'de> serde::de::Visitor<'de> for CustomVisitor {
|
||||||
|
type Value = Vec<Address>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a non-empty list of recipient addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<S>(self, mut access: S) -> Result<Self::Value, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::de::SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut seq: Vec<Address> = Vec::with_capacity(access.size_hint().unwrap_or(0));
|
||||||
|
while let Some(key) = access.next_element()? {
|
||||||
|
seq.push(key);
|
||||||
|
}
|
||||||
|
if seq.is_empty() {
|
||||||
|
Err(serde::de::Error::invalid_length(seq.len(), &self))
|
||||||
|
} else {
|
||||||
|
Ok(seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Address {
|
|||||||
{
|
{
|
||||||
struct FieldVisitor;
|
struct FieldVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for FieldVisitor {
|
impl Visitor<'_> for FieldVisitor {
|
||||||
type Value = Field;
|
type Value = Field;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use idna::domain_to_ascii;
|
|||||||
///
|
///
|
||||||
/// This type contains email in canonical form (_user@domain.tld_).
|
/// This type contains email in canonical form (_user@domain.tld_).
|
||||||
///
|
///
|
||||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -227,6 +227,7 @@ fn check_address(val: &str) -> Result<usize, AddressError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
#[non_exhaustive]
|
||||||
/// Errors in email addresses parsing
|
/// Errors in email addresses parsing
|
||||||
pub enum AddressError {
|
pub enum AddressError {
|
||||||
/// Missing domain or user
|
/// Missing domain or user
|
||||||
@@ -237,6 +238,8 @@ pub enum AddressError {
|
|||||||
InvalidUser,
|
InvalidUser,
|
||||||
/// Invalid email domain
|
/// Invalid email domain
|
||||||
InvalidDomain,
|
InvalidDomain,
|
||||||
|
/// Invalid input found
|
||||||
|
InvalidInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for AddressError {}
|
impl Error for AddressError {}
|
||||||
@@ -248,6 +251,7 @@ impl Display for AddressError {
|
|||||||
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
|
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
|
||||||
AddressError::InvalidUser => f.write_str("Invalid email user"),
|
AddressError::InvalidUser => f.write_str("Invalid email user"),
|
||||||
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
|
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
|
||||||
|
AddressError::InvalidInput => f.write_str("Invalid input"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ 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;
|
||||||
@@ -82,8 +83,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 trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
|
pub(crate) 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`
|
||||||
@@ -133,8 +134,8 @@ 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-tls"))]
|
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls"))]
|
||||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
@@ -147,14 +148,14 @@ impl Executor for Tokio1Executor {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls"))]
|
||||||
match tls {
|
match tls {
|
||||||
Tls::Opportunistic(ref tls_parameters) => {
|
Tls::Opportunistic(tls_parameters) => {
|
||||||
if conn.can_starttls() {
|
if conn.can_starttls() {
|
||||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tls::Required(ref tls_parameters) => {
|
Tls::Required(tls_parameters) => {
|
||||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
@@ -177,7 +178,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +202,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 = async_std::task::JoinHandle<()>;
|
type Handle = futures_util::future::AbortHandle;
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
type Sleep = BoxFuture<'static, ()>;
|
type Sleep = BoxFuture<'static, ()>;
|
||||||
|
|
||||||
@@ -211,12 +212,14 @@ impl Executor for AsyncStd1Executor {
|
|||||||
F: Future<Output = ()> + Send + 'static,
|
F: Future<Output = ()> + Send + 'static,
|
||||||
F::Output: Send + 'static,
|
F::Output: Send + 'static,
|
||||||
{
|
{
|
||||||
async_std::task::spawn(fut)
|
let (handle, registration) = futures_util::future::AbortHandle::new_pair();
|
||||||
|
async_std::task::spawn(futures_util::future::Abortable::new(fut, registration));
|
||||||
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
fn sleep(duration: Duration) -> Self::Sleep {
|
fn sleep(duration: Duration) -> Self::Sleep {
|
||||||
let fut = async move { async_std::task::sleep(duration).await };
|
let fut = async_std::task::sleep(duration);
|
||||||
Box::pin(fut)
|
Box::pin(fut)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,8 +233,8 @@ impl Executor for AsyncStd1Executor {
|
|||||||
) -> Result<AsyncSmtpConnection, Error> {
|
) -> Result<AsyncSmtpConnection, Error> {
|
||||||
#[allow(clippy::match_single_binding)]
|
#[allow(clippy::match_single_binding)]
|
||||||
let tls_parameters = match tls {
|
let tls_parameters = match tls {
|
||||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
@@ -243,14 +246,14 @@ impl Executor for AsyncStd1Executor {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
match tls {
|
match tls {
|
||||||
Tls::Opportunistic(ref tls_parameters) => {
|
Tls::Opportunistic(tls_parameters) => {
|
||||||
if conn.can_starttls() {
|
if conn.can_starttls() {
|
||||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tls::Required(ref tls_parameters) => {
|
Tls::Required(tls_parameters) => {
|
||||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
@@ -272,9 +275,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 async_std::task::JoinHandle<()> {
|
impl SpawnHandle for futures_util::future::AbortHandle {
|
||||||
async fn shutdown(self) {
|
async fn shutdown(&self) {
|
||||||
self.cancel().await;
|
self.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,5 +294,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 async_std::task::JoinHandle<()> {}
|
impl Sealed for futures_util::future::AbortHandle {}
|
||||||
}
|
}
|
||||||
|
|||||||
142
src/lib.rs
142
src/lib.rs
@@ -6,7 +6,7 @@
|
|||||||
//! * Secure defaults
|
//! * Secure defaults
|
||||||
//! * Async support
|
//! * Async support
|
||||||
//!
|
//!
|
||||||
//! Lettre requires Rust 1.60 or newer.
|
//! Lettre requires Rust 1.74 or newer.
|
||||||
//!
|
//!
|
||||||
//! ## Features
|
//! ## Features
|
||||||
//!
|
//!
|
||||||
@@ -34,13 +34,25 @@
|
|||||||
//!
|
//!
|
||||||
//! _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 on Linux.
|
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL
|
||||||
|
//! on all other platforms.
|
||||||
//!
|
//!
|
||||||
//! * **native-tls** 📫: TLS support for the synchronous version of the API
|
//! * **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_
|
||||||
@@ -52,13 +64,46 @@
|
|||||||
//!
|
//!
|
||||||
//! #### SMTP over TLS via the rustls crate
|
//! #### SMTP over TLS via the rustls crate
|
||||||
//!
|
//!
|
||||||
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
|
//! _Secure SMTP connections using TLS from the `rustls` crate_
|
||||||
//!
|
//!
|
||||||
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
|
//! * **rustls**: TLS support for the synchronous version of the API
|
||||||
|
//! * **tokio1-rustls**: TLS support for the `tokio1` async version of the API
|
||||||
|
//! * **async-std1-rustls**: TLS support for the `async-std1` async version of the API
|
||||||
//!
|
//!
|
||||||
//! * **rustls-tls**: TLS support for the synchronous version of the API
|
//! ##### rustls crypto backends
|
||||||
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
|
//!
|
||||||
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
|
//! _The crypto implementation to use with rustls_
|
||||||
|
//!
|
||||||
|
//! When the `rustls` feature is enabled, one of the following crypto backends MUST also
|
||||||
|
//! be enabled.
|
||||||
|
//!
|
||||||
|
//! * **aws-lc-rs**: use [AWS-LC] (via [`aws-lc-rs`]) as the `rustls` crypto backend
|
||||||
|
//! * **ring**: use [`ring`] as the `rustls` crypto backend
|
||||||
|
//!
|
||||||
|
//! When enabling `aws-lc-rs`, the `fips` feature can also be enabled to have
|
||||||
|
//! rustls use the FIPS certified module of AWS-LC.
|
||||||
|
//!
|
||||||
|
//! `aws-lc-rs` may require cmake on some platforms to compile.
|
||||||
|
//! `fips` always requires cmake and the Go compiler to compile.
|
||||||
|
//!
|
||||||
|
//! ##### rustls certificate verification backend
|
||||||
|
//!
|
||||||
|
//! _The TLS certificate verification backend to use with rustls_
|
||||||
|
//!
|
||||||
|
//! When the `rustls` feature is enabled, one of the following verification backends
|
||||||
|
//! MUST also be enabled.
|
||||||
|
//!
|
||||||
|
//! * **rustls-native-certs**: verify TLS certificates using the platform's native certificate store (see [`rustls-native-certs`])
|
||||||
|
//! * **webpki-roots**: verify TLS certificates against Mozilla's root certificates (see [`webpki-roots`])
|
||||||
|
//!
|
||||||
|
//! For the `rustls-native-certs` backend to work correctly, the following packages
|
||||||
|
//! will need to be installed in order for the build stage and the compiled program
|
||||||
|
//! to run properly.
|
||||||
|
//!
|
||||||
|
//! | Distro | Build-time packages | Runtime packages |
|
||||||
|
//! | ------------ | -------------------------- | ---------------------------- |
|
||||||
|
//! | Debian | none | `ca-certificates` |
|
||||||
|
//! | Alpine Linux | none | `ca-certificates` |
|
||||||
//!
|
//!
|
||||||
//! ### Sendmail transport
|
//! ### Sendmail transport
|
||||||
//!
|
//!
|
||||||
@@ -95,6 +140,7 @@
|
|||||||
//! * **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
|
||||||
@@ -102,18 +148,22 @@
|
|||||||
//! [`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
|
||||||
//! [ring]: https://github.com/briansmith/ring#ring
|
//! [AWS-LC]: https://github.com/aws/aws-lc
|
||||||
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
|
//! [`aws-lc-rs`]: https://crates.io/crates/aws-lc-rs
|
||||||
|
//! [`ring`]: https://crates.io/crates/ring
|
||||||
|
//! [`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.10.3")]
|
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.15")]
|
||||||
#![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,
|
||||||
@@ -137,12 +187,35 @@
|
|||||||
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-native-certs"),
|
||||||
|
not(feature = "webpki-roots")
|
||||||
|
))]
|
||||||
|
compile_error!(
|
||||||
|
"feature `rustls` also requires either 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.");
|
||||||
@@ -153,16 +226,12 @@ 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-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
If you were trying to opt into `rustls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(feature = "tokio1", feature = "rustls", not(feature = "tokio1-rustls")))]
|
||||||
feature = "tokio1",
|
compile_error!("Lettre is being built with the `tokio1` and the `rustls` features, but the `tokio1-rustls` feature hasn't been turned on.
|
||||||
feature = "rustls-tls",
|
If you'd like to use `native-tls` make sure that the `rustls` feature hasn't been enabled by mistake.
|
||||||
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(
|
||||||
@@ -171,36 +240,22 @@ 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-tls` feature hasn't been enabled by mistake.
|
If you'd like to use `boring-tls` make sure that the `rustls` feature hasn't been enabled by mistake.
|
||||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||||
|
|
||||||
/*
|
#[cfg(all(feature = "async-std1", feature = "native-tls"))]
|
||||||
#[cfg(all(
|
|
||||||
feature = "async-std1",
|
|
||||||
feature = "native-tls",
|
|
||||||
not(feature = "async-std1-native-tls")
|
|
||||||
))]
|
|
||||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the `async-std1-native-tls` feature hasn't been turned on.
|
|
||||||
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
|
|
||||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
|
||||||
*/
|
|
||||||
#[cfg(all(
|
|
||||||
feature = "async-std1",
|
|
||||||
feature = "native-tls",
|
|
||||||
not(feature = "async-std1-native-tls")
|
|
||||||
))]
|
|
||||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
|
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
|
||||||
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
|
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
|
||||||
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
If you were trying to opt into `rustls` 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-tls",
|
feature = "rustls",
|
||||||
not(feature = "async-std1-rustls-tls")
|
not(feature = "async-std1-rustls")
|
||||||
))]
|
))]
|
||||||
compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
|
compile_error!("Lettre is being built with the `async-std1` and the `rustls` features, but the `async-std1-rustls` feature hasn't been turned on.
|
||||||
If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
|
If you'd like to use `native-tls` make sure that the `rustls` hasn't been enabled by mistake.
|
||||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,17 +268,20 @@ 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;
|
||||||
|
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
pub use self::executor::AsyncStd1Executor;
|
pub use self::executor::AsyncStd1Executor;
|
||||||
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
pub use self::executor::Executor;
|
pub use self::executor::Executor;
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
pub use self::executor::Tokio1Executor;
|
pub use self::executor::Tokio1Executor;
|
||||||
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use self::transport::AsyncTransport;
|
pub use self::transport::AsyncTransport;
|
||||||
pub use crate::address::Address;
|
pub use crate::address::Address;
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ pub struct Attachment {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum Disposition {
|
enum Disposition {
|
||||||
/// file name
|
/// File name
|
||||||
Attached(String),
|
Attached(String),
|
||||||
/// content id
|
/// Content id
|
||||||
Inline(String),
|
Inline(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn base64_encode_bytes_wrapping() {
|
fn base64_encode_bytes_wrapping() {
|
||||||
let encoded = Body::new_with_encoding(
|
let encoded = Body::new_with_encoding(
|
||||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
|
||||||
ContentTransferEncoding::Base64,
|
ContentTransferEncoding::Base64,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use std::{
|
|||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
fmt::{self, Display},
|
fmt::{self, Display},
|
||||||
iter::IntoIterator,
|
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ impl Display for DkimSigningAlgorithm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describe DkimSigning key error
|
/// Describe [`DkimSigningKey`] key error
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
|
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
|
||||||
|
|
||||||
@@ -101,14 +100,14 @@ 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);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum InnerDkimSigningKey {
|
enum InnerDkimSigningKey {
|
||||||
Rsa(RsaPrivateKey),
|
Rsa(RsaPrivateKey),
|
||||||
Ed25519(ed25519_dalek::Keypair),
|
Ed25519(ed25519_dalek::SigningKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DkimSigningKey {
|
impl DkimSigningKey {
|
||||||
@@ -121,14 +120,18 @@ impl DkimSigningKey {
|
|||||||
RsaPrivateKey::from_pkcs1_pem(private_key)
|
RsaPrivateKey::from_pkcs1_pem(private_key)
|
||||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
|
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
|
||||||
),
|
),
|
||||||
DkimSigningAlgorithm::Ed25519 => InnerDkimSigningKey::Ed25519(
|
DkimSigningAlgorithm::Ed25519 => {
|
||||||
ed25519_dalek::Keypair::from_bytes(
|
InnerDkimSigningKey::Ed25519(ed25519_dalek::SigningKey::from_bytes(
|
||||||
&crate::base64::decode(private_key).map_err(|err| {
|
&crate::base64::decode(private_key)
|
||||||
DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err))
|
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)))?
|
||||||
})?,
|
.try_into()
|
||||||
)
|
.map_err(|_| {
|
||||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
|
DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(
|
||||||
),
|
ed25519_dalek::ed25519::Error::new(),
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
))
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
|
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
|
||||||
@@ -140,19 +143,18 @@ impl DkimSigningKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A struct to describe Dkim configuration applied when signing a message
|
/// A struct to describe Dkim configuration applied when signing a message
|
||||||
/// selector: the name of the key publied in DNS
|
|
||||||
/// domain: the domain for which we sign the message
|
|
||||||
/// private_key: private key in PKCS1 string format
|
|
||||||
/// headers: a list of headers name to be included in the signature. Signing of more than one
|
|
||||||
/// header with same name is not supported
|
|
||||||
/// canonicalization: the canonicalization to be applied on the message
|
|
||||||
/// pub signing_algorithm: the signing algorithm to be used when signing
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DkimConfig {
|
pub struct DkimConfig {
|
||||||
|
/// The name of the key published in DNS
|
||||||
selector: String,
|
selector: String,
|
||||||
|
/// The domain for which we sign the message
|
||||||
domain: String,
|
domain: String,
|
||||||
|
/// The private key in PKCS1 string format
|
||||||
private_key: DkimSigningKey,
|
private_key: DkimSigningKey,
|
||||||
|
/// A list of header names to be included in the signature. Signing of more than one
|
||||||
|
/// header with the same name is not supported
|
||||||
headers: Vec<HeaderName>,
|
headers: Vec<HeaderName>,
|
||||||
|
/// The signing algorithm to be used when signing
|
||||||
canonicalization: DkimCanonicalization,
|
canonicalization: DkimCanonicalization,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,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,
|
||||||
@@ -281,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);
|
||||||
}
|
}
|
||||||
[] => {}
|
[] => {}
|
||||||
}
|
}
|
||||||
@@ -315,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,
|
||||||
@@ -341,11 +343,10 @@ fn dkim_canonicalize_headers<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sign with Dkim a message by adding Dkim-Signture 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, SystemTime::now())
|
dkim_sign_fixed_time(message, dkim_config, crate::time::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
|
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
|
||||||
@@ -376,7 +377,7 @@ fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timesta
|
|||||||
}
|
}
|
||||||
let dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
|
let 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(|h| h.as_ref()),
|
dkim_config.headers.iter().map(AsRef::as_ref),
|
||||||
headers,
|
headers,
|
||||||
dkim_config.canonicalization.header,
|
dkim_config.canonicalization.header,
|
||||||
);
|
);
|
||||||
@@ -486,14 +487,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]
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ use crate::BoxError;
|
|||||||
/// use-caches this header shouldn't be set manually.
|
/// use-caches this header shouldn't be set manually.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
#[derive(Default)]
|
||||||
pub enum ContentTransferEncoding {
|
pub enum ContentTransferEncoding {
|
||||||
/// ASCII
|
/// ASCII
|
||||||
SevenBit,
|
SevenBit,
|
||||||
/// Quoted-Printable encoding
|
/// Quoted-Printable encoding
|
||||||
QuotedPrintable,
|
QuotedPrintable,
|
||||||
/// base64 encoding
|
/// base64 encoding
|
||||||
|
#[default]
|
||||||
Base64,
|
Base64,
|
||||||
/// Requires `8BITMIME`
|
/// Requires `8BITMIME`
|
||||||
EightBit,
|
EightBit,
|
||||||
@@ -67,12 +69,6 @@ impl FromStr for ContentTransferEncoding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ContentTransferEncoding {
|
|
||||||
fn default() -> Self {
|
|
||||||
ContentTransferEncoding::Base64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use email_encoding::headers::EmailWriter;
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
use super::{Header, HeaderName, HeaderValue};
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
@@ -22,7 +22,7 @@ impl ContentDisposition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An attachment which should be displayed inline into the message, but that also
|
/// An attachment which should be displayed inline into the message, but that also
|
||||||
/// species the filename in case it were to be downloaded
|
/// species the filename in case it is downloaded
|
||||||
pub fn inline_with_name(file_name: &str) -> Self {
|
pub fn inline_with_name(file_name: &str) -> Self {
|
||||||
Self::with_name("inline", file_name)
|
Self::with_name("inline", file_name)
|
||||||
}
|
}
|
||||||
@@ -38,10 +38,10 @@ impl ContentDisposition {
|
|||||||
let mut encoded_value = String::new();
|
let mut encoded_value = String::new();
|
||||||
let line_len = "Content-Disposition: ".len();
|
let line_len = "Content-Disposition: ".len();
|
||||||
{
|
{
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||||
w.write_str(kind).expect("writing `kind` returned an error");
|
w.write_str(kind).expect("writing `kind` returned an error");
|
||||||
w.write_char(';').expect("writing `;` returned an error");
|
w.write_char(';').expect("writing `;` returned an error");
|
||||||
w.optional_breakpoint();
|
w.space();
|
||||||
|
|
||||||
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
|
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
|
||||||
.expect("some Write implementation returned an error");
|
.expect("some Write implementation returned an error");
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ use crate::BoxError;
|
|||||||
|
|
||||||
/// `Content-Type` of the body
|
/// `Content-Type` of the body
|
||||||
///
|
///
|
||||||
/// This struct can represent any valid [mime type], which can be parsed via
|
/// This struct can represent any valid [MIME type], which can be parsed via
|
||||||
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
|
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
|
||||||
///
|
///
|
||||||
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
|
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
|
||||||
///
|
///
|
||||||
/// [mime type]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
/// [MIME type]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ContentType(Mime);
|
pub struct ContentType(Mime);
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ mod serde {
|
|||||||
{
|
{
|
||||||
struct ContentTypeVisitor;
|
struct ContentTypeVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for ContentTypeVisitor {
|
impl Visitor<'_> 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
|
||||||
|
|||||||
@@ -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(SystemTime::now())
|
Self::new(crate::time::now())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use email_encoding::headers::EmailWriter;
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
use super::{Header, HeaderName, HeaderValue};
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -31,7 +31,7 @@ macro_rules! mailbox_header {
|
|||||||
let mut encoded_value = String::new();
|
let mut encoded_value = String::new();
|
||||||
let line_len = $header_name.len() + ": ".len();
|
let line_len = $header_name.len() + ": ".len();
|
||||||
{
|
{
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||||
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ macro_rules! mailboxes_header {
|
|||||||
let mut encoded_value = String::new();
|
let mut encoded_value = String::new();
|
||||||
let line_len = $header_name.len() + ": ".len();
|
let line_len = $header_name.len() + ": ".len();
|
||||||
{
|
{
|
||||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||||
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ mailbox_header! {
|
|||||||
|
|
||||||
`Sender` header
|
`Sender` header
|
||||||
|
|
||||||
This header contains [`Mailbox`][self::Mailbox] associated with sender.
|
This header contains [`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`][self::Mailboxes].
|
This header contains [`Mailboxes`].
|
||||||
|
|
||||||
*/
|
*/
|
||||||
(From, "From")
|
(From, "From")
|
||||||
@@ -135,7 +135,7 @@ mailboxes_header! {
|
|||||||
|
|
||||||
`Reply-To` header
|
`Reply-To` header
|
||||||
|
|
||||||
This header contains [`Mailboxes`][self::Mailboxes].
|
This header contains [`Mailboxes`].
|
||||||
|
|
||||||
*/
|
*/
|
||||||
(ReplyTo, "Reply-To")
|
(ReplyTo, "Reply-To")
|
||||||
@@ -146,7 +146,7 @@ mailboxes_header! {
|
|||||||
|
|
||||||
`To` header
|
`To` header
|
||||||
|
|
||||||
This header contains [`Mailboxes`][self::Mailboxes].
|
This header contains [`Mailboxes`].
|
||||||
|
|
||||||
*/
|
*/
|
||||||
(To, "To")
|
(To, "To")
|
||||||
@@ -157,7 +157,7 @@ mailboxes_header! {
|
|||||||
|
|
||||||
`Cc` header
|
`Cc` header
|
||||||
|
|
||||||
This header contains [`Mailboxes`][self::Mailboxes].
|
This header contains [`Mailboxes`].
|
||||||
|
|
||||||
*/
|
*/
|
||||||
(Cc, "Cc")
|
(Cc, "Cc")
|
||||||
@@ -168,7 +168,7 @@ mailboxes_header! {
|
|||||||
|
|
||||||
`Bcc` header
|
`Bcc` header
|
||||||
|
|
||||||
This header contains [`Mailboxes`][self::Mailboxes].
|
This header contains [`Mailboxes`].
|
||||||
|
|
||||||
*/
|
*/
|
||||||
(Bcc, "Bcc")
|
(Bcc, "Bcc")
|
||||||
@@ -306,14 +306,30 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parse_multi_with_name_containing_comma() {
|
fn parse_multi_with_name_containing_comma() {
|
||||||
let from: Vec<Mailbox> = vec![
|
let from: Vec<Mailbox> = vec![
|
||||||
"Test, test <1@example.com>".parse().unwrap(),
|
"\"Test, test\" <1@example.com>".parse().unwrap(),
|
||||||
"Test2, test2 <2@example.com>".parse().unwrap(),
|
"\"Test2, test2\" <2@example.com>".parse().unwrap(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(HeaderValue::new(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("From"),
|
HeaderName::new_from_ascii_str("From"),
|
||||||
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_owned(),
|
"\"Test, test\" <1@example.com>, \"Test2, test2\" <2@example.com>".to_owned(),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multi_with_name_containing_double_quotes() {
|
||||||
|
let from: Vec<Mailbox> = vec![
|
||||||
|
"\"Test, test\" <1@example.com>".parse().unwrap(),
|
||||||
|
"\"Test2, \"test2\"\" <2@example.com>".parse().unwrap(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut headers = Headers::new();
|
||||||
|
headers.insert_raw(HeaderValue::new(
|
||||||
|
HeaderName::new_from_ascii_str("From"),
|
||||||
|
"\"Test, test\" <1@example.com>, \"Test2, \"test2\"\" <2@example.com>".to_owned(),
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||||
@@ -324,9 +340,20 @@ mod test {
|
|||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(HeaderValue::new(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("From"),
|
HeaderName::new_from_ascii_str("From"),
|
||||||
"Test, test <1@example.com>, Test2, test2".to_owned(),
|
"\"Test, test\" <1@example.com>, \"Test2, test2\"".to_owned(),
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<From>(), None);
|
assert_eq!(headers.get::<From>(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mailbox_format_address_with_angle_bracket() {
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{}",
|
||||||
|
Mailbox::new(Some("<3".into()), "i@love.example".parse().unwrap())
|
||||||
|
),
|
||||||
|
r#""<3" <i@love.example>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::{
|
|||||||
ops::Deref,
|
ops::Deref,
|
||||||
};
|
};
|
||||||
|
|
||||||
use email_encoding::headers::EmailWriter;
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
content::*,
|
content::*,
|
||||||
@@ -66,7 +66,7 @@ impl Headers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a copy of an `Header` present in `Headers`
|
/// Returns a copy of a `Header` present in `Headers`
|
||||||
///
|
///
|
||||||
/// Returns `None` if `Header` isn't present in `Headers`.
|
/// Returns `None` if `Header` isn't present in `Headers`.
|
||||||
pub fn get<H: Header>(&self) -> Option<H> {
|
pub fn get<H: Header>(&self) -> Option<H> {
|
||||||
@@ -124,22 +124,18 @@ impl Headers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
|
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
|
||||||
self.headers
|
self.headers.iter().find(|value| name == value.name)
|
||||||
.iter()
|
|
||||||
.find(|value| name.eq_ignore_ascii_case(&value.name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
|
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
|
||||||
self.headers
|
self.headers.iter_mut().find(|value| name == value.name)
|
||||||
.iter_mut()
|
|
||||||
.find(|value| name.eq_ignore_ascii_case(&value.name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_header_index(&self, name: &str) -> Option<usize> {
|
fn find_header_index(&self, name: &str) -> Option<usize> {
|
||||||
self.headers
|
self.headers
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|(_i, value)| name.eq_ignore_ascii_case(&value.name))
|
.find(|(_i, value)| name == value.name)
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,18 +157,9 @@ impl Display for Headers {
|
|||||||
/// A possible error when converting a `HeaderName` from another type.
|
/// A possible error when converting a `HeaderName` from another type.
|
||||||
// comes from `http` crate
|
// comes from `http` crate
|
||||||
#[allow(missing_copy_implementations)]
|
#[allow(missing_copy_implementations)]
|
||||||
#[derive(Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct InvalidHeaderName {
|
#[non_exhaustive]
|
||||||
_priv: (),
|
pub struct InvalidHeaderName;
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for InvalidHeaderName {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("InvalidHeaderName")
|
|
||||||
// skip _priv noise
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for InvalidHeaderName {
|
impl fmt::Display for InvalidHeaderName {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
@@ -189,14 +176,11 @@ pub struct HeaderName(Cow<'static, str>);
|
|||||||
impl HeaderName {
|
impl HeaderName {
|
||||||
/// Creates a new header name
|
/// Creates a new header name
|
||||||
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
|
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
|
||||||
if !ascii.is_empty()
|
if !ascii.is_empty() && ascii.len() <= 76 && ascii.is_ascii() && !ascii.contains([':', ' '])
|
||||||
&& ascii.len() <= 76
|
|
||||||
&& ascii.is_ascii()
|
|
||||||
&& !ascii.contains(|c| c == ':' || c == ' ')
|
|
||||||
{
|
{
|
||||||
Ok(Self(Cow::Owned(ascii)))
|
Ok(Self(Cow::Owned(ascii)))
|
||||||
} else {
|
} else {
|
||||||
Err(InvalidHeaderName { _priv: () })
|
Err(InvalidHeaderName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,23 +241,19 @@ impl AsRef<str> for HeaderName {
|
|||||||
|
|
||||||
impl PartialEq<HeaderName> for HeaderName {
|
impl PartialEq<HeaderName> for HeaderName {
|
||||||
fn eq(&self, other: &HeaderName) -> bool {
|
fn eq(&self, other: &HeaderName) -> bool {
|
||||||
let s1: &str = self.as_ref();
|
self.eq_ignore_ascii_case(other)
|
||||||
let s2: &str = other.as_ref();
|
|
||||||
s1 == s2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq<&str> for HeaderName {
|
impl PartialEq<&str> for HeaderName {
|
||||||
fn eq(&self, other: &&str) -> bool {
|
fn eq(&self, other: &&str) -> bool {
|
||||||
let s: &str = self.as_ref();
|
self.eq_ignore_ascii_case(other)
|
||||||
s == *other
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq<HeaderName> for &str {
|
impl PartialEq<HeaderName> for &str {
|
||||||
fn eq(&self, other: &HeaderName) -> bool {
|
fn eq(&self, other: &HeaderName) -> bool {
|
||||||
let s: &str = other.as_ref();
|
self.eq_ignore_ascii_case(other)
|
||||||
*self == s
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +290,7 @@ impl HeaderValue {
|
|||||||
/// acceptable for use if `encoded_value` contains only ascii
|
/// acceptable for use if `encoded_value` contains only ascii
|
||||||
/// printable characters and is already line folded.
|
/// printable characters and is already line folded.
|
||||||
///
|
///
|
||||||
/// When in doubt use [`HeaderValue::new`].
|
/// When in doubt, use [`HeaderValue::new`].
|
||||||
pub fn dangerous_new_pre_encoded(
|
pub fn dangerous_new_pre_encoded(
|
||||||
name: HeaderName,
|
name: HeaderName,
|
||||||
raw_value: String,
|
raw_value: String,
|
||||||
@@ -348,7 +328,7 @@ impl<'a> HeaderValueEncoder<'a> {
|
|||||||
|
|
||||||
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
|
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
|
||||||
let line_len = name.len() + ": ".len();
|
let line_len = name.len() + ": ".len();
|
||||||
let writer = EmailWriter::new(writer, line_len, 0, false, false);
|
let writer = EmailWriter::new(writer, line_len, 0, false);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
writer,
|
writer,
|
||||||
@@ -435,7 +415,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_headername() {
|
fn empty_headername() {
|
||||||
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
|
assert!(HeaderName::new_from_ascii("".to_owned()).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -467,6 +447,60 @@ mod tests {
|
|||||||
let _ = HeaderName::new_from_ascii_str("");
|
let _ = HeaderName::new_from_ascii_str("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headername_headername_eq() {
|
||||||
|
assert_eq!(
|
||||||
|
HeaderName::new_from_ascii_str("From"),
|
||||||
|
HeaderName::new_from_ascii_str("From")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headername_str_eq() {
|
||||||
|
assert_eq!(HeaderName::new_from_ascii_str("From"), "From");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn str_headername_eq() {
|
||||||
|
assert_eq!("From", HeaderName::new_from_ascii_str("From"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headername_headername_eq_case_insensitive() {
|
||||||
|
assert_eq!(
|
||||||
|
HeaderName::new_from_ascii_str("From"),
|
||||||
|
HeaderName::new_from_ascii_str("from")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headername_str_eq_case_insensitive() {
|
||||||
|
assert_eq!(HeaderName::new_from_ascii_str("From"), "from");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn str_headername_eq_case_insensitive() {
|
||||||
|
assert_eq!("from", HeaderName::new_from_ascii_str("From"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headername_headername_ne() {
|
||||||
|
assert_ne!(
|
||||||
|
HeaderName::new_from_ascii_str("From"),
|
||||||
|
HeaderName::new_from_ascii_str("To")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn headername_str_ne() {
|
||||||
|
assert_ne!(HeaderName::new_from_ascii_str("From"), "To");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn str_headername_ne() {
|
||||||
|
assert_ne!("From", HeaderName::new_from_ascii_str("To"));
|
||||||
|
}
|
||||||
|
|
||||||
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
|
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -612,17 +646,14 @@ mod tests {
|
|||||||
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
|
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// TODO: fix the fact that the encoder doesn't know that
|
|
||||||
// the space between the name and the address should be
|
|
||||||
// removed when wrapping.
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||||
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -687,9 +718,6 @@ mod tests {
|
|||||||
"quoted-printable".to_owned(),
|
"quoted-printable".to_owned(),
|
||||||
));
|
));
|
||||||
|
|
||||||
// TODO: fix the fact that the encoder doesn't know that
|
|
||||||
// the space between the name and the address should be
|
|
||||||
// removed when wrapping.
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
@@ -699,8 +727,8 @@ mod tests {
|
|||||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||||
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||||
"From: Someone <somewhere@example.com>\r\n",
|
"From: Someone <somewhere@example.com>\r\n",
|
||||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod parsers;
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
mod serde;
|
mod serde;
|
||||||
mod types;
|
mod types;
|
||||||
|
|||||||
5
src/message/mailbox/parsers/mod.rs
Normal file
5
src/message/mailbox/parsers/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod rfc2234;
|
||||||
|
mod rfc2822;
|
||||||
|
mod rfc5336;
|
||||||
|
|
||||||
|
pub(crate) use rfc2822::{mailbox, mailbox_list};
|
||||||
32
src/message/mailbox/parsers/rfc2234.rs
Normal file
32
src/message/mailbox/parsers/rfc2234.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//! Partial parsers implementation of [RFC2234]: Augmented BNF for
|
||||||
|
//! Syntax Specifications: ABNF.
|
||||||
|
//!
|
||||||
|
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
|
||||||
|
|
||||||
|
use chumsky::{error::Cheap, prelude::*};
|
||||||
|
|
||||||
|
// 6.1 Core Rules
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1
|
||||||
|
|
||||||
|
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
||||||
|
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
filter(|c: &char| c.is_ascii_alphabetic())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DIGIT = %x30-39
|
||||||
|
// ; 0-9
|
||||||
|
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
filter(|c: &char| c.is_ascii_digit())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DQUOTE = %x22
|
||||||
|
// ; " (Double Quote)
|
||||||
|
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
just('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// WSP = SP / HTAB
|
||||||
|
// ; white space
|
||||||
|
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
choice((just(' '), just('\t')))
|
||||||
|
}
|
||||||
250
src/message/mailbox/parsers/rfc2822.rs
Normal file
250
src/message/mailbox/parsers/rfc2822.rs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
//! Partial parsers implementation of [RFC2822]: Internet Message
|
||||||
|
//! Format.
|
||||||
|
//!
|
||||||
|
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
|
||||||
|
|
||||||
|
use chumsky::{error::Cheap, prelude::*};
|
||||||
|
|
||||||
|
use super::{rfc2234, rfc5336};
|
||||||
|
|
||||||
|
// 3.2.1. Primitive Tokens
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
|
||||||
|
|
||||||
|
// NO-WS-CTL = %d1-8 / ; US-ASCII control characters
|
||||||
|
// %d11 / ; that do not include the
|
||||||
|
// %d12 / ; carriage return, line feed,
|
||||||
|
// %d14-31 / ; and white space characters
|
||||||
|
// %d127
|
||||||
|
fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
filter(|c| matches!(u32::from(*c), 1..=8 | 11 | 12 | 14..=31 | 127))
|
||||||
|
}
|
||||||
|
|
||||||
|
// text = %d1-9 / ; Characters excluding CR and LF
|
||||||
|
// %d11 /
|
||||||
|
// %d12 /
|
||||||
|
// %d14-127 /
|
||||||
|
// obs-text
|
||||||
|
fn text() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
filter(|c| matches!(u32::from(*c), 1..=9 | 11 | 12 | 14..=127))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2.2. Quoted characters
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
|
||||||
|
|
||||||
|
// quoted-pair = ("\" text) / obs-qp
|
||||||
|
fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
just('\\').ignore_then(text())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2.3. Folding white space and comments
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.3
|
||||||
|
|
||||||
|
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
|
||||||
|
// obs-FWS
|
||||||
|
pub(super) fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||||
|
rfc2234::wsp()
|
||||||
|
.or_not()
|
||||||
|
.then_ignore(rfc2234::wsp().ignored().repeated())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
|
||||||
|
pub(super) fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||||
|
// TODO: comment are not currently supported, so for now a cfws is
|
||||||
|
// the same as a fws.
|
||||||
|
fws()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2.4. Atom
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
|
||||||
|
|
||||||
|
// atext = ALPHA / DIGIT / ; Any character except controls,
|
||||||
|
// "!" / "#" / ; SP, and specials.
|
||||||
|
// "$" / "%" / ; Used for atoms
|
||||||
|
// "&" / "'" /
|
||||||
|
// "*" / "+" /
|
||||||
|
// "-" / "/" /
|
||||||
|
// "=" / "?" /
|
||||||
|
// "^" / "_" /
|
||||||
|
// "`" / "{" /
|
||||||
|
// "|" / "}" /
|
||||||
|
// "~"
|
||||||
|
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
choice((
|
||||||
|
rfc2234::alpha(),
|
||||||
|
rfc2234::digit(),
|
||||||
|
filter(|c| {
|
||||||
|
matches!(
|
||||||
|
*c,
|
||||||
|
'!' | '#'
|
||||||
|
| '$'
|
||||||
|
| '%'
|
||||||
|
| '&'
|
||||||
|
| '\''
|
||||||
|
| '*'
|
||||||
|
| '+'
|
||||||
|
| '-'
|
||||||
|
| '/'
|
||||||
|
| '='
|
||||||
|
| '?'
|
||||||
|
| '^'
|
||||||
|
| '_'
|
||||||
|
| '`'
|
||||||
|
| '{'
|
||||||
|
| '|'
|
||||||
|
| '}'
|
||||||
|
| '~'
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
// also allow non ASCII UTF8 chars
|
||||||
|
rfc5336::utf8_non_ascii(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// atom = [CFWS] 1*atext [CFWS]
|
||||||
|
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
cfws().chain(atext().repeated().at_least(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// dot-atom = [CFWS] dot-atom-text [CFWS]
|
||||||
|
pub(super) fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
cfws().chain(dot_atom_text())
|
||||||
|
}
|
||||||
|
|
||||||
|
// dot-atom-text = 1*atext *("." 1*atext)
|
||||||
|
pub(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
atext().repeated().at_least(1).chain(
|
||||||
|
just('.')
|
||||||
|
.chain(atext().repeated().at_least(1))
|
||||||
|
.repeated()
|
||||||
|
.at_least(1)
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2.5. Quoted strings
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
||||||
|
|
||||||
|
// qtext = NO-WS-CTL / ; Non white space controls
|
||||||
|
//
|
||||||
|
// %d33 / ; The rest of the US-ASCII
|
||||||
|
// %d35-91 / ; characters not including "\"
|
||||||
|
// %d93-126 ; or the quote character
|
||||||
|
fn qtext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
choice((
|
||||||
|
filter(|c| matches!(u32::from(*c), 33 | 35..=91 | 93..=126)),
|
||||||
|
no_ws_ctl(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// qcontent = qtext / quoted-pair
|
||||||
|
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// quoted-string = [CFWS]
|
||||||
|
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
|
||||||
|
// [CFWS]
|
||||||
|
fn quoted_string() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
rfc2234::dquote()
|
||||||
|
.ignore_then(fws().chain(qcontent()).repeated().flatten())
|
||||||
|
.then_ignore(text::whitespace())
|
||||||
|
.then_ignore(rfc2234::dquote())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2.6. Miscellaneous tokens
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
|
||||||
|
|
||||||
|
// word = atom / quoted-string
|
||||||
|
fn word() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
choice((quoted_string(), atom()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// phrase = 1*word / obs-phrase
|
||||||
|
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
choice((obs_phrase(), word().repeated().at_least(1).flatten()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.4. Address Specification
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||||
|
|
||||||
|
// mailbox = name-addr / addr-spec
|
||||||
|
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>>
|
||||||
|
{
|
||||||
|
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
|
||||||
|
.padded()
|
||||||
|
.then_ignore(end())
|
||||||
|
}
|
||||||
|
|
||||||
|
// name-addr = [display-name] angle-addr
|
||||||
|
fn name_addr() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> {
|
||||||
|
display_name().collect().or_not().then(angle_addr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
|
||||||
|
fn angle_addr() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||||
|
addr_spec()
|
||||||
|
.delimited_by(just('<').ignored(), just('>').ignored())
|
||||||
|
.padded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// display-name = phrase
|
||||||
|
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
phrase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
|
||||||
|
pub(crate) fn mailbox_list(
|
||||||
|
) -> impl Parser<char, Vec<(Option<String>, (String, String))>, Error = Cheap<char>> {
|
||||||
|
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
|
||||||
|
.separated_by(just(',').padded())
|
||||||
|
.then_ignore(end())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.4.1. Addr-spec specification
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
|
||||||
|
|
||||||
|
// addr-spec = local-part "@" domain
|
||||||
|
pub(super) fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||||
|
local_part()
|
||||||
|
.collect()
|
||||||
|
.then_ignore(just('@'))
|
||||||
|
.then(domain().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
// local-part = dot-atom / quoted-string / obs-local-part
|
||||||
|
pub(super) fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
choice((dot_atom(), quoted_string(), obs_local_part()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// domain = dot-atom / domain-literal / obs-domain
|
||||||
|
pub(super) fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
// NOTE: omitting domain-literal since it may never be used
|
||||||
|
choice((dot_atom(), obs_domain()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.1. Miscellaneous obsolete tokens
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.1
|
||||||
|
|
||||||
|
// obs-phrase = word *(word / "." / CFWS)
|
||||||
|
fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
// NOTE: the CFWS is already captured by the word, no need to add
|
||||||
|
// it there.
|
||||||
|
word().chain(
|
||||||
|
choice((word(), just('.').repeated().exactly(1)))
|
||||||
|
.repeated()
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.4. Obsolete Addressing
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
|
||||||
|
|
||||||
|
// obs-local-part = word *("." word)
|
||||||
|
pub(super) fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
word().chain(just('.').chain(word()).repeated().flatten())
|
||||||
|
}
|
||||||
|
|
||||||
|
// obs-domain = atom *("." atom)
|
||||||
|
pub(super) fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||||
|
atom().chain(just('.').chain(atom()).repeated().flatten())
|
||||||
|
}
|
||||||
17
src/message/mailbox/parsers/rfc5336.rs
Normal file
17
src/message/mailbox/parsers/rfc5336.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//! Partial parsers implementation of [RFC5336]: SMTP Extension for
|
||||||
|
//! Internationalized Email Addresses.
|
||||||
|
//!
|
||||||
|
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
|
||||||
|
|
||||||
|
use chumsky::{error::Cheap, prelude::*};
|
||||||
|
|
||||||
|
// 3.3. Extended Mailbox Address Syntax
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
|
||||||
|
|
||||||
|
// UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
|
||||||
|
// UTF8-2 = <See Section 4 of RFC 3629>
|
||||||
|
// UTF8-3 = <See Section 4 of RFC 3629>
|
||||||
|
// UTF8-4 = <See Section 4 of RFC 3629>
|
||||||
|
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||||
|
filter(|c: &char| c.len_utf8() > 1)
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ impl<'de> Deserialize<'de> for Mailbox {
|
|||||||
{
|
{
|
||||||
struct FieldVisitor;
|
struct FieldVisitor;
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for FieldVisitor {
|
impl Visitor<'_> for FieldVisitor {
|
||||||
type Value = Field;
|
type Value = Field;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||||
@@ -179,7 +179,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_mailbox_object_address_stirng() {
|
fn parse_mailbox_object_address_string() {
|
||||||
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
|
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
|
||||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||||
}
|
}
|
||||||
@@ -198,7 +198,7 @@ mod test {
|
|||||||
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
|
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
m,
|
m,
|
||||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
@@ -211,7 +211,7 @@ mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
m,
|
m,
|
||||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use email_encoding::headers::EmailWriter;
|
use chumsky::prelude::*;
|
||||||
|
use email_encoding::headers::writer::EmailWriter;
|
||||||
|
|
||||||
|
use super::parsers;
|
||||||
use crate::address::{Address, AddressError};
|
use crate::address::{Address, AddressError};
|
||||||
|
|
||||||
/// Represents an email address with an optional name for the sender/recipient.
|
/// Represents an email address with an optional name for the sender/recipient.
|
||||||
///
|
///
|
||||||
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
|
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
|
||||||
///
|
///
|
||||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
@@ -70,7 +72,7 @@ impl Mailbox {
|
|||||||
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
||||||
if let Some(name) = &self.name {
|
if let Some(name) = &self.name {
|
||||||
email_encoding::headers::quoted_string::encode(name, w)?;
|
email_encoding::headers::quoted_string::encode(name, w)?;
|
||||||
w.optional_breakpoint();
|
w.space();
|
||||||
w.write_char('<')?;
|
w.write_char('<')?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ impl Mailbox {
|
|||||||
|
|
||||||
impl Display for Mailbox {
|
impl Display for Mailbox {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
if let Some(ref name) = self.name {
|
if let Some(name) = &self.name {
|
||||||
let name = name.trim();
|
let name = name.trim();
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
write_word(f, name)?;
|
write_word(f, name)?;
|
||||||
@@ -108,40 +110,24 @@ impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
|
|
||||||
type Error = AddressError;
|
|
||||||
|
|
||||||
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
|
|
||||||
let (name, address) = header;
|
|
||||||
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
impl FromStr for Mailbox {
|
impl FromStr for Mailbox {
|
||||||
type Err = AddressError;
|
type Err = AddressError;
|
||||||
|
|
||||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||||
match (src.find('<'), src.find('>')) {
|
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
|
||||||
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => {
|
// TODO: improve error management
|
||||||
let name = src.split_at(addr_open).0;
|
AddressError::InvalidInput
|
||||||
let addr_open = addr_open + 1;
|
})?;
|
||||||
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
|
|
||||||
let addr = addr.parse()?;
|
let mailbox = Mailbox::new(name, Address::new(user, domain)?);
|
||||||
let name = name.trim();
|
|
||||||
let name = if name.is_empty() {
|
Ok(mailbox)
|
||||||
None
|
}
|
||||||
} else {
|
}
|
||||||
Some(name.into())
|
|
||||||
};
|
impl From<Address> for Mailbox {
|
||||||
Ok(Mailbox::new(name, addr))
|
fn from(value: Address) -> Self {
|
||||||
}
|
Self::new(None, value)
|
||||||
(Some(_), _) => Err(AddressError::Unbalanced),
|
|
||||||
_ => {
|
|
||||||
let addr = src.parse()?;
|
|
||||||
Ok(Mailbox::new(None, addr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +135,7 @@ impl FromStr for Mailbox {
|
|||||||
///
|
///
|
||||||
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
|
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
|
||||||
///
|
///
|
||||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
|
||||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||||
pub struct Mailboxes(Vec<Mailbox>);
|
pub struct Mailboxes(Vec<Mailbox>);
|
||||||
|
|
||||||
@@ -188,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
|
||||||
///
|
///
|
||||||
@@ -275,7 +261,7 @@ impl Mailboxes {
|
|||||||
for mailbox in self.iter() {
|
for mailbox in self.iter() {
|
||||||
if !mem::take(&mut first) {
|
if !mem::take(&mut first) {
|
||||||
w.write_char(',')?;
|
w.write_char(',')?;
|
||||||
w.optional_breakpoint();
|
w.space();
|
||||||
}
|
}
|
||||||
|
|
||||||
mailbox.encode(w)?;
|
mailbox.encode(w)?;
|
||||||
@@ -356,34 +342,16 @@ impl Display for Mailboxes {
|
|||||||
impl FromStr for Mailboxes {
|
impl FromStr for Mailboxes {
|
||||||
type Err = AddressError;
|
type Err = AddressError;
|
||||||
|
|
||||||
fn from_str(mut src: &str) -> Result<Self, Self::Err> {
|
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||||
let mut mailboxes = Vec::new();
|
let mut mailboxes = Vec::new();
|
||||||
|
|
||||||
if !src.is_empty() {
|
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
|
||||||
// n-1 elements
|
// TODO: improve error management
|
||||||
let mut skip = 0;
|
AddressError::InvalidInput
|
||||||
while let Some(i) = src[skip..].find(',') {
|
})?;
|
||||||
let left = &src[..skip + i];
|
|
||||||
|
|
||||||
match left.trim().parse() {
|
for (name, (user, domain)) in parsed_mailboxes {
|
||||||
Ok(mailbox) => {
|
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?));
|
||||||
mailboxes.push(mailbox);
|
|
||||||
|
|
||||||
src = &src[left.len() + ",".len()..];
|
|
||||||
skip = 0;
|
|
||||||
}
|
|
||||||
Err(AddressError::MissingParts) => {
|
|
||||||
skip = left.len() + ",".len();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// last element
|
|
||||||
let mailbox = src.trim().parse()?;
|
|
||||||
mailboxes.push(mailbox);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Mailboxes(mailboxes))
|
Ok(Mailboxes(mailboxes))
|
||||||
@@ -436,7 +404,7 @@ fn is_valid_atom_char(c: u8) -> bool {
|
|||||||
b'}' |
|
b'}' |
|
||||||
b'~' |
|
b'~' |
|
||||||
|
|
||||||
// Not techically allowed but will be escaped into allowed characters.
|
// Not technically allowed but will be escaped into allowed characters.
|
||||||
128..=255)
|
128..=255)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,8 +444,6 @@ fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::convert::TryInto;
|
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::Mailbox;
|
use super::Mailbox;
|
||||||
@@ -565,7 +531,7 @@ mod test {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
|
Mailbox::new(Some("".to_owned()), "kayo@example.com".parse().unwrap())
|
||||||
),
|
),
|
||||||
"kayo@example.com"
|
"kayo@example.com"
|
||||||
);
|
);
|
||||||
@@ -590,6 +556,14 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_address_only_trim() {
|
||||||
|
assert_eq!(
|
||||||
|
" kayo@example.com ".parse(),
|
||||||
|
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_address_with_name() {
|
fn parse_address_with_name() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -601,6 +575,17 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_address_with_name_trim() {
|
||||||
|
assert_eq!(
|
||||||
|
" K. <kayo@example.com> ".parse(),
|
||||||
|
Ok(Mailbox::new(
|
||||||
|
Some("K.".into()),
|
||||||
|
"kayo@example.com".parse().unwrap()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_address_with_empty_name() {
|
fn parse_address_with_empty_name() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -612,7 +597,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parse_address_with_empty_name_trim() {
|
fn parse_address_with_empty_name_trim() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
" <kayo@example.com>".parse(),
|
" <kayo@example.com> ".parse(),
|
||||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ pub(super) enum Part {
|
|||||||
Multi(MultiPart),
|
Multi(MultiPart),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Part {
|
||||||
|
#[cfg(feature = "dkim")]
|
||||||
|
pub(super) fn format_body(&self, out: &mut Vec<u8>) {
|
||||||
|
match self {
|
||||||
|
Part::Single(part) => part.format_body(out),
|
||||||
|
Part::Multi(part) => part.format_body(out),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EmailFormat for Part {
|
impl EmailFormat for Part {
|
||||||
fn format(&self, out: &mut Vec<u8>) {
|
fn format(&self, out: &mut Vec<u8>) {
|
||||||
match self {
|
match self {
|
||||||
@@ -100,14 +110,14 @@ impl SinglePart {
|
|||||||
SinglePartBuilder::new()
|
SinglePartBuilder::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Directly create a `SinglePart` from an plain UTF-8 content
|
/// Directly create a `SinglePart` from a plain UTF-8 content
|
||||||
pub fn plain<T: IntoBody>(body: T) -> Self {
|
pub fn plain<T: IntoBody>(body: T) -> Self {
|
||||||
Self::builder()
|
Self::builder()
|
||||||
.header(header::ContentType::TEXT_PLAIN)
|
.header(header::ContentType::TEXT_PLAIN)
|
||||||
.body(body)
|
.body(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Directly create a `SinglePart` from an UTF-8 HTML content
|
/// Directly create a `SinglePart` from a UTF-8 HTML content
|
||||||
pub fn html<T: IntoBody>(body: T) -> Self {
|
pub fn html<T: IntoBody>(body: T) -> Self {
|
||||||
Self::builder()
|
Self::builder()
|
||||||
.header(header::ContentType::TEXT_HTML)
|
.header(header::ContentType::TEXT_HTML)
|
||||||
@@ -132,6 +142,12 @@ impl SinglePart {
|
|||||||
self.format(&mut out);
|
self.format(&mut out);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format only the signlepart body
|
||||||
|
fn format_body(&self, out: &mut Vec<u8>) {
|
||||||
|
out.extend_from_slice(&self.body);
|
||||||
|
out.extend_from_slice(b"\r\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmailFormat for SinglePart {
|
impl EmailFormat for SinglePart {
|
||||||
@@ -139,8 +155,7 @@ impl EmailFormat for SinglePart {
|
|||||||
write!(out, "{}", self.headers)
|
write!(out, "{}", self.headers)
|
||||||
.expect("A Write implementation panicked while formatting headers");
|
.expect("A Write implementation panicked while formatting headers");
|
||||||
out.extend_from_slice(b"\r\n");
|
out.extend_from_slice(b"\r\n");
|
||||||
out.extend_from_slice(&self.body);
|
self.format_body(out);
|
||||||
out.extend_from_slice(b"\r\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,17 +164,17 @@ impl EmailFormat for SinglePart {
|
|||||||
pub enum MultiPartKind {
|
pub enum MultiPartKind {
|
||||||
/// Mixed kind to combine unrelated content parts
|
/// Mixed kind to combine unrelated content parts
|
||||||
///
|
///
|
||||||
/// For example this kind can be used to mix email message and attachments.
|
/// For example, this kind can be used to mix an email message and attachments.
|
||||||
Mixed,
|
Mixed,
|
||||||
|
|
||||||
/// Alternative kind to join several variants of same email contents.
|
/// Alternative kind to join several variants of same email contents.
|
||||||
///
|
///
|
||||||
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message.
|
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into a single email message.
|
||||||
Alternative,
|
Alternative,
|
||||||
|
|
||||||
/// Related kind to mix content and related resources.
|
/// Related kind to mix content and related resources.
|
||||||
///
|
///
|
||||||
/// For example, you can include images into HTML content using that.
|
/// For example, you can include images in HTML content using that.
|
||||||
Related,
|
Related,
|
||||||
|
|
||||||
/// Encrypted kind for encrypted messages
|
/// Encrypted kind for encrypted messages
|
||||||
@@ -373,14 +388,9 @@ impl MultiPart {
|
|||||||
self.format(&mut out);
|
self.format(&mut out);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl EmailFormat for MultiPart {
|
|
||||||
fn format(&self, out: &mut Vec<u8>) {
|
|
||||||
write!(out, "{}", self.headers)
|
|
||||||
.expect("A Write implementation panicked while formatting headers");
|
|
||||||
out.extend_from_slice(b"\r\n");
|
|
||||||
|
|
||||||
|
/// Format only the multipart body
|
||||||
|
fn format_body(&self, out: &mut Vec<u8>) {
|
||||||
let boundary = self.boundary();
|
let boundary = self.boundary();
|
||||||
|
|
||||||
for part in &self.parts {
|
for part in &self.parts {
|
||||||
@@ -396,12 +406,20 @@ impl EmailFormat for MultiPart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EmailFormat for MultiPart {
|
||||||
|
fn format(&self, out: &mut Vec<u8>) {
|
||||||
|
write!(out, "{}", self.headers)
|
||||||
|
.expect("A Write implementation panicked while formatting headers");
|
||||||
|
out.extend_from_slice(b"\r\n");
|
||||||
|
self.format_body(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
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() {
|
||||||
|
|||||||
@@ -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(SystemTime::now())
|
self.date(crate::time::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();
|
||||||
|
|
||||||
@@ -388,13 +388,13 @@ impl MessageBuilder {
|
|||||||
|
|
||||||
/// Keep the `Bcc` header
|
/// Keep the `Bcc` header
|
||||||
///
|
///
|
||||||
/// By default the `Bcc` header is removed from the email after
|
/// By default, the `Bcc` header is removed from the email after
|
||||||
/// using it to generate the message envelope. In some cases though,
|
/// using it to generate the message envelope. In some cases though,
|
||||||
/// like when saving the email as an `.eml`, or sending through
|
/// like when saving the email as an `.eml`, or sending through
|
||||||
/// some transports (like the Gmail API) that don't take a separate
|
/// some transports (like the Gmail API) that don't take a separate
|
||||||
/// envelope value, it becomes necessary to keep the `Bcc` header.
|
/// envelope value, it becomes necessary to keep the `Bcc` header.
|
||||||
///
|
///
|
||||||
/// Calling this method overrides the default behaviour.
|
/// Calling this method overrides the default behavior.
|
||||||
pub fn keep_bcc(mut self) -> Self {
|
pub fn keep_bcc(mut self) -> Self {
|
||||||
self.drop_bcc = false;
|
self.drop_bcc = false;
|
||||||
self
|
self
|
||||||
@@ -457,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`][self::MultiPart])
|
/// Create message using mime body ([`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`][self::SinglePart])
|
/// Create message using mime body ([`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)))
|
||||||
}
|
}
|
||||||
@@ -503,6 +503,11 @@ impl Message {
|
|||||||
&self.headers
|
&self.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a mutable reference to the headers
|
||||||
|
pub fn headers_mut(&mut self) -> &mut Headers {
|
||||||
|
&mut self.headers
|
||||||
|
}
|
||||||
|
|
||||||
/// Get `Message` envelope
|
/// Get `Message` envelope
|
||||||
pub fn envelope(&self) -> &Envelope {
|
pub fn envelope(&self) -> &Envelope {
|
||||||
&self.envelope
|
&self.envelope
|
||||||
@@ -520,9 +525,9 @@ impl Message {
|
|||||||
pub(crate) fn body_raw(&self) -> Vec<u8> {
|
pub(crate) fn body_raw(&self) -> Vec<u8> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
match &self.body {
|
match &self.body {
|
||||||
MessageBody::Mime(p) => p.format(&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
|
||||||
}
|
}
|
||||||
@@ -532,7 +537,10 @@ impl Message {
|
|||||||
/// Example:
|
/// Example:
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use lettre::{
|
/// use lettre::{
|
||||||
/// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
|
/// message::{
|
||||||
|
/// dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
|
||||||
|
/// header::ContentType,
|
||||||
|
/// },
|
||||||
/// Message,
|
/// Message,
|
||||||
/// };
|
/// };
|
||||||
///
|
///
|
||||||
@@ -541,6 +549,7 @@ 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-----
|
||||||
@@ -596,7 +605,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -630,7 +639,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn email_miminal_message() {
|
fn email_minimal_message() {
|
||||||
assert!(Message::builder()
|
assert!(Message::builder()
|
||||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
.to("NoBody <nobody@domain.tld>".parse().unwrap())
|
.to("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
@@ -760,7 +769,7 @@ mod test {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(line.0, line.1)
|
assert_eq!(line.0, line.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
14
src/rustls_crypto.rs
Normal file
14
src/rustls_crypto.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use rustls::crypto::CryptoProvider;
|
||||||
|
|
||||||
|
pub(crate) fn crypto_provider() -> Arc<CryptoProvider> {
|
||||||
|
CryptoProvider::get_default().cloned().unwrap_or_else(|| {
|
||||||
|
#[cfg(feature = "aws-lc-rs")]
|
||||||
|
let provider = rustls::crypto::aws_lc_rs::default_provider();
|
||||||
|
#[cfg(not(feature = "aws-lc-rs"))]
|
||||||
|
let provider = rustls::crypto::ring::default_provider();
|
||||||
|
|
||||||
|
Arc::new(provider)
|
||||||
|
})
|
||||||
|
}
|
||||||
26
src/time.rs
Normal file
26
src/time.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
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(feature = "web"))]
|
||||||
|
pub(crate) fn now() -> SystemTime {
|
||||||
|
// FIXME: change to #[expect(clippy::disallowed_methods, reason = "the `web` feature is disabled")]
|
||||||
|
#[allow(clippy::disallowed_methods)]
|
||||||
|
SystemTime::now()
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ impl fmt::Debug for Error {
|
|||||||
|
|
||||||
builder.field("kind", &self.inner.kind);
|
builder.field("kind", &self.inner.kind);
|
||||||
|
|
||||||
if let Some(ref source) = self.inner.source {
|
if let Some(source) = &self.inner.source {
|
||||||
builder.field("source", source);
|
builder.field("source", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +68,9 @@ 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(ref e) = self.inner.source {
|
if let Some(e) = &self.inner.source {
|
||||||
write!(f, ": {e}")?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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::{FileTransport, Message, Transport};
|
//! use lettre::{message::header::ContentType, 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!"))?;
|
||||||
//!
|
//!
|
||||||
//! let result = sender.send(&email);
|
//! 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::{FileTransport, Message, Transport};
|
//! use lettre::{message::header::ContentType, 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!"))?;
|
||||||
//!
|
//!
|
||||||
//! let result = sender.send(&email);
|
//! sender.send(&email)?;
|
||||||
//! assert!(result.is_ok());
|
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//!
|
//!
|
||||||
@@ -73,7 +73,9 @@
|
|||||||
//! # 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::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
//! use lettre::{
|
||||||
|
//! 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());
|
||||||
@@ -82,10 +84,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!"))?;
|
||||||
//!
|
//!
|
||||||
//! let result = sender.send(email).await;
|
//! sender.send(email).await?;
|
||||||
//! assert!(result.is_ok());
|
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
@@ -99,7 +101,10 @@
|
|||||||
//! # 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::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
//! use lettre::{
|
||||||
|
//! 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());
|
||||||
@@ -108,10 +113,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!"))?;
|
||||||
//!
|
//!
|
||||||
//! let result = sender.send(email).await;
|
//! sender.send(email).await?;
|
||||||
//! assert!(result.is_ok());
|
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
@@ -125,6 +130,7 @@
|
|||||||
//! 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!
|
||||||
@@ -157,7 +163,7 @@ mod error;
|
|||||||
type Id = String;
|
type Id = String;
|
||||||
|
|
||||||
/// Writes the content and the envelope information to a file
|
/// 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(feature = "file-transport")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||||
pub struct FileTransport {
|
pub struct FileTransport {
|
||||||
@@ -167,7 +173,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"))]
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
|
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
|
||||||
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
|
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
|
||||||
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
|
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
|
||||||
//! | [`stub`] | Debug | [`StubTransport`] | [`StubTransport`] | Drops the email - Useful for debugging |
|
//! | [`stub`] | Debug | [`StubTransport`] | [`AsyncStubTransport`] | Drops the email - Useful for debugging |
|
||||||
//!
|
//!
|
||||||
//! ## Building an email
|
//! ## Building an email
|
||||||
//!
|
//!
|
||||||
@@ -56,13 +56,17 @@
|
|||||||
//! #
|
//! #
|
||||||
//! # #[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::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
//! use lettre::{
|
||||||
|
//! 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());
|
||||||
@@ -75,7 +79,7 @@
|
|||||||
//! // 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:?}"),
|
||||||
//! }
|
//! }
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
@@ -97,6 +101,7 @@
|
|||||||
//! [`FileTransport`]: crate::FileTransport
|
//! [`FileTransport`]: crate::FileTransport
|
||||||
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||||
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
||||||
|
//! [`AsyncStubTransport`]: crate::transport::stub::AsyncStubTransport
|
||||||
|
|
||||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -135,6 +140,10 @@ 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
|
||||||
@@ -161,4 +170,8 @@ 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) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ impl fmt::Debug for Error {
|
|||||||
|
|
||||||
builder.field("kind", &self.inner.kind);
|
builder.field("kind", &self.inner.kind);
|
||||||
|
|
||||||
if let Some(ref source) = self.inner.source {
|
if let Some(source) = &self.inner.source {
|
||||||
builder.field("source", source);
|
builder.field("source", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +65,9 @@ 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(ref e) = self.inner.source {
|
if let Some(e) = &self.inner.source {
|
||||||
write!(f, ": {e}")?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, SendmailTransport, Transport};
|
//! use lettre::{message::header::ContentType, 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();
|
||||||
//! let result = sender.send(&email);
|
//! sender.send(&email)?;
|
||||||
//! assert!(result.is_ok());
|
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//!
|
//!
|
||||||
@@ -34,7 +34,8 @@
|
|||||||
//! # #[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::{
|
||||||
//! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
|
//! message::header::ContentType, AsyncSendmailTransport, AsyncTransport, Message,
|
||||||
|
//! SendmailTransport, Tokio1Executor,
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
//! let email = Message::builder()
|
//! let email = Message::builder()
|
||||||
@@ -42,11 +43,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();
|
||||||
//! let result = sender.send(email).await;
|
//! sender.send(email).await?;
|
||||||
//! assert!(result.is_ok());
|
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
@@ -58,18 +59,17 @@
|
|||||||
//! #
|
//! #
|
||||||
//! # #[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, AsyncSendmailTransport};
|
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor,message::header::ContentType, 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")
|
//! .subject("Happy new year").header(ContentType::TEXT_PLAIN)
|
||||||
//! .body(String::from("Be happy!"))?;
|
//! .body(String::from("Be happy!"))?;
|
||||||
//!
|
//!
|
||||||
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
|
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
|
||||||
//! let result = sender.send(email).await;
|
//! 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(),
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ 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,
|
||||||
};
|
};
|
||||||
@@ -24,6 +30,30 @@ 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")]
|
||||||
@@ -45,10 +75,15 @@ 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.quit().await?;
|
conn.abort().await;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn shutdown(&self) {
|
||||||
|
#[cfg(feature = "pool")]
|
||||||
|
self.inner.shutdown().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
@@ -67,6 +102,11 @@ 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>
|
||||||
@@ -81,16 +121,15 @@ where
|
|||||||
/// to validate TLS certificates.
|
/// to validate TLS certificates.
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "tokio1-native-tls",
|
feature = "tokio1-native-tls",
|
||||||
feature = "tokio1-rustls-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "async-std1-native-tls",
|
feature = "async-std1-rustls"
|
||||||
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-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "async-std1-rustls-tls"
|
feature = "async-std1-rustls"
|
||||||
)))
|
)))
|
||||||
)]
|
)]
|
||||||
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||||
@@ -103,7 +142,7 @@ where
|
|||||||
.tls(Tls::Wrapper(tls_parameters)))
|
.tls(Tls::Wrapper(tls_parameters)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
|
/// Simple and secure transport, using STARTTLS to obtain encrypted connections
|
||||||
///
|
///
|
||||||
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
|
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
|
||||||
/// that don't take SMTPS connections.
|
/// that don't take SMTPS connections.
|
||||||
@@ -116,16 +155,15 @@ 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-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "async-std1-native-tls",
|
feature = "async-std1-rustls"
|
||||||
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-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "async-std1-rustls-tls"
|
feature = "async-std1-rustls"
|
||||||
)))
|
)))
|
||||||
)]
|
)]
|
||||||
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||||
@@ -151,28 +189,128 @@ where
|
|||||||
///
|
///
|
||||||
/// * No authentication
|
/// * No authentication
|
||||||
/// * No TLS
|
/// * No TLS
|
||||||
/// * A 60 seconds timeout for smtp commands
|
/// * A 60-seconds timeout for smtp commands
|
||||||
/// * Port 25
|
/// * Port 25
|
||||||
///
|
///
|
||||||
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
|
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
|
||||||
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||||
/// if possible.
|
/// if possible.
|
||||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
||||||
let info = SmtpInfo {
|
AsyncSmtpTransportBuilder::new(server)
|
||||||
server: server.into(),
|
}
|
||||||
..Default::default()
|
|
||||||
};
|
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL
|
||||||
AsyncSmtpTransportBuilder {
|
///
|
||||||
info,
|
/// The protocol, credentials, host, port and EHLO name can be provided
|
||||||
#[cfg(feature = "pool")]
|
/// in a single URL. This may be simpler than having to configure SMTP
|
||||||
pool_config: PoolConfig::default(),
|
/// through multiple configuration parameters and then having to pass
|
||||||
}
|
/// those options to lettre.
|
||||||
|
///
|
||||||
|
/// The URL is created in the following way:
|
||||||
|
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
|
||||||
|
///
|
||||||
|
/// `user` (Username) and `pass` (Password) are optional in case the
|
||||||
|
/// SMTP relay doesn't require authentication. When `port` is not
|
||||||
|
/// configured it is automatically determined based on the `scheme`.
|
||||||
|
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
|
||||||
|
/// command. `TLS` controls whether STARTTLS is simply enabled
|
||||||
|
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
|
||||||
|
/// or `required` (require the server to upgrade the connection to
|
||||||
|
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
|
||||||
|
///
|
||||||
|
/// Use the following table to construct your SMTP url:
|
||||||
|
///
|
||||||
|
/// | scheme | `tls` query parameter | example | default port | remarks |
|
||||||
|
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
|
||||||
|
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
|
||||||
|
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
|
||||||
|
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
|
||||||
|
///
|
||||||
|
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
|
||||||
|
/// be concatenated to construct the final URL because special characters
|
||||||
|
/// contained within the parameter may confuse the URL decoder.
|
||||||
|
/// Manually URL encode the parameters before concatenating them or use
|
||||||
|
/// a proper URL encoder, like the following cargo script:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # let _ = r#"
|
||||||
|
/// #!/usr/bin/env cargo
|
||||||
|
///
|
||||||
|
/// //! ```cargo
|
||||||
|
/// //! [dependencies]
|
||||||
|
/// //! url = "2"
|
||||||
|
/// //! ```
|
||||||
|
/// # "#;
|
||||||
|
///
|
||||||
|
/// use url::Url;
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// // don't touch this line
|
||||||
|
/// let mut url = Url::parse("foo://bar").unwrap();
|
||||||
|
///
|
||||||
|
/// // configure the scheme (`smtp` or `smtps`) here.
|
||||||
|
/// url.set_scheme("smtps").unwrap();
|
||||||
|
/// // configure the username and password.
|
||||||
|
/// // remove the following two lines if unauthenticated.
|
||||||
|
/// url.set_username("username").unwrap();
|
||||||
|
/// url.set_password(Some("password")).unwrap();
|
||||||
|
/// // configure the hostname
|
||||||
|
/// url.set_host(Some("smtp.example.com")).unwrap();
|
||||||
|
/// // configure the port - only necessary if using a non-default port
|
||||||
|
/// url.set_port(Some(465)).unwrap();
|
||||||
|
/// // configure the EHLO name
|
||||||
|
/// url.set_path("ehlo-name");
|
||||||
|
///
|
||||||
|
/// println!("{url}");
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The connection URL can then be used in the following way:
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use lettre::{
|
||||||
|
/// message::header::ContentType, transport::smtp::authentication::Credentials,
|
||||||
|
/// AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
||||||
|
/// };
|
||||||
|
/// # use tokio1_crate as tokio;
|
||||||
|
///
|
||||||
|
/// # #[tokio::main]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let email = Message::builder()
|
||||||
|
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
|
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
|
/// .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
|
/// .subject("Happy new year")
|
||||||
|
/// .header(ContentType::TEXT_PLAIN)
|
||||||
|
/// .body(String::from("Be happy!"))
|
||||||
|
/// .unwrap();
|
||||||
|
///
|
||||||
|
/// // Open a remote connection to gmail
|
||||||
|
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||||
|
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
|
||||||
|
/// "smtps://username:password@smtp.example.com:465",
|
||||||
|
/// )?
|
||||||
|
/// .build();
|
||||||
|
///
|
||||||
|
/// // Send the email
|
||||||
|
/// mailer.send(email).await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[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 from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||||
|
super::connection_url::from_connection_url(connection_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tests the SMTP connection
|
/// Tests the SMTP connection
|
||||||
///
|
///
|
||||||
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
||||||
/// The connection is closed afterwards if a connection pool is not used.
|
/// The connection is closed afterward if a connection pool is not used.
|
||||||
pub async fn test_connection(&self) -> Result<bool, Error> {
|
pub async fn test_connection(&self) -> Result<bool, Error> {
|
||||||
let mut conn = self.inner.connection().await?;
|
let mut conn = self.inner.connection().await?;
|
||||||
|
|
||||||
@@ -219,13 +357,27 @@ pub struct AsyncSmtpTransportBuilder {
|
|||||||
|
|
||||||
/// Builder for the SMTP `AsyncSmtpTransport`
|
/// Builder for the SMTP `AsyncSmtpTransport`
|
||||||
impl AsyncSmtpTransportBuilder {
|
impl AsyncSmtpTransportBuilder {
|
||||||
|
// Create new builder with default parameters
|
||||||
|
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
|
||||||
|
let info = SmtpInfo {
|
||||||
|
server: server.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
AsyncSmtpTransportBuilder {
|
||||||
|
info,
|
||||||
|
#[cfg(feature = "pool")]
|
||||||
|
pool_config: PoolConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the name used during EHLO
|
/// Set the name used during EHLO
|
||||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||||
self.info.hello_name = name;
|
self.info.hello_name = name;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the authentication mechanism to use
|
/// Set the authentication credentials 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
|
||||||
@@ -238,6 +390,17 @@ 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
|
||||||
@@ -250,21 +413,31 @@ 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-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "async-std1-native-tls",
|
feature = "async-std1-rustls"
|
||||||
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-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "async-std1-rustls-tls"
|
feature = "async-std1-rustls"
|
||||||
)))
|
)))
|
||||||
)]
|
)]
|
||||||
pub fn tls(mut self, tls: super::Tls) -> Self {
|
pub fn tls(mut self, tls: Tls) -> Self {
|
||||||
self.info.tls = tls;
|
self.info.tls = tls;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -297,7 +470,7 @@ impl AsyncSmtpTransportBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build client
|
/// Build client
|
||||||
pub struct AsyncSmtpClient<E> {
|
pub(super) struct AsyncSmtpClient<E> {
|
||||||
info: SmtpInfo,
|
info: SmtpInfo,
|
||||||
marker_: PhantomData<E>,
|
marker_: PhantomData<E>,
|
||||||
}
|
}
|
||||||
@@ -309,7 +482,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 async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
|
pub(super) 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,
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ pub struct Credentials {
|
|||||||
|
|
||||||
impl Credentials {
|
impl Credentials {
|
||||||
/// Create a `Credentials` struct from username and password
|
/// Create a `Credentials` struct from username and password
|
||||||
|
///
|
||||||
|
/// When using [`Mechanism::Xoauth2`], `password` is the raw
|
||||||
|
/// bearer access token.
|
||||||
pub fn new(username: String, password: String) -> Credentials {
|
pub fn new(username: String, password: String) -> Credentials {
|
||||||
Credentials {
|
Credentials {
|
||||||
authentication_identity: username,
|
authentication_identity: username,
|
||||||
@@ -51,7 +54,7 @@ pub enum Mechanism {
|
|||||||
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
|
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
|
||||||
Plain,
|
Plain,
|
||||||
/// LOGIN authentication mechanism
|
/// LOGIN authentication mechanism
|
||||||
/// Obsolete but needed for some providers (like office365)
|
/// Obsolete but needed for some providers (like Office 365)
|
||||||
///
|
///
|
||||||
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
|
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
|
||||||
Login,
|
Login,
|
||||||
@@ -71,7 +74,7 @@ impl Display for Mechanism {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Mechanism {
|
impl Mechanism {
|
||||||
/// Does the mechanism supports initial response
|
/// Does the mechanism support initial response?
|
||||||
pub fn supports_initial_response(self) -> bool {
|
pub fn supports_initial_response(self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Mechanism::Plain | Mechanism::Xoauth2 => true,
|
Mechanism::Plain | Mechanism::Xoauth2 => true,
|
||||||
@@ -98,11 +101,17 @@ 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 ["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
if contains_ignore_ascii_case(
|
||||||
|
decoded_challenge,
|
||||||
|
["User Name", "Username:", "Username", "User Name\0"],
|
||||||
|
) {
|
||||||
return Ok(credentials.authentication_identity.clone());
|
return Ok(credentials.authentication_identity.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ["Password", "Password:"].contains(&decoded_challenge) {
|
if contains_ignore_ascii_case(
|
||||||
|
decoded_challenge,
|
||||||
|
["Password", "Password:", "Password\0"],
|
||||||
|
) {
|
||||||
return Ok(credentials.secret.clone());
|
return Ok(credentials.secret.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +128,15 @@ impl Mechanism {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn contains_ignore_ascii_case<'a>(
|
||||||
|
haystack: &str,
|
||||||
|
needles: impl IntoIterator<Item = &'a str>,
|
||||||
|
) -> bool {
|
||||||
|
needles
|
||||||
|
.into_iter()
|
||||||
|
.any(|item| item.eq_ignore_ascii_case(haystack))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{Credentials, Mechanism};
|
use super::{Credentials, Mechanism};
|
||||||
@@ -153,6 +171,23 @@ 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;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|||||||
use super::async_net::AsyncTokioStream;
|
use super::async_net::AsyncTokioStream;
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
use super::escape_crlf;
|
use super::escape_crlf;
|
||||||
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
#[allow(deprecated)]
|
||||||
|
use super::{async_net::AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||||
use crate::{
|
use crate::{
|
||||||
transport::smtp::{
|
transport::smtp::{
|
||||||
authentication::{Credentials, Mechanism},
|
authentication::{Credentials, Mechanism},
|
||||||
@@ -35,6 +36,7 @@ macro_rules! try_smtp (
|
|||||||
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 state
|
||||||
panic: bool,
|
panic: bool,
|
||||||
@@ -56,13 +58,41 @@ impl AsyncSmtpConnection {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connects to the configured server
|
/// Connects to the configured server
|
||||||
///
|
///
|
||||||
|
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
|
||||||
|
/// referred to as `SMTPS`). See also [`AsyncSmtpConnection::starttls`].
|
||||||
|
///
|
||||||
|
/// If `local_address` is `Some`, then the address provided shall be used to bind the
|
||||||
|
/// connection to a specific local address using [`tokio1_crate::net::TcpSocket::bind`].
|
||||||
|
///
|
||||||
/// Sends EHLO and parses server information
|
/// Sends EHLO and parses server information
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use std::time::Duration;
|
||||||
|
/// # use lettre::transport::smtp::{client::{AsyncSmtpConnection, TlsParameters}, extension::ClientId};
|
||||||
|
/// # use tokio1_crate::{self as tokio, net::ToSocketAddrs as _};
|
||||||
|
/// #
|
||||||
|
/// # #[tokio::main]
|
||||||
|
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let connection = AsyncSmtpConnection::connect_tokio1(
|
||||||
|
/// ("example.com", 465),
|
||||||
|
/// Some(Duration::from_secs(60)),
|
||||||
|
/// &ClientId::default(),
|
||||||
|
/// Some(TlsParameters::new("example.com".to_owned())?),
|
||||||
|
/// None,
|
||||||
|
/// )
|
||||||
|
/// .await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
#[cfg(feature = "tokio1")]
|
#[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,
|
||||||
@@ -71,6 +101,7 @@ 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?;
|
||||||
@@ -87,10 +118,12 @@ impl AsyncSmtpConnection {
|
|||||||
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,
|
||||||
@@ -132,7 +165,7 @@ impl AsyncSmtpConnection {
|
|||||||
mail_options.push(MailParameter::SmtpUtfEight);
|
mail_options.push(MailParameter::SmtpUtfEight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for non-ascii content in message
|
// Check for non-ascii content in the message
|
||||||
if !email.is_ascii() {
|
if !email.is_ascii() {
|
||||||
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||||
return Err(error::client(
|
return Err(error::client(
|
||||||
@@ -172,6 +205,12 @@ impl AsyncSmtpConnection {
|
|||||||
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upgrade the connection using `STARTTLS`.
|
||||||
|
///
|
||||||
|
/// As described in [rfc3207]. Note that this mechanism has been deprecated in [rfc8314].
|
||||||
|
///
|
||||||
|
/// [rfc3207]: https://www.rfc-editor.org/rfc/rfc3207
|
||||||
|
/// [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,
|
||||||
@@ -212,6 +251,7 @@ impl AsyncSmtpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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);
|
||||||
}
|
}
|
||||||
@@ -226,7 +266,7 @@ impl AsyncSmtpConnection {
|
|||||||
self.command(Noop).await.is_ok()
|
self.command(Noop).await.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
/// Sends an AUTH command with the given mechanism, and handles the challenge if needed
|
||||||
pub async fn auth(
|
pub async fn auth(
|
||||||
&mut self,
|
&mut self,
|
||||||
mechanisms: &[Mechanism],
|
mechanisms: &[Mechanism],
|
||||||
@@ -335,8 +375,30 @@ 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-tls", feature = "boring-tls"))]
|
#[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")]
|
||||||
|
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"))]
|
||||||
|
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||||
|
self.stream.get_ref().certificate_chain()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +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, ErrorKind,
|
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
|
||||||
Result as IoResult,
|
Result as IoResult,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||||
|
#[cfg(any(feature = "tokio1-rustls", feature = "async-std1-rustls"))]
|
||||||
|
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;
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
@@ -27,15 +27,14 @@ 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-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "tokio1-native-tls",
|
feature = "tokio1-native-tls",
|
||||||
feature = "tokio1-rustls-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "tokio1-boring-tls",
|
feature = "tokio1-boring-tls",
|
||||||
feature = "async-std1-native-tls",
|
feature = "async-std1-rustls"
|
||||||
feature = "async-std1-rustls-tls"
|
|
||||||
))]
|
))]
|
||||||
use super::InnerTlsParameters;
|
use super::InnerTlsParameters;
|
||||||
use super::TlsParameters;
|
use super::TlsParameters;
|
||||||
@@ -45,6 +44,10 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -75,7 +78,7 @@ 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-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
Tokio1RustlsTls(Tokio1RustlsTlsStream<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")]
|
||||||
@@ -84,15 +87,13 @@ enum InnerAsyncNetworkStream {
|
|||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
AsyncStd1Tcp(AsyncStd1TcpStream),
|
AsyncStd1Tcp(AsyncStd1TcpStream),
|
||||||
/// Encrypted Tokio 1.x TCP stream
|
/// Encrypted Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
AsyncStd1NativeTls(AsyncStd1TlsStream<AsyncStd1TcpStream>),
|
|
||||||
/// Encrypted Tokio 1.x TCP stream
|
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
|
||||||
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
|
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
|
||||||
/// Can't be built
|
/// Can't be built
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
impl AsyncNetworkStream {
|
impl AsyncNetworkStream {
|
||||||
fn new(inner: InnerAsyncNetworkStream) -> Self {
|
fn new(inner: InnerAsyncNetworkStream) -> Self {
|
||||||
if let InnerAsyncNetworkStream::None = inner {
|
if let InnerAsyncNetworkStream::None = inner {
|
||||||
@@ -104,23 +105,21 @@ impl AsyncNetworkStream {
|
|||||||
|
|
||||||
/// Returns peer's address
|
/// Returns peer's address
|
||||||
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
|
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
|
||||||
match self.inner {
|
match &self.inner {
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
|
InnerAsyncNetworkStream::Tokio1Tcp(s) => s.peer_addr(),
|
||||||
#[cfg(feature = "tokio1-native-tls")]
|
#[cfg(feature = "tokio1-native-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref 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-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref 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(ref s) => s.get_ref().peer_addr(),
|
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref s) => s.get_ref().peer_addr(),
|
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
|
||||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
|
||||||
InnerAsyncNetworkStream::None => {
|
InnerAsyncNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||||
Err(IoError::new(
|
Err(IoError::new(
|
||||||
@@ -176,7 +175,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 {
|
||||||
@@ -208,9 +207,9 @@ impl AsyncNetworkStream {
|
|||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
tls_parameters: Option<TlsParameters>,
|
tls_parameters: Option<TlsParameters>,
|
||||||
) -> Result<AsyncNetworkStream, Error> {
|
) -> Result<AsyncNetworkStream, Error> {
|
||||||
// Unfortunately there doesn't currently seem to be a way to set the local address
|
// Unfortunately, there doesn't currently seem to be a way to set the local address.
|
||||||
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
|
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
|
||||||
// connected which is a blocking operation.
|
// been connected, which is a blocking operation.
|
||||||
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
|
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
|
||||||
server: T,
|
server: T,
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
@@ -228,7 +227,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",
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,26 +258,25 @@ impl AsyncNetworkStream {
|
|||||||
feature = "tokio1",
|
feature = "tokio1",
|
||||||
not(any(
|
not(any(
|
||||||
feature = "tokio1-native-tls",
|
feature = "tokio1-native-tls",
|
||||||
feature = "tokio1-rustls-tls",
|
feature = "tokio1-rustls",
|
||||||
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-tls feature");
|
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls feature");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "tokio1-native-tls",
|
feature = "tokio1-native-tls",
|
||||||
feature = "tokio1-rustls-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "tokio1-boring-tls"
|
feature = "tokio1-boring-tls"
|
||||||
))]
|
))]
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||||
// get owned TcpStream
|
// get owned TcpStream
|
||||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||||
let tcp_stream = match tcp_stream {
|
let InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) = tcp_stream else {
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
|
unreachable!()
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
||||||
@@ -286,22 +284,18 @@ impl AsyncNetworkStream {
|
|||||||
.map_err(error::connection)?;
|
.map_err(error::connection)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(all(
|
#[cfg(all(feature = "async-std1", not(feature = "async-std1-rustls")))]
|
||||||
feature = "async-std1",
|
|
||||||
not(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))
|
|
||||||
))]
|
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||||
let _ = tls_parameters;
|
let _ = tls_parameters;
|
||||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the async-std1-native-tls or the async-std1-rustls-tls feature");
|
panic!("Trying to upgrade an AsyncNetworkStream without having enabled the async-std1-rustls feature");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||||
// get owned TcpStream
|
// get owned TcpStream
|
||||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||||
let tcp_stream = match tcp_stream {
|
let InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) = tcp_stream else {
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
|
unreachable!()
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
||||||
@@ -316,7 +310,7 @@ impl AsyncNetworkStream {
|
|||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "tokio1-native-tls",
|
feature = "tokio1-native-tls",
|
||||||
feature = "tokio1-rustls-tls",
|
feature = "tokio1-rustls",
|
||||||
feature = "tokio1-boring-tls"
|
feature = "tokio1-boring-tls"
|
||||||
))]
|
))]
|
||||||
async fn upgrade_tokio1_tls(
|
async fn upgrade_tokio1_tls(
|
||||||
@@ -343,14 +337,13 @@ impl AsyncNetworkStream {
|
|||||||
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
|
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerTlsParameters::RustlsTls(config) => {
|
InnerTlsParameters::RustlsTls(config) => {
|
||||||
#[cfg(not(feature = "tokio1-rustls-tls"))]
|
#[cfg(not(feature = "tokio1-rustls"))]
|
||||||
panic!("built without the tokio1-rustls-tls feature");
|
panic!("built without the tokio1-rustls feature");
|
||||||
|
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
return {
|
return {
|
||||||
use rustls::ServerName;
|
|
||||||
use tokio1_rustls::TlsConnector;
|
use tokio1_rustls::TlsConnector;
|
||||||
|
|
||||||
let domain = ServerName::try_from(domain.as_str())
|
let domain = ServerName::try_from(domain.as_str())
|
||||||
@@ -358,7 +351,7 @@ impl AsyncNetworkStream {
|
|||||||
|
|
||||||
let connector = TlsConnector::from(config);
|
let connector = TlsConnector::from(config);
|
||||||
let stream = connector
|
let stream = connector
|
||||||
.connect(domain, tcp_stream)
|
.connect(domain.to_owned(), tcp_stream)
|
||||||
.await
|
.await
|
||||||
.map_err(error::connection)?;
|
.map_err(error::connection)?;
|
||||||
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
||||||
@@ -384,11 +377,7 @@ impl AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[cfg(any(
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
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,
|
||||||
@@ -399,39 +388,22 @@ impl AsyncNetworkStream {
|
|||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerTlsParameters::NativeTls(connector) => {
|
InnerTlsParameters::NativeTls(connector) => {
|
||||||
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
|
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
|
||||||
|
|
||||||
/*
|
|
||||||
#[cfg(not(feature = "async-std1-native-tls"))]
|
|
||||||
panic!("built without the async-std1-native-tls feature");
|
|
||||||
|
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
|
||||||
return {
|
|
||||||
use async_native_tls::TlsConnector;
|
|
||||||
|
|
||||||
// TODO: fix
|
|
||||||
let connector: TlsConnector = todo!();
|
|
||||||
// let connector = TlsConnector::from(connector);
|
|
||||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
|
||||||
Ok(InnerAsyncNetworkStream::AsyncStd1NativeTls(stream))
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerTlsParameters::RustlsTls(config) => {
|
InnerTlsParameters::RustlsTls(config) => {
|
||||||
#[cfg(not(feature = "async-std1-rustls-tls"))]
|
#[cfg(not(feature = "async-std1-rustls"))]
|
||||||
panic!("built without the async-std1-rustls-tls feature");
|
panic!("built without the async-std1-rustls feature");
|
||||||
|
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
return {
|
return {
|
||||||
use futures_rustls::TlsConnector;
|
use futures_rustls::TlsConnector;
|
||||||
use rustls::ServerName;
|
|
||||||
|
|
||||||
let domain = ServerName::try_from(domain.as_str())
|
let domain = ServerName::try_from(domain.as_str())
|
||||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||||
|
|
||||||
let connector = TlsConnector::from(config);
|
let connector = TlsConnector::from(config);
|
||||||
let stream = connector
|
let stream = connector
|
||||||
.connect(domain, tcp_stream)
|
.connect(domain.to_owned(), tcp_stream)
|
||||||
.await
|
.await
|
||||||
.map_err(error::connection)?;
|
.map_err(error::connection)?;
|
||||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||||
@@ -445,25 +417,89 @@ impl AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_encrypted(&self) -> bool {
|
pub fn is_encrypted(&self) -> bool {
|
||||||
match self.inner {
|
match &self.inner {
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
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-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => 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-native-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
|
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
|
||||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
|
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
|
||||||
InnerAsyncNetworkStream::None => false,
|
InnerAsyncNetworkStream::None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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::Tokio1RustlsTls(_) => 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::AsyncStd1RustlsTls(_) => 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::Tokio1RustlsTls(stream) => Ok(stream
|
||||||
|
.get_ref()
|
||||||
|
.1
|
||||||
|
.peer_certificates()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.to_vec())
|
||||||
|
.collect()),
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
||||||
|
.ssl()
|
||||||
|
.peer_cert_chain()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.to_der().map_err(error::tls))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?),
|
||||||
|
#[cfg(feature = "async-std1")]
|
||||||
|
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||||
|
Err(error::client("Connection is not encrypted"))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
|
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
|
||||||
|
.get_ref()
|
||||||
|
.1
|
||||||
|
.peer_certificates()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.to_vec())
|
||||||
|
.collect()),
|
||||||
|
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
@@ -478,7 +514,7 @@ impl AsyncNetworkStream {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.to_der()
|
.to_der()
|
||||||
.map_err(error::tls)?),
|
.map_err(error::tls)?),
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
|
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
|
||||||
.get_ref()
|
.get_ref()
|
||||||
.1
|
.1
|
||||||
@@ -486,8 +522,7 @@ impl AsyncNetworkStream {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.first()
|
.first()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.to_vec()),
|
||||||
.0),
|
|
||||||
#[cfg(feature = "tokio1-boring-tls")]
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
||||||
.ssl()
|
.ssl()
|
||||||
@@ -499,9 +534,7 @@ impl AsyncNetworkStream {
|
|||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||||
Err(error::client("Connection is not encrypted"))
|
Err(error::client("Connection is not encrypted"))
|
||||||
}
|
}
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(t) => panic!("Unsupported"),
|
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
|
||||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
|
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
|
||||||
.get_ref()
|
.get_ref()
|
||||||
.1
|
.1
|
||||||
@@ -509,22 +542,22 @@ impl AsyncNetworkStream {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.first()
|
.first()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.to_vec()),
|
||||||
.0),
|
|
||||||
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
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>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
) -> Poll<IoResult<usize>> {
|
) -> Poll<IoResult<usize>> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => {
|
InnerAsyncNetworkStream::Tokio1Tcp(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())),
|
||||||
@@ -533,7 +566,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "tokio1-native-tls")]
|
#[cfg(feature = "tokio1-native-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => {
|
InnerAsyncNetworkStream::Tokio1NativeTls(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())),
|
||||||
@@ -541,8 +574,8 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
|||||||
Poll::Pending => Poll::Pending,
|
Poll::Pending => Poll::Pending,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut 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())),
|
||||||
@@ -551,7 +584,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "tokio1-boring-tls")]
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => {
|
InnerAsyncNetworkStream::Tokio1BoringTls(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())),
|
||||||
@@ -560,15 +593,9 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
|
||||||
Pin::new(s).poll_read(cx, buf)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
|
||||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
|
||||||
Pin::new(s).poll_read(cx, buf)
|
|
||||||
}
|
|
||||||
InnerAsyncNetworkStream::None => {
|
InnerAsyncNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||||
Poll::Ready(Ok(0))
|
Poll::Ready(Ok(0))
|
||||||
@@ -577,31 +604,26 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
cx: &mut Context<'_>,
|
cx: &mut Context<'_>,
|
||||||
buf: &[u8],
|
buf: &[u8],
|
||||||
) -> Poll<IoResult<usize>> {
|
) -> Poll<IoResult<usize>> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut 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(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut 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(ref mut 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(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||||
Pin::new(s).poll_write(cx, buf)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
|
||||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
|
||||||
Pin::new(s).poll_write(cx, buf)
|
|
||||||
}
|
|
||||||
InnerAsyncNetworkStream::None => {
|
InnerAsyncNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||||
Poll::Ready(Ok(0))
|
Poll::Ready(Ok(0))
|
||||||
@@ -610,21 +632,19 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut 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(ref mut s) => Pin::new(s).poll_flush(cx),
|
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut 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(ref mut 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(ref mut s) => Pin::new(s).poll_flush(cx),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
|
||||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
|
||||||
InnerAsyncNetworkStream::None => {
|
InnerAsyncNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||||
Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
@@ -633,21 +653,19 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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<()>> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut 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(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut 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(ref mut 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(ref mut s) => Pin::new(s).poll_close(cx),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-rustls")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_close(cx),
|
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
|
||||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_close(cx),
|
|
||||||
InnerAsyncNetworkStream::None => {
|
InnerAsyncNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||||
Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ impl SmtpConnection {
|
|||||||
mail_options.push(MailParameter::SmtpUtfEight);
|
mail_options.push(MailParameter::SmtpUtfEight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for non-ascii content in message
|
// Check for non-ascii content in the message
|
||||||
if !email.is_ascii() {
|
if !email.is_ascii() {
|
||||||
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||||
return Err(error::client(
|
return Err(error::client(
|
||||||
@@ -143,7 +143,7 @@ impl SmtpConnection {
|
|||||||
hello_name: &ClientId,
|
hello_name: &ClientId,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if self.server_info.supports_feature(Extension::StartTls) {
|
if self.server_info.supports_feature(Extension::StartTls) {
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
{
|
{
|
||||||
try_smtp!(self.command(Starttls), self);
|
try_smtp!(self.command(Starttls), self);
|
||||||
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
||||||
@@ -153,11 +153,7 @@ impl SmtpConnection {
|
|||||||
try_smtp!(self.ehlo(hello_name), self);
|
try_smtp!(self.ehlo(hello_name), self);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
|
||||||
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");
|
||||||
@@ -207,7 +203,7 @@ impl SmtpConnection {
|
|||||||
self.command(Noop).is_ok()
|
self.command(Noop).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
/// Sends an AUTH command with the given mechanism, and handles the challenge if needed
|
||||||
pub fn auth(
|
pub fn auth(
|
||||||
&mut self,
|
&mut self,
|
||||||
mechanisms: &[Mechanism],
|
mechanisms: &[Mechanism],
|
||||||
@@ -303,8 +299,30 @@ 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-tls", feature = "boring-tls"))]
|
#[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")]
|
||||||
|
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"))]
|
||||||
|
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||||
|
self.stream.get_ref().certificate_chain()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,17 +28,18 @@ 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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
pub(super) use self::tls::InnerTlsParameters;
|
pub(super) use self::tls::InnerTlsParameters;
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
pub use self::tls::TlsVersion;
|
pub use self::tls::TlsVersion;
|
||||||
pub use self::{
|
pub use self::{
|
||||||
connection::SmtpConnection,
|
connection::SmtpConnection,
|
||||||
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
|
tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
@@ -57,7 +58,7 @@ struct ClientCodec {
|
|||||||
|
|
||||||
impl ClientCodec {
|
impl ClientCodec {
|
||||||
/// Creates a new client codec
|
/// Creates a new client codec
|
||||||
pub fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
status: CodecStatus::StartOfNewLine,
|
status: CodecStatus::StartOfNewLine,
|
||||||
}
|
}
|
||||||
@@ -139,7 +140,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "log")]
|
#[cfg(feature = "tracing")]
|
||||||
fn test_escape_crlf() {
|
fn test_escape_crlf() {
|
||||||
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
|
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
|
||||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
|
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
@@ -11,11 +11,11 @@ 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-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
use rustls::{ClientConnection, ServerName, 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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
use super::InnerTlsParameters;
|
use super::InnerTlsParameters;
|
||||||
use super::TlsParameters;
|
use super::TlsParameters;
|
||||||
use crate::transport::smtp::{error, Error};
|
use crate::transport::smtp::{error, Error};
|
||||||
@@ -36,7 +36,7 @@ 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-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
|
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
BoringTls(SslStream<TcpStream>),
|
BoringTls(SslStream<TcpStream>),
|
||||||
@@ -55,14 +55,14 @@ impl NetworkStream {
|
|||||||
|
|
||||||
/// Returns peer's address
|
/// Returns peer's address
|
||||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||||
match self.inner {
|
match &self.inner {
|
||||||
InnerNetworkStream::Tcp(ref s) => s.peer_addr(),
|
InnerNetworkStream::Tcp(s) => s.peer_addr(),
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
|
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
|
InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(),
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(ref s) => s.get_ref().peer_addr(),
|
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(SocketAddr::V4(SocketAddrV4::new(
|
Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||||
@@ -75,14 +75,14 @@ impl NetworkStream {
|
|||||||
|
|
||||||
/// Shutdowns the connection
|
/// Shutdowns the connection
|
||||||
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||||
match self.inner {
|
match &self.inner {
|
||||||
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
|
InnerNetworkStream::Tcp(s) => s.shutdown(how),
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
|
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
|
InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how),
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(ref s) => s.get_ref().shutdown(how),
|
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -119,12 +119,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,23 +146,18 @@ impl NetworkStream {
|
|||||||
|
|
||||||
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
#[cfg(not(any(
|
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
|
||||||
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-tls feature");
|
panic!("Trying to upgrade an NetworkStream without having enabled either the `native-tls` or the `rustls` feature");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
InnerNetworkStream::Tcp(_) => {
|
InnerNetworkStream::Tcp(_) => {
|
||||||
// get owned TcpStream
|
// get owned TcpStream
|
||||||
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
||||||
let tcp_stream = match tcp_stream {
|
let InnerNetworkStream::Tcp(tcp_stream) = tcp_stream else {
|
||||||
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
|
unreachable!()
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
||||||
@@ -172,7 +167,7 @@ impl NetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
fn upgrade_tls_impl(
|
fn upgrade_tls_impl(
|
||||||
tcp_stream: TcpStream,
|
tcp_stream: TcpStream,
|
||||||
tls_parameters: &TlsParameters,
|
tls_parameters: &TlsParameters,
|
||||||
@@ -185,11 +180,11 @@ impl NetworkStream {
|
|||||||
.map_err(error::connection)?;
|
.map_err(error::connection)?;
|
||||||
InnerNetworkStream::NativeTls(stream)
|
InnerNetworkStream::NativeTls(stream)
|
||||||
}
|
}
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerTlsParameters::RustlsTls(connector) => {
|
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(connector), domain)
|
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::RustlsTls(stream)
|
InnerNetworkStream::RustlsTls(stream)
|
||||||
@@ -208,11 +203,11 @@ impl NetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_encrypted(&self) -> bool {
|
pub fn is_encrypted(&self) -> bool {
|
||||||
match self.inner {
|
match &self.inner {
|
||||||
InnerNetworkStream::Tcp(_) => false,
|
InnerNetworkStream::Tcp(_) => false,
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerNetworkStream::NativeTls(_) => true,
|
InnerNetworkStream::NativeTls(_) => true,
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerNetworkStream::RustlsTls(_) => true,
|
InnerNetworkStream::RustlsTls(_) => true,
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(_) => true,
|
InnerNetworkStream::BoringTls(_) => true,
|
||||||
@@ -223,7 +218,49 @@ impl NetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[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::RustlsTls(_) => 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"))]
|
||||||
|
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::RustlsTls(stream) => Ok(stream
|
||||||
|
.conn
|
||||||
|
.peer_certificates()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.to_vec())
|
||||||
|
.collect()),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
||||||
|
.ssl()
|
||||||
|
.peer_cert_chain()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.to_der().map_err(error::tls))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?),
|
||||||
|
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls", 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")),
|
||||||
@@ -234,15 +271,14 @@ impl NetworkStream {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.to_der()
|
.to_der()
|
||||||
.map_err(error::tls)?),
|
.map_err(error::tls)?),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerNetworkStream::RustlsTls(stream) => Ok(stream
|
InnerNetworkStream::RustlsTls(stream) => Ok(stream
|
||||||
.conn
|
.conn
|
||||||
.peer_certificates()
|
.peer_certificates()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.first()
|
.first()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.clone()
|
.to_vec()),
|
||||||
.0),
|
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
||||||
.ssl()
|
.ssl()
|
||||||
@@ -255,20 +291,14 @@ impl NetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||||
stream.get_ref().set_read_timeout(duration)
|
#[cfg(feature = "rustls")]
|
||||||
}
|
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||||
#[cfg(feature = "rustls-tls")]
|
|
||||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
|
||||||
stream.get_ref().set_read_timeout(duration)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(ref mut stream) => {
|
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||||
stream.get_ref().set_read_timeout(duration)
|
|
||||||
}
|
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -278,21 +308,15 @@ impl NetworkStream {
|
|||||||
|
|
||||||
/// Set write timeout for IO calls
|
/// Set write timeout for IO calls
|
||||||
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
InnerNetworkStream::Tcp(stream) => stream.set_write_timeout(duration),
|
||||||
|
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||||
stream.get_ref().set_write_timeout(duration)
|
#[cfg(feature = "rustls")]
|
||||||
}
|
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||||
#[cfg(feature = "rustls-tls")]
|
|
||||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
|
||||||
stream.get_ref().set_write_timeout(duration)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(ref mut stream) => {
|
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||||
stream.get_ref().set_write_timeout(duration)
|
|
||||||
}
|
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -303,14 +327,14 @@ impl NetworkStream {
|
|||||||
|
|
||||||
impl Read for NetworkStream {
|
impl Read for NetworkStream {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
InnerNetworkStream::Tcp(ref mut s) => s.read(buf),
|
InnerNetworkStream::Tcp(s) => s.read(buf),
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
|
InnerNetworkStream::NativeTls(s) => s.read(buf),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
|
InnerNetworkStream::RustlsTls(s) => s.read(buf),
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(ref mut s) => s.read(buf),
|
InnerNetworkStream::BoringTls(s) => s.read(buf),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(0)
|
Ok(0)
|
||||||
@@ -321,14 +345,14 @@ impl Read for NetworkStream {
|
|||||||
|
|
||||||
impl Write for NetworkStream {
|
impl Write for NetworkStream {
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
InnerNetworkStream::Tcp(ref mut s) => s.write(buf),
|
InnerNetworkStream::Tcp(s) => s.write(buf),
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
|
InnerNetworkStream::NativeTls(s) => s.write(buf),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
|
InnerNetworkStream::RustlsTls(s) => s.write(buf),
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(ref mut s) => s.write(buf),
|
InnerNetworkStream::BoringTls(s) => s.write(buf),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(0)
|
Ok(0)
|
||||||
@@ -337,14 +361,14 @@ impl Write for NetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
match self.inner {
|
match &mut self.inner {
|
||||||
InnerNetworkStream::Tcp(ref mut s) => s.flush(),
|
InnerNetworkStream::Tcp(s) => s.flush(),
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
|
InnerNetworkStream::NativeTls(s) => s.flush(),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
|
InnerNetworkStream::RustlsTls(s) => s.flush(),
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
InnerNetworkStream::BoringTls(ref mut s) => s.flush(),
|
InnerNetworkStream::BoringTls(s) => s.flush(),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -354,9 +378,9 @@ 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 to 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,
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
use std::fmt::{self, Debug};
|
use std::fmt::{self, Debug};
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
use std::{sync::Arc, time::SystemTime};
|
use std::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-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
use rustls::{
|
use rustls::{
|
||||||
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
|
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||||
ClientConfig, Error as TlsError, RootCertStore, ServerName,
|
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider},
|
||||||
|
pki_types::{self, pem::PemObject, CertificateDer, PrivateKeyDer, ServerName, UnixTime},
|
||||||
|
server::ParsedCertificate,
|
||||||
|
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
use crate::transport::smtp::{error, Error};
|
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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
pub enum TlsVersion {
|
pub enum TlsVersion {
|
||||||
/// TLS 1.0
|
/// TLS 1.0
|
||||||
///
|
///
|
||||||
@@ -47,38 +51,72 @@ pub enum TlsVersion {
|
|||||||
Tlsv12,
|
Tlsv12,
|
||||||
/// TLS 1.3
|
/// TLS 1.3
|
||||||
///
|
///
|
||||||
/// The most secure option, altough not supported by all SMTP servers.
|
/// The most secure option, although not supported by all SMTP servers.
|
||||||
///
|
///
|
||||||
/// Altough it is technically supported by all TLS backends,
|
/// Although it is technically supported by all TLS backends,
|
||||||
/// trying to set it for `native-tls` will give a runtime error.
|
/// trying to set it for `native-tls` will give a runtime error.
|
||||||
Tlsv13,
|
Tlsv13,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How to apply TLS to a client connection
|
/// Specifies how to establish a TLS connection
|
||||||
|
///
|
||||||
|
/// TLDR: Use [`Tls::Wrapper`] or [`Tls::Required`] when
|
||||||
|
/// connecting to a remote server, [`Tls::None`] when
|
||||||
|
/// connecting to a local server.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(missing_copy_implementations)]
|
#[allow(missing_copy_implementations)]
|
||||||
pub enum Tls {
|
pub enum Tls {
|
||||||
/// Insecure connection only (for testing purposes)
|
/// Insecure (plaintext) connection only.
|
||||||
|
///
|
||||||
|
/// This option **always** uses a plaintext connection and should only
|
||||||
|
/// be used for trusted local relays. It is **highly discouraged**
|
||||||
|
/// for remote servers, as it exposes credentials and emails to potential
|
||||||
|
/// interception.
|
||||||
|
///
|
||||||
|
/// Note: Servers requiring credentials or emails to be sent over TLS
|
||||||
|
/// may reject connections when this option is used.
|
||||||
None,
|
None,
|
||||||
/// Start with insecure connection and use `STARTTLS` when available
|
/// Begin with a plaintext connection and attempt to use `STARTTLS` if 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-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
Opportunistic(TlsParameters),
|
Opportunistic(TlsParameters),
|
||||||
/// Start with insecure connection and require `STARTTLS`
|
/// Begin with a plaintext connection and require `STARTTLS` for security.
|
||||||
#[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-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
Required(TlsParameters),
|
Required(TlsParameters),
|
||||||
/// Use TLS wrapped connection
|
/// Establish a connection wrapped in TLS from the start.
|
||||||
#[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-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
Wrapper(TlsParameters),
|
Wrapper(TlsParameters),
|
||||||
}
|
}
|
||||||
@@ -99,33 +137,28 @@ 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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
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 also use the the system store if the `rustls-native-certs` feature is
|
/// For rustls, this will also use the system store if the `rustls-native-certs` feature is
|
||||||
/// enabled, or will fall back to `webpki-roots`.
|
/// enabled, or will fall back to `webpki-roots`.
|
||||||
///
|
///
|
||||||
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
|
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
|
||||||
|
#[default]
|
||||||
Default,
|
Default,
|
||||||
/// 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(feature = "webpki-roots")]
|
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
|
||||||
WebpkiRoots,
|
WebpkiRoots,
|
||||||
/// Don't use any system certificates.
|
/// Don't use any system certificates.
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CertificateStore {
|
|
||||||
fn default() -> Self {
|
|
||||||
CertificateStore::Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters to use for secure clients
|
/// Parameters to use for secure clients
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TlsParameters {
|
pub struct TlsParameters {
|
||||||
@@ -142,9 +175,10 @@ pub struct TlsParametersBuilder {
|
|||||||
domain: String,
|
domain: String,
|
||||||
cert_store: CertificateStore,
|
cert_store: CertificateStore,
|
||||||
root_certs: Vec<Certificate>,
|
root_certs: Vec<Certificate>,
|
||||||
|
identity: Option<Identity>,
|
||||||
accept_invalid_hostnames: bool,
|
accept_invalid_hostnames: bool,
|
||||||
accept_invalid_certs: bool,
|
accept_invalid_certs: bool,
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
min_tls_version: TlsVersion,
|
min_tls_version: TlsVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +189,10 @@ impl TlsParametersBuilder {
|
|||||||
domain,
|
domain,
|
||||||
cert_store: CertificateStore::Default,
|
cert_store: CertificateStore::Default,
|
||||||
root_certs: Vec::new(),
|
root_certs: Vec::new(),
|
||||||
|
identity: None,
|
||||||
accept_invalid_hostnames: false,
|
accept_invalid_hostnames: false,
|
||||||
accept_invalid_certs: false,
|
accept_invalid_certs: false,
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
min_tls_version: TlsVersion::Tlsv12,
|
min_tls_version: TlsVersion::Tlsv12,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,12 +205,20 @@ impl TlsParametersBuilder {
|
|||||||
|
|
||||||
/// Add a custom root certificate
|
/// Add a custom root certificate
|
||||||
///
|
///
|
||||||
/// Can be used to safely connect to a server using a self signed certificate, for example.
|
/// Can be used to safely connect to a server using a self-signed certificate, for example.
|
||||||
pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
|
pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
|
||||||
self.root_certs.push(cert);
|
self.root_certs.push(cert);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a client certificate
|
||||||
|
///
|
||||||
|
/// Can be used to configure a client certificate to present to the server.
|
||||||
|
pub fn identify_with(mut self, identity: Identity) -> Self {
|
||||||
|
self.identity = Some(identity);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Controls whether certificates with an invalid hostname are accepted
|
/// Controls whether certificates with an invalid hostname are accepted
|
||||||
///
|
///
|
||||||
/// Defaults to `false`.
|
/// Defaults to `false`.
|
||||||
@@ -187,10 +230,11 @@ impl TlsParametersBuilder {
|
|||||||
/// including those from other sites, are trusted.
|
/// including those from other sites, are trusted.
|
||||||
///
|
///
|
||||||
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
||||||
///
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
|
#[cfg_attr(
|
||||||
#[cfg(any(feature = "native-tls", feature = "boring-tls"))]
|
docsrs,
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
|
doc(cfg(any(feature = "native-tls", feature = "rustls", 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
|
||||||
@@ -199,7 +243,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-tls", feature = "boring-tls"))]
|
#[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
|
||||||
@@ -228,17 +272,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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
docsrs,
|
docsrs,
|
||||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
pub fn build(self) -> Result<TlsParameters, Error> {
|
pub fn build(self) -> Result<TlsParameters, Error> {
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
return self.build_rustls();
|
return self.build_rustls();
|
||||||
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
|
#[cfg(all(not(feature = "rustls"), feature = "native-tls"))]
|
||||||
return self.build_native();
|
return self.build_native();
|
||||||
#[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
|
#[cfg(all(not(feature = "rustls"), feature = "boring-tls"))]
|
||||||
return self.build_boring();
|
return self.build_boring();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +322,10 @@ impl TlsParametersBuilder {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tls_builder.min_protocol_version(Some(min_tls_version));
|
tls_builder.min_protocol_version(Some(min_tls_version));
|
||||||
|
if let Some(identity) = self.identity {
|
||||||
|
tls_builder.identity(identity.native_tls);
|
||||||
|
}
|
||||||
|
|
||||||
let connector = tls_builder.build().map_err(error::tls)?;
|
let connector = tls_builder.build().map_err(error::tls)?;
|
||||||
Ok(TlsParameters {
|
Ok(TlsParameters {
|
||||||
connector: InnerTlsParameters::NativeTls(connector),
|
connector: InnerTlsParameters::NativeTls(connector),
|
||||||
@@ -320,6 +368,15 @@ impl TlsParametersBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(identity) = self.identity {
|
||||||
|
tls_builder
|
||||||
|
.set_certificate(identity.boring_tls.0.as_ref())
|
||||||
|
.map_err(error::tls)?;
|
||||||
|
tls_builder
|
||||||
|
.set_private_key(identity.boring_tls.1.as_ref())
|
||||||
|
.map_err(error::tls)?;
|
||||||
|
}
|
||||||
|
|
||||||
let min_tls_version = match self.min_tls_version {
|
let min_tls_version = match self.min_tls_version {
|
||||||
TlsVersion::Tlsv10 => SslVersion::TLS1,
|
TlsVersion::Tlsv10 => SslVersion::TLS1,
|
||||||
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
|
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
|
||||||
@@ -339,11 +396,9 @@ impl TlsParametersBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new `TlsParameters` using rustls with the provided configuration
|
/// Creates a new `TlsParameters` using rustls with the provided configuration
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||||
let tls = ClientConfig::builder();
|
|
||||||
|
|
||||||
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 {
|
||||||
TlsVersion::Tlsv10 => {
|
TlsVersion::Tlsv10 => {
|
||||||
@@ -356,75 +411,73 @@ impl TlsParametersBuilder {
|
|||||||
TlsVersion::Tlsv13 => just_version3,
|
TlsVersion::Tlsv13 => just_version3,
|
||||||
};
|
};
|
||||||
|
|
||||||
let tls = tls
|
let crypto_provider = crate::rustls_crypto::crypto_provider();
|
||||||
.with_safe_default_cipher_suites()
|
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
|
||||||
.with_safe_default_kx_groups()
|
|
||||||
.with_protocol_versions(supported_versions)
|
.with_protocol_versions(supported_versions)
|
||||||
.map_err(error::tls)?;
|
.map_err(error::tls)?;
|
||||||
|
|
||||||
let tls = if self.accept_invalid_certs {
|
// Build TLS config
|
||||||
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
|
let mut root_cert_store = RootCertStore::empty();
|
||||||
|
|
||||||
|
#[cfg(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());
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.cert_store {
|
||||||
|
CertificateStore::Default => {
|
||||||
|
#[cfg(feature = "rustls-native-certs")]
|
||||||
|
load_native_roots(&mut root_cert_store);
|
||||||
|
#[cfg(all(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 {
|
||||||
|
root_cert_store.add(rustls_cert).map_err(error::tls)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tls = if self.accept_invalid_certs || self.accept_invalid_hostnames {
|
||||||
|
let verifier = InvalidCertsVerifier {
|
||||||
|
ignore_invalid_hostnames: self.accept_invalid_hostnames,
|
||||||
|
ignore_invalid_certs: self.accept_invalid_certs,
|
||||||
|
roots: root_cert_store,
|
||||||
|
crypto_provider,
|
||||||
|
};
|
||||||
|
tls.dangerous()
|
||||||
|
.with_custom_certificate_verifier(Arc::new(verifier))
|
||||||
} else {
|
} else {
|
||||||
let mut root_cert_store = RootCertStore::empty();
|
tls.with_root_certificates(root_cert_store)
|
||||||
|
};
|
||||||
#[cfg(feature = "rustls-native-certs")]
|
|
||||||
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
|
let tls = if let Some(identity) = self.identity {
|
||||||
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
|
let (client_certificates, private_key) = identity.rustls_tls;
|
||||||
let mut valid_count = 0;
|
tls.with_client_auth_cert(client_certificates, private_key)
|
||||||
let mut invalid_count = 0;
|
.map_err(error::tls)?
|
||||||
for cert in native_certs {
|
} else {
|
||||||
match store.add(&rustls::Certificate(cert.0)) {
|
tls.with_no_client_auth()
|
||||||
Ok(_) => valid_count += 1,
|
|
||||||
Err(err) => {
|
|
||||||
#[cfg(feature = "tracing")]
|
|
||||||
tracing::debug!("certificate parsing failed: {:?}", err);
|
|
||||||
invalid_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(feature = "tracing")]
|
|
||||||
tracing::debug!(
|
|
||||||
"loaded platform certs with {valid_count} valid and {invalid_count} invalid certs"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "webpki-roots")]
|
|
||||||
fn load_webpki_roots(store: &mut RootCertStore) {
|
|
||||||
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
|
|
||||||
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
|
|
||||||
ta.subject,
|
|
||||||
ta.spki,
|
|
||||||
ta.name_constraints,
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.cert_store {
|
|
||||||
CertificateStore::Default => {
|
|
||||||
#[cfg(feature = "rustls-native-certs")]
|
|
||||||
load_native_roots(&mut root_cert_store)?;
|
|
||||||
#[cfg(all(not(feature = "rustls-native-certs"), feature = "webpki-roots"))]
|
|
||||||
load_webpki_roots(&mut root_cert_store);
|
|
||||||
}
|
|
||||||
#[cfg(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 {
|
|
||||||
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
|
|
||||||
root_cert_store,
|
|
||||||
None,
|
|
||||||
)))
|
|
||||||
};
|
};
|
||||||
let tls = tls.with_no_client_auth();
|
|
||||||
|
|
||||||
Ok(TlsParameters {
|
Ok(TlsParameters {
|
||||||
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
||||||
@@ -437,10 +490,10 @@ impl TlsParametersBuilder {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub enum InnerTlsParameters {
|
pub(crate) enum InnerTlsParameters {
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
NativeTls(TlsConnector),
|
NativeTls(TlsConnector),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
RustlsTls(Arc<ClientConfig>),
|
RustlsTls(Arc<ClientConfig>),
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
BoringTls(SslConnector),
|
BoringTls(SslConnector),
|
||||||
@@ -449,10 +502,10 @@ pub enum InnerTlsParameters {
|
|||||||
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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
docsrs,
|
docsrs,
|
||||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
pub fn new(domain: String) -> Result<Self, Error> {
|
pub fn new(domain: String) -> Result<Self, Error> {
|
||||||
TlsParametersBuilder::new(domain).build()
|
TlsParametersBuilder::new(domain).build()
|
||||||
@@ -471,8 +524,8 @@ impl TlsParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new `TlsParameters` using rustls
|
/// Creates a new `TlsParameters` using rustls
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||||
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()
|
||||||
}
|
}
|
||||||
@@ -489,19 +542,19 @@ impl TlsParameters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
|
/// A certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(missing_copy_implementations)]
|
#[allow(missing_copy_implementations)]
|
||||||
pub struct Certificate {
|
pub struct Certificate {
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
native_tls: native_tls::Certificate,
|
native_tls: native_tls::Certificate,
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
rustls: Vec<rustls::Certificate>,
|
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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", 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> {
|
||||||
@@ -514,8 +567,8 @@ 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-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
rustls: vec![rustls::Certificate(der)],
|
rustls: vec![der.into()],
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
boring_tls: boring_tls_cert,
|
boring_tls: boring_tls_cert,
|
||||||
})
|
})
|
||||||
@@ -529,22 +582,17 @@ 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-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
let rustls_cert = {
|
let rustls_cert = {
|
||||||
use std::io::Cursor;
|
CertificateDer::pem_slice_iter(pem)
|
||||||
|
.collect::<Result<Vec<_>, pki_types::pem::Error>>()
|
||||||
let mut pem = Cursor::new(pem);
|
|
||||||
rustls_pemfile::certs(&mut pem)
|
|
||||||
.map_err(|_| error::tls("invalid certificates"))?
|
.map_err(|_| error::tls("invalid certificates"))?
|
||||||
.into_iter()
|
|
||||||
.map(rustls::Certificate)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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-tls")]
|
#[cfg(feature = "rustls")]
|
||||||
rustls: rustls_cert,
|
rustls: rustls_cert,
|
||||||
#[cfg(feature = "boring-tls")]
|
#[cfg(feature = "boring-tls")]
|
||||||
boring_tls: boring_tls_cert,
|
boring_tls: boring_tls_cert,
|
||||||
@@ -558,20 +606,149 @@ impl Debug for Certificate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rustls-tls")]
|
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
|
||||||
struct InvalidCertsVerifier;
|
#[allow(missing_copy_implementations)]
|
||||||
|
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>),
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rustls-tls")]
|
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)]
|
||||||
|
struct InvalidCertsVerifier {
|
||||||
|
ignore_invalid_hostnames: bool,
|
||||||
|
ignore_invalid_certs: bool,
|
||||||
|
roots: RootCertStore,
|
||||||
|
crypto_provider: Arc<CryptoProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "rustls")]
|
||||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||||
fn verify_server_cert(
|
fn verify_server_cert(
|
||||||
&self,
|
&self,
|
||||||
_end_entity: &rustls::Certificate,
|
end_entity: &CertificateDer<'_>,
|
||||||
_intermediates: &[rustls::Certificate],
|
intermediates: &[CertificateDer<'_>],
|
||||||
_server_name: &ServerName,
|
server_name: &ServerName<'_>,
|
||||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
|
||||||
_ocsp_response: &[u8],
|
_ocsp_response: &[u8],
|
||||||
_now: SystemTime,
|
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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn verify_tls12_signature(
|
||||||
|
&self,
|
||||||
|
message: &[u8],
|
||||||
|
cert: &CertificateDer<'_>,
|
||||||
|
dss: &DigitallySignedStruct,
|
||||||
|
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||||
|
verify_tls12_signature(
|
||||||
|
message,
|
||||||
|
cert,
|
||||||
|
dss,
|
||||||
|
&self.crypto_provider.signature_verification_algorithms,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_tls13_signature(
|
||||||
|
&self,
|
||||||
|
message: &[u8],
|
||||||
|
cert: &CertificateDer<'_>,
|
||||||
|
dss: &DigitallySignedStruct,
|
||||||
|
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||||
|
verify_tls13_signature(
|
||||||
|
message,
|
||||||
|
cert,
|
||||||
|
dss,
|
||||||
|
&self.crypto_provider.signature_verification_algorithms,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||||
|
self.crypto_provider
|
||||||
|
.signature_verification_algorithms
|
||||||
|
.supported_schemes()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
src/transport/smtp/connection_url.rs
Normal file
131
src/transport/smtp/connection_url.rs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
|
use super::client::{Tls, TlsParameters};
|
||||||
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
|
use super::AsyncSmtpTransportBuilder;
|
||||||
|
use super::{
|
||||||
|
authentication::Credentials, error, extension::ClientId, Error, SmtpTransportBuilder,
|
||||||
|
SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) trait TransportBuilder {
|
||||||
|
fn new<T: Into<String>>(server: T) -> Self;
|
||||||
|
fn tls(self, tls: super::Tls) -> Self;
|
||||||
|
fn port(self, port: u16) -> Self;
|
||||||
|
fn credentials(self, credentials: Credentials) -> Self;
|
||||||
|
fn hello_name(self, name: ClientId) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportBuilder for SmtpTransportBuilder {
|
||||||
|
fn new<T: Into<String>>(server: T) -> Self {
|
||||||
|
Self::new(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tls(self, tls: super::Tls) -> Self {
|
||||||
|
self.tls(tls)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port(self, port: u16) -> Self {
|
||||||
|
self.port(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn credentials(self, credentials: Credentials) -> Self {
|
||||||
|
self.credentials(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hello_name(self, name: ClientId) -> Self {
|
||||||
|
self.hello_name(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
|
impl TransportBuilder for AsyncSmtpTransportBuilder {
|
||||||
|
fn new<T: Into<String>>(server: T) -> Self {
|
||||||
|
Self::new(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tls(self, tls: super::Tls) -> Self {
|
||||||
|
self.tls(tls)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port(self, port: u16) -> Self {
|
||||||
|
self.port(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn credentials(self, credentials: Credentials) -> Self {
|
||||||
|
self.credentials(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hello_name(self, name: ClientId) -> Self {
|
||||||
|
self.hello_name(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new `SmtpTransportBuilder` or `AsyncSmtpTransportBuilder` from a connection URL
|
||||||
|
pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) -> Result<B, Error> {
|
||||||
|
let connection_url = Url::parse(connection_url).map_err(error::connection)?;
|
||||||
|
let tls: Option<String> = connection_url
|
||||||
|
.query_pairs()
|
||||||
|
.find(|(k, _)| k == "tls")
|
||||||
|
.map(|(_, v)| v.to_string());
|
||||||
|
|
||||||
|
let host = connection_url
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| error::connection("smtp host undefined"))?;
|
||||||
|
|
||||||
|
let mut builder = B::new(host);
|
||||||
|
|
||||||
|
match (connection_url.scheme(), tls.as_deref()) {
|
||||||
|
("smtp", None) => {
|
||||||
|
builder = builder.port(connection_url.port().unwrap_or(SMTP_PORT));
|
||||||
|
}
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
|
("smtp", Some("required")) => {
|
||||||
|
builder = builder
|
||||||
|
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
|
||||||
|
.tls(Tls::Required(TlsParameters::new(host.into())?));
|
||||||
|
}
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
|
("smtp", Some("opportunistic")) => {
|
||||||
|
builder = builder
|
||||||
|
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
|
||||||
|
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?));
|
||||||
|
}
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
|
("smtps", _) => {
|
||||||
|
builder = builder
|
||||||
|
.port(connection_url.port().unwrap_or(SUBMISSIONS_PORT))
|
||||||
|
.tls(Tls::Wrapper(TlsParameters::new(host.into())?));
|
||||||
|
}
|
||||||
|
(scheme, tls) => {
|
||||||
|
return Err(error::connection(format!(
|
||||||
|
"Unknown scheme '{scheme}' or tls parameter '{tls:?}', note that a transport with TLS requires one of the TLS features"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the path segment of the URL as name in the name in the HELO / EHLO command
|
||||||
|
if connection_url.path().len() > 1 {
|
||||||
|
let name = connection_url.path().trim_matches('/').to_owned();
|
||||||
|
builder = builder.hello_name(ClientId::Domain(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(password) = connection_url.password() {
|
||||||
|
let percent_decode = |s: &str| {
|
||||||
|
percent_encoding::percent_decode_str(s)
|
||||||
|
.decode_utf8()
|
||||||
|
.map(Cow::into_owned)
|
||||||
|
.map_err(error::connection)
|
||||||
|
};
|
||||||
|
let credentials = Credentials::new(
|
||||||
|
percent_decode(connection_url.username())?,
|
||||||
|
percent_decode(password)?,
|
||||||
|
);
|
||||||
|
builder = builder.credentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(builder)
|
||||||
|
}
|
||||||
@@ -68,15 +68,20 @@ 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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
docsrs,
|
docsrs,
|
||||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
pub fn is_tls(&self) -> bool {
|
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 {
|
||||||
@@ -107,10 +112,12 @@ pub(crate) enum Kind {
|
|||||||
/// TLS error
|
/// TLS error
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
docsrs,
|
docsrs,
|
||||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
Tls,
|
Tls,
|
||||||
|
/// Transport shutdown error
|
||||||
|
TransportShutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Error {
|
impl fmt::Debug for Error {
|
||||||
@@ -119,7 +126,7 @@ impl fmt::Debug for Error {
|
|||||||
|
|
||||||
builder.field("kind", &self.inner.kind);
|
builder.field("kind", &self.inner.kind);
|
||||||
|
|
||||||
if let Some(ref source) = self.inner.source {
|
if let Some(source) = &self.inner.source {
|
||||||
builder.field("source", source);
|
builder.field("source", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,22 +136,23 @@ impl fmt::Debug for Error {
|
|||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
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")?,
|
||||||
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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
Kind::Tls => f.write_str("tls error")?,
|
Kind::Tls => f.write_str("tls error")?,
|
||||||
Kind::Transient(ref code) => {
|
Kind::TransportShutdown => f.write_str("transport has been shut down")?,
|
||||||
|
Kind::Transient(code) => {
|
||||||
write!(f, "transient error ({code})")?;
|
write!(f, "transient error ({code})")?;
|
||||||
}
|
}
|
||||||
Kind::Permanent(ref code) => {
|
Kind::Permanent(code) => {
|
||||||
write!(f, "permanent error ({code})")?;
|
write!(f, "permanent error ({code})")?;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
if let Some(ref e) = self.inner.source {
|
if let Some(e) = &self.inner.source {
|
||||||
write!(f, ": {e}")?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +193,11 @@ 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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", 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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use std::{
|
|||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fmt::{self, Display, Formatter},
|
fmt::{self, Display, Formatter},
|
||||||
net::{Ipv4Addr, Ipv6Addr},
|
net::{Ipv4Addr, Ipv6Addr},
|
||||||
result::Result,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::{
|
||||||
@@ -53,10 +52,10 @@ impl Default for ClientId {
|
|||||||
|
|
||||||
impl Display for ClientId {
|
impl Display for ClientId {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match self {
|
||||||
Self::Domain(ref value) => f.write_str(value),
|
Self::Domain(value) => f.write_str(value),
|
||||||
Self::Ipv4(ref value) => write!(f, "[{value}]"),
|
Self::Ipv4(value) => write!(f, "[{value}]"),
|
||||||
Self::Ipv6(ref value) => write!(f, "[IPv6:{value}]"),
|
Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,11 +92,11 @@ pub enum Extension {
|
|||||||
|
|
||||||
impl Display for Extension {
|
impl Display for Extension {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match self {
|
||||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||||
Extension::StartTls => f.write_str("STARTTLS"),
|
Extension::StartTls => f.write_str("STARTTLS"),
|
||||||
Extension::Authentication(ref mechanism) => write!(f, "AUTH {mechanism}"),
|
Extension::Authentication(mechanism) => write!(f, "AUTH {mechanism}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,9 +129,8 @@ 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 name = match response.first_word() {
|
let Some(name) = response.first_word() else {
|
||||||
Some(name) => name,
|
return Err(error::response("Could not read server 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();
|
||||||
@@ -170,7 +168,7 @@ impl ServerInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ServerInfo {
|
Ok(ServerInfo {
|
||||||
@@ -190,7 +188,7 @@ impl ServerInfo {
|
|||||||
.contains(&Extension::Authentication(mechanism))
|
.contains(&Extension::Authentication(mechanism))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a compatible mechanism from list
|
/// Gets a compatible mechanism from a list
|
||||||
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
|
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
|
||||||
for mechanism in mechanisms {
|
for mechanism in mechanisms {
|
||||||
if self.supports_auth_mechanism(*mechanism) {
|
if self.supports_auth_mechanism(*mechanism) {
|
||||||
@@ -227,16 +225,16 @@ pub enum MailParameter {
|
|||||||
|
|
||||||
impl Display for MailParameter {
|
impl Display for MailParameter {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match self {
|
||||||
MailParameter::Body(ref value) => write!(f, "BODY={value}"),
|
MailParameter::Body(value) => write!(f, "BODY={value}"),
|
||||||
MailParameter::Size(size) => write!(f, "SIZE={size}"),
|
MailParameter::Size(size) => write!(f, "SIZE={size}"),
|
||||||
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||||
MailParameter::Other {
|
MailParameter::Other {
|
||||||
ref keyword,
|
keyword,
|
||||||
value: Some(ref value),
|
value: Some(value),
|
||||||
} => write!(f, "{}={}", keyword, XText(value)),
|
} => write!(f, "{}={}", keyword, XText(value)),
|
||||||
MailParameter::Other {
|
MailParameter::Other {
|
||||||
ref keyword,
|
keyword,
|
||||||
value: None,
|
value: None,
|
||||||
} => f.write_str(keyword),
|
} => f.write_str(keyword),
|
||||||
}
|
}
|
||||||
@@ -277,13 +275,13 @@ pub enum RcptParameter {
|
|||||||
|
|
||||||
impl Display for RcptParameter {
|
impl Display for RcptParameter {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match &self {
|
||||||
RcptParameter::Other {
|
RcptParameter::Other {
|
||||||
ref keyword,
|
keyword,
|
||||||
value: Some(ref value),
|
value: Some(value),
|
||||||
} => write!(f, "{}={}", keyword, XText(value)),
|
} => write!(f, "{keyword}={}", XText(value)),
|
||||||
RcptParameter::Other {
|
RcptParameter::Other {
|
||||||
ref keyword,
|
keyword,
|
||||||
value: None,
|
value: None,
|
||||||
} => f.write_str(keyword),
|
} => f.write_str(keyword),
|
||||||
}
|
}
|
||||||
@@ -292,14 +290,8 @@ impl Display for RcptParameter {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::response::{Category, Code, Detail, Severity};
|
||||||
authentication::Mechanism,
|
|
||||||
response::{Category, Code, Detail, Response, Severity},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clientid_fmt() {
|
fn test_clientid_fmt() {
|
||||||
|
|||||||
@@ -26,43 +26,17 @@
|
|||||||
//!
|
//!
|
||||||
//! 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
|
//! #### Simple example with authentication
|
||||||
//!
|
//!
|
||||||
//! This is the most basic example of usage:
|
//! A good starting point for sending emails via SMTP relay is to
|
||||||
|
//! do the following:
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
//! use lettre::{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::{
|
||||||
//! transport::smtp::{
|
//! message::header::ContentType,
|
||||||
//! authentication::{Credentials, Mechanism},
|
//! transport::smtp::authentication::{Credentials, Mechanism},
|
||||||
//! PoolConfig,
|
|
||||||
//! },
|
|
||||||
//! Message, SmtpTransport, Transport,
|
//! Message, SmtpTransport, Transport,
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
@@ -71,35 +45,39 @@
|
|||||||
//! .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 TLS transport on port 587 with STARTTLS
|
//! // Create the SMTPS transport
|
||||||
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
|
//! let sender = SmtpTransport::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(),
|
||||||
//! ))
|
//! ))
|
||||||
//! // Configure expected authentication mechanism
|
//! // Optionally 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
|
||||||
//! let result = sender.send(&email);
|
//! sender.send(&email)?;
|
||||||
//! assert!(result.is_ok());
|
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! You can specify custom TLS settings:
|
//! #### Shortening configuration
|
||||||
|
//!
|
||||||
|
//! It can be very repetitive to ask the user for every SMTP connection parameter.
|
||||||
|
//! In some cases this can be simplified by using a connection URI instead.
|
||||||
|
//!
|
||||||
|
//! For more information take a look at [`SmtpTransport::from_url`] or [`AsyncSmtpTransport::from_url`].
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
//! use lettre::{
|
//! use lettre::{
|
||||||
//! transport::smtp::client::{Tls, TlsParameters},
|
//! message::header::ContentType,
|
||||||
|
//! transport::smtp::authentication::{Credentials, Mechanism},
|
||||||
//! Message, SmtpTransport, Transport,
|
//! Message, SmtpTransport, Transport,
|
||||||
//! };
|
//! };
|
||||||
//!
|
//!
|
||||||
@@ -108,25 +86,106 @@
|
|||||||
//! .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!"))?;
|
||||||
//!
|
//!
|
||||||
//! // Custom TLS configuration
|
//! // Create the SMTPS transport
|
||||||
//! let tls = TlsParameters::builder("smtp.example.com".to_owned())
|
//! let sender = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
|
||||||
//! .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 TLS transport on port 465
|
//! // Create the SMTPS transport
|
||||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||||
//! // Custom TLS configuration
|
//! .tls(Tls::Wrapper(tls))
|
||||||
//! .tls(Tls::Required(tls))
|
|
||||||
//! .build();
|
//! .build();
|
||||||
//!
|
//!
|
||||||
//! // Send the email via remote relay
|
//! // Send the email via remote relay
|
||||||
//! let result = sender.send(&email);
|
//! 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;
|
||||||
|
|
||||||
@@ -140,7 +199,7 @@ pub use self::{
|
|||||||
error::Error,
|
error::Error,
|
||||||
transport::{SmtpTransport, SmtpTransportBuilder},
|
transport::{SmtpTransport, SmtpTransportBuilder},
|
||||||
};
|
};
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
use crate::transport::smtp::client::TlsParameters;
|
use crate::transport::smtp::client::TlsParameters;
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::{
|
||||||
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||||
@@ -154,6 +213,7 @@ mod async_transport;
|
|||||||
pub mod authentication;
|
pub mod authentication;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
mod connection_url;
|
||||||
mod error;
|
mod error;
|
||||||
pub mod extension;
|
pub mod extension;
|
||||||
#[cfg(feature = "pool")]
|
#[cfg(feature = "pool")]
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Debug},
|
fmt::{self, Debug},
|
||||||
mem,
|
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
sync::Arc,
|
sync::{Arc, OnceLock},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,19 +9,22 @@ use futures_util::{
|
|||||||
lock::Mutex,
|
lock::Mutex,
|
||||||
stream::{self, StreamExt},
|
stream::{self, StreamExt},
|
||||||
};
|
};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
super::{client::AsyncSmtpConnection, Error},
|
super::{client::AsyncSmtpConnection, Error},
|
||||||
PoolConfig,
|
PoolConfig,
|
||||||
};
|
};
|
||||||
use crate::{executor::SpawnHandle, transport::smtp::async_transport::AsyncSmtpClient, Executor};
|
use crate::{
|
||||||
|
executor::SpawnHandle,
|
||||||
|
transport::smtp::{async_transport::AsyncSmtpClient, error},
|
||||||
|
Executor,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Pool<E: Executor> {
|
pub(crate) struct Pool<E: Executor> {
|
||||||
config: PoolConfig,
|
config: PoolConfig,
|
||||||
connections: Mutex<Vec<ParkedConnection>>,
|
connections: Mutex<Option<Vec<ParkedConnection>>>,
|
||||||
client: AsyncSmtpClient<E>,
|
client: AsyncSmtpClient<E>,
|
||||||
handle: OnceCell<E::Handle>,
|
handle: OnceLock<E::Handle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParkedConnection {
|
struct ParkedConnection {
|
||||||
@@ -30,18 +32,18 @@ struct ParkedConnection {
|
|||||||
since: Instant,
|
since: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PooledConnection<E: Executor> {
|
pub(crate) 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 fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
|
pub(crate) fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
|
||||||
let pool = Arc::new(Self {
|
let pool = Arc::new(Self {
|
||||||
config,
|
config,
|
||||||
connections: Mutex::new(Vec::new()),
|
connections: Mutex::new(Some(Vec::new())),
|
||||||
client,
|
client,
|
||||||
handle: OnceCell::new(),
|
handle: OnceLock::new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -61,6 +63,10 @@ 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()
|
||||||
@@ -79,7 +85,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) => {
|
||||||
@@ -93,6 +99,11 @@ 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")]
|
||||||
@@ -110,7 +121,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(|conn| conn.unpark()))
|
abort_concurrent(dropped.into_iter().map(ParkedConnection::unpark))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,10 +146,29 @@ impl<E: Executor> Pool<E> {
|
|||||||
pool
|
pool
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
|
pub(crate) async fn shutdown(&self) {
|
||||||
|
let connections = { self.connections.lock().await.take() };
|
||||||
|
if let Some(connections) = connections {
|
||||||
|
stream::iter(connections)
|
||||||
|
.for_each_concurrent(8, |conn| async move {
|
||||||
|
conn.unpark().abort().await;
|
||||||
|
})
|
||||||
|
.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()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,13 +212,20 @@ impl<E: Executor> Pool<E> {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("recycling connection");
|
tracing::debug!("recycling connection");
|
||||||
|
|
||||||
let mut connections = self.connections.lock().await;
|
let mut connections_guard = self.connections.lock().await;
|
||||||
if connections.len() >= self.config.max_size as usize {
|
|
||||||
drop(connections);
|
if let Some(connections) = connections_guard.as_mut() {
|
||||||
conn.abort().await;
|
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 {
|
} else {
|
||||||
let conn = ParkedConnection::park(conn);
|
// The pool has already been shut down
|
||||||
connections.push(conn);
|
drop(connections_guard);
|
||||||
|
conn.abort().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +238,13 @@ impl<E: Executor> Debug for Pool<E> {
|
|||||||
.field(
|
.field(
|
||||||
"connections",
|
"connections",
|
||||||
&match self.connections.try_lock() {
|
&match self.connections.try_lock() {
|
||||||
Some(connections) => format!("{} connections", connections.len()),
|
Some(connections) => {
|
||||||
|
if let Some(connections) = connections.as_ref() {
|
||||||
|
format!("{} connections", connections.len())
|
||||||
|
} else {
|
||||||
|
"SHUT DOWN".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
None => "LOCKED".to_owned(),
|
None => "LOCKED".to_owned(),
|
||||||
},
|
},
|
||||||
@@ -223,14 +266,16 @@ impl<E: Executor> Drop for Pool<E> {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("dropping Pool");
|
tracing::debug!("dropping Pool");
|
||||||
|
|
||||||
let connections = mem::take(self.connections.get_mut());
|
let connections = self.connections.get_mut().take();
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
|
if let Some(connections) = connections {
|
||||||
|
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
pub mod async_impl;
|
pub(super) mod async_impl;
|
||||||
pub mod sync_impl;
|
pub(super) mod sync_impl;
|
||||||
|
|
||||||
/// Configuration for a connection pool
|
/// Configuration for a connection pool
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Debug},
|
fmt::{self, Debug},
|
||||||
mem,
|
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
sync::{Arc, Mutex, TryLockError},
|
sync::{mpsc, Arc, Mutex, TryLockError},
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
@@ -11,11 +10,12 @@ use super::{
|
|||||||
super::{client::SmtpConnection, Error},
|
super::{client::SmtpConnection, Error},
|
||||||
PoolConfig,
|
PoolConfig,
|
||||||
};
|
};
|
||||||
use crate::transport::smtp::transport::SmtpClient;
|
use crate::transport::smtp::{error, transport::SmtpClient};
|
||||||
|
|
||||||
pub struct Pool {
|
pub(crate) struct Pool {
|
||||||
config: PoolConfig,
|
config: PoolConfig,
|
||||||
connections: Mutex<Vec<ParkedConnection>>,
|
connections: Mutex<Option<Vec<ParkedConnection>>>,
|
||||||
|
thread_terminator: mpsc::SyncSender<()>,
|
||||||
client: SmtpClient,
|
client: SmtpClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,16 +24,19 @@ struct ParkedConnection {
|
|||||||
since: Instant,
|
since: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PooledConnection {
|
pub(crate) struct PooledConnection {
|
||||||
conn: Option<SmtpConnection>,
|
conn: Option<SmtpConnection>,
|
||||||
pool: Arc<Pool>,
|
pool: Arc<Pool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pool {
|
impl Pool {
|
||||||
pub fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
|
pub(crate) fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
|
||||||
|
let (thread_tx, thread_rx) = mpsc::sync_channel(1);
|
||||||
|
|
||||||
let pool = Arc::new(Self {
|
let pool = Arc::new(Self {
|
||||||
config,
|
config,
|
||||||
connections: Mutex::new(Vec::new()),
|
connections: Mutex::new(Some(Vec::new())),
|
||||||
|
thread_terminator: thread_tx,
|
||||||
client,
|
client,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,6 +57,10 @@ 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()
|
||||||
@@ -72,7 +79,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) => {
|
||||||
@@ -86,6 +93,11 @@ 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")]
|
||||||
@@ -109,7 +121,15 @@ impl Pool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::sleep(idle_timeout);
|
drop(pool);
|
||||||
|
|
||||||
|
match thread_rx.recv_timeout(idle_timeout) {
|
||||||
|
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||||
|
// The transport was shut down
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.expect("couldn't spawn the Pool thread");
|
.expect("couldn't spawn the Pool thread");
|
||||||
@@ -118,10 +138,25 @@ impl Pool {
|
|||||||
pool
|
pool
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
|
pub(crate) fn shutdown(&self) {
|
||||||
|
let connections = { self.connections.lock().unwrap().take() };
|
||||||
|
if let Some(connections) = connections {
|
||||||
|
for conn in connections {
|
||||||
|
conn.unpark().abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.thread_terminator.try_send(());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
|
||||||
loop {
|
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()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,13 +200,20 @@ impl Pool {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("recycling connection");
|
tracing::debug!("recycling connection");
|
||||||
|
|
||||||
let mut connections = self.connections.lock().unwrap();
|
let mut connections_guard = self.connections.lock().unwrap();
|
||||||
if connections.len() >= self.config.max_size as usize {
|
|
||||||
drop(connections);
|
if let Some(connections) = connections_guard.as_mut() {
|
||||||
conn.abort();
|
if connections.len() >= self.config.max_size as usize {
|
||||||
|
drop(connections_guard);
|
||||||
|
conn.abort();
|
||||||
|
} else {
|
||||||
|
let conn = ParkedConnection::park(conn);
|
||||||
|
connections.push(conn);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let conn = ParkedConnection::park(conn);
|
// The pool has already been shut down
|
||||||
connections.push(conn);
|
drop(connections_guard);
|
||||||
|
conn.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +226,13 @@ impl Debug for Pool {
|
|||||||
.field(
|
.field(
|
||||||
"connections",
|
"connections",
|
||||||
&match self.connections.try_lock() {
|
&match self.connections.try_lock() {
|
||||||
Ok(connections) => format!("{} connections", connections.len()),
|
Ok(connections) => {
|
||||||
|
if let Some(connections) = connections.as_ref() {
|
||||||
|
format!("{} connections", connections.len())
|
||||||
|
} else {
|
||||||
|
"SHUT DOWN".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
|
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
|
||||||
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
|
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
|
||||||
@@ -200,10 +248,11 @@ impl Drop for Pool {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("dropping Pool");
|
tracing::debug!("dropping Pool");
|
||||||
|
|
||||||
let connections = mem::take(&mut *self.connections.get_mut().unwrap());
|
if let Some(connections) = self.connections.get_mut().unwrap().take() {
|
||||||
for conn in connections {
|
for conn in connections {
|
||||||
let mut conn = conn.unpark();
|
let mut conn = conn.unpark();
|
||||||
conn.abort();
|
conn.abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use std::{
|
|||||||
fmt::{Display, Formatter, Result},
|
fmt::{Display, Formatter, Result},
|
||||||
result,
|
result,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
string::ToString,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use nom::{
|
use nom::{
|
||||||
@@ -13,13 +12,13 @@ use nom::{
|
|||||||
bytes::streaming::{tag, take_until},
|
bytes::streaming::{tag, take_until},
|
||||||
combinator::{complete, map},
|
combinator::{complete, map},
|
||||||
multi::many0,
|
multi::many0,
|
||||||
sequence::{preceded, tuple},
|
sequence::preceded,
|
||||||
IResult,
|
IResult, Parser,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::transport::smtp::{error, Error};
|
use crate::transport::smtp::{error, Error};
|
||||||
|
|
||||||
/// First digit indicates severity
|
/// The first digit indicates severity
|
||||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub enum Severity {
|
pub enum Severity {
|
||||||
@@ -132,6 +131,12 @@ impl Code {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Code> for u16 {
|
||||||
|
fn from(code: Code) -> Self {
|
||||||
|
code.detail as u16 + 10 * code.category as u16 + 100 * code.severity as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Contains an SMTP reply, with separated code and message
|
/// Contains an SMTP reply, with separated code and message
|
||||||
///
|
///
|
||||||
/// The text message is optional, only the code is mandatory
|
/// The text message is optional, only the code is mandatory
|
||||||
@@ -216,7 +221,8 @@ 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> {
|
||||||
@@ -227,7 +233,8 @@ 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> {
|
||||||
@@ -242,18 +249,20 @@ 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(tuple((
|
let (i, lines) = many0((
|
||||||
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)) =
|
||||||
tuple((parse_code, preceded(tag(" "), take_until("\r\n"))))(i)?;
|
(parse_code, preceded(tag(" "), take_until("\r\n"))).parse(i)?;
|
||||||
let (i, _) = complete(tag("\r\n"))(i)?;
|
let (i, _) = complete(tag("\r\n")).parse(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) {
|
||||||
@@ -317,6 +326,17 @@ mod test {
|
|||||||
assert_eq!(code.to_string(), "421");
|
assert_eq!(code.to_string(), "421");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_code_to_u16() {
|
||||||
|
let code = Code {
|
||||||
|
severity: Severity::TransientNegativeCompletion,
|
||||||
|
category: Category::Connections,
|
||||||
|
detail: Detail::One,
|
||||||
|
};
|
||||||
|
let c: u16 = code.into();
|
||||||
|
assert_eq!(c, 421);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_from_str() {
|
fn test_response_from_str() {
|
||||||
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
|
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
|
||||||
|
|||||||
@@ -1,17 +1,41 @@
|
|||||||
#[cfg(feature = "pool")]
|
#[cfg(feature = "pool")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::{fmt::Debug, time::Duration};
|
||||||
|
|
||||||
#[cfg(feature = "pool")]
|
#[cfg(feature = "pool")]
|
||||||
use super::pool::sync_impl::Pool;
|
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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", 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};
|
||||||
|
|
||||||
/// Sends emails using the SMTP protocol
|
/// Synchronously send emails using the SMTP protocol
|
||||||
|
///
|
||||||
|
/// `SmtpTransport` is the primary way for communicating
|
||||||
|
/// with SMTP relay servers to send email messages. It holds the
|
||||||
|
/// client connect configuration and creates new connections
|
||||||
|
/// as necessary.
|
||||||
|
///
|
||||||
|
/// # Connection pool
|
||||||
|
///
|
||||||
|
/// When the `pool` feature is enabled (default), `SmtpTransport` maintains a
|
||||||
|
/// connection pool to manage SMTP connections. The pool:
|
||||||
|
///
|
||||||
|
/// - Establishes a new connection when sending a message.
|
||||||
|
/// - Recycles connections internally after a message is sent.
|
||||||
|
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
|
||||||
|
///
|
||||||
|
/// The connection pool can grow to hold multiple SMTP connections if multiple
|
||||||
|
/// emails are sent concurrently, as SMTP does not support multiplexing within a
|
||||||
|
/// single connection.
|
||||||
|
///
|
||||||
|
/// However, **connection reuse is not possible** if the `SmtpTransport` instance
|
||||||
|
/// is dropped after every email send operation. You must reuse the instance
|
||||||
|
/// of this struct for the connection pool to be of any use.
|
||||||
|
///
|
||||||
|
/// To customize connection pool settings, use [`SmtpTransportBuilder::pool_config`].
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SmtpTransport {
|
pub struct SmtpTransport {
|
||||||
@@ -32,10 +56,23 @@ 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.quit()?;
|
conn.abort();
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shutdown(&self) {
|
||||||
|
#[cfg(feature = "pool")]
|
||||||
|
self.inner.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for SmtpTransport {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut builder = f.debug_struct("SmtpTransport");
|
||||||
|
builder.field("inner", &self.inner);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SmtpTransport {
|
impl SmtpTransport {
|
||||||
@@ -45,10 +82,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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
docsrs,
|
docsrs,
|
||||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||||
@@ -58,7 +95,7 @@ impl SmtpTransport {
|
|||||||
.tls(Tls::Wrapper(tls_parameters)))
|
.tls(Tls::Wrapper(tls_parameters)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
|
/// Simple and secure transport, using STARTTLS to obtain encrypted connections
|
||||||
///
|
///
|
||||||
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
|
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
|
||||||
/// that don't take SMTPS connections.
|
/// that don't take SMTPS connections.
|
||||||
@@ -69,10 +106,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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
docsrs,
|
docsrs,
|
||||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||||
)]
|
)]
|
||||||
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||||
@@ -95,29 +132,122 @@ impl SmtpTransport {
|
|||||||
///
|
///
|
||||||
/// * No authentication
|
/// * No authentication
|
||||||
/// * No TLS
|
/// * No TLS
|
||||||
/// * A 60 seconds timeout for smtp commands
|
/// * A 60-seconds timeout for smtp commands
|
||||||
/// * Port 25
|
/// * Port 25
|
||||||
///
|
///
|
||||||
/// Consider using [`SmtpTransport::relay`](#method.relay) or
|
/// Consider using [`SmtpTransport::relay`](#method.relay) or
|
||||||
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||||
/// if possible.
|
/// if possible.
|
||||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
|
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
|
||||||
let new = SmtpInfo {
|
SmtpTransportBuilder::new(server)
|
||||||
server: server.into(),
|
}
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
SmtpTransportBuilder {
|
/// Creates a `SmtpTransportBuilder` from a connection URL
|
||||||
info: new,
|
///
|
||||||
#[cfg(feature = "pool")]
|
/// The protocol, credentials, host, port and EHLO name can be provided
|
||||||
pool_config: PoolConfig::default(),
|
/// in a single URL. This may be simpler than having to configure SMTP
|
||||||
}
|
/// through multiple configuration parameters and then having to pass
|
||||||
|
/// those options to lettre.
|
||||||
|
///
|
||||||
|
/// The URL is created in the following way:
|
||||||
|
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
|
||||||
|
///
|
||||||
|
/// `user` (Username) and `pass` (Password) are optional in case the
|
||||||
|
/// SMTP relay doesn't require authentication. When `port` is not
|
||||||
|
/// configured it is automatically determined based on the `scheme`.
|
||||||
|
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
|
||||||
|
/// command. `TLS` controls whether STARTTLS is simply enabled
|
||||||
|
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
|
||||||
|
/// or `required` (require the server to upgrade the connection to
|
||||||
|
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
|
||||||
|
///
|
||||||
|
/// Use the following table to construct your SMTP url:
|
||||||
|
///
|
||||||
|
/// | scheme | `tls` query parameter | example | default port | remarks |
|
||||||
|
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
|
||||||
|
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
|
||||||
|
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
|
||||||
|
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
|
||||||
|
///
|
||||||
|
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
|
||||||
|
/// be concatenated to construct the final URL because special characters
|
||||||
|
/// contained within the parameter may confuse the URL decoder.
|
||||||
|
/// Manually URL encode the parameters before concatenating them or use
|
||||||
|
/// a proper URL encoder, like the following cargo script:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # let _ = r#"
|
||||||
|
/// #!/usr/bin/env cargo
|
||||||
|
///
|
||||||
|
/// //! ```cargo
|
||||||
|
/// //! [dependencies]
|
||||||
|
/// //! url = "2"
|
||||||
|
/// //! ```
|
||||||
|
/// # "#;
|
||||||
|
///
|
||||||
|
/// use url::Url;
|
||||||
|
///
|
||||||
|
/// fn main() {
|
||||||
|
/// // don't touch this line
|
||||||
|
/// let mut url = Url::parse("foo://bar").unwrap();
|
||||||
|
///
|
||||||
|
/// // configure the scheme (`smtp` or `smtps`) here.
|
||||||
|
/// url.set_scheme("smtps").unwrap();
|
||||||
|
/// // configure the username and password.
|
||||||
|
/// // remove the following two lines if unauthenticated.
|
||||||
|
/// url.set_username("username").unwrap();
|
||||||
|
/// url.set_password(Some("password")).unwrap();
|
||||||
|
/// // configure the hostname
|
||||||
|
/// url.set_host(Some("smtp.example.com")).unwrap();
|
||||||
|
/// // configure the port - only necessary if using a non-default port
|
||||||
|
/// url.set_port(Some(465)).unwrap();
|
||||||
|
/// // configure the EHLO name
|
||||||
|
/// url.set_path("ehlo-name");
|
||||||
|
///
|
||||||
|
/// println!("{url}");
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The connection URL can then be used in the following way:
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use lettre::{
|
||||||
|
/// message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||||
|
/// SmtpTransport, Transport,
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// let email = Message::builder()
|
||||||
|
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
|
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
|
/// .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
|
/// .subject("Happy new year")
|
||||||
|
/// .header(ContentType::TEXT_PLAIN)
|
||||||
|
/// .body(String::from("Be happy!"))
|
||||||
|
/// .unwrap();
|
||||||
|
///
|
||||||
|
/// // Open a remote connection to example
|
||||||
|
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
|
||||||
|
///
|
||||||
|
/// // Send the email
|
||||||
|
/// mailer.send(&email)?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[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 from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||||
|
super::connection_url::from_connection_url(connection_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tests the SMTP connection
|
/// Tests the SMTP connection
|
||||||
///
|
///
|
||||||
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
||||||
/// The connection is closed afterwards if a connection pool is not used.
|
/// The connection is closed afterward if a connection pool is not used.
|
||||||
pub fn test_connection(&self) -> Result<bool, Error> {
|
pub fn test_connection(&self) -> Result<bool, Error> {
|
||||||
let mut conn = self.inner.connection()?;
|
let mut conn = self.inner.connection()?;
|
||||||
|
|
||||||
@@ -141,13 +271,27 @@ pub struct SmtpTransportBuilder {
|
|||||||
|
|
||||||
/// Builder for the SMTP `SmtpTransport`
|
/// Builder for the SMTP `SmtpTransport`
|
||||||
impl SmtpTransportBuilder {
|
impl SmtpTransportBuilder {
|
||||||
|
// Create new builder with default parameters
|
||||||
|
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
|
||||||
|
let new = SmtpInfo {
|
||||||
|
server: server.into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
info: new,
|
||||||
|
#[cfg(feature = "pool")]
|
||||||
|
pool_config: PoolConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the name used during EHLO
|
/// Set the name used during EHLO
|
||||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||||
self.info.hello_name = name;
|
self.info.hello_name = name;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the authentication mechanism to use
|
/// Set the authentication credentials 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
|
||||||
@@ -166,16 +310,38 @@ 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-tls", feature = "boring-tls")))
|
doc(cfg(any(feature = "native-tls", feature = "rustls", 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;
|
||||||
@@ -194,7 +360,7 @@ impl SmtpTransportBuilder {
|
|||||||
|
|
||||||
/// Build the transport
|
/// Build the transport
|
||||||
///
|
///
|
||||||
/// If the `pool` feature is enabled an `Arc` wrapped pool is be created.
|
/// If the `pool` feature is enabled, an `Arc` wrapped pool is created.
|
||||||
/// Defaults can be found at [`PoolConfig`]
|
/// Defaults can be found at [`PoolConfig`]
|
||||||
pub fn build(self) -> SmtpTransport {
|
pub fn build(self) -> SmtpTransport {
|
||||||
let client = SmtpClient { info: self.info };
|
let client = SmtpClient { info: self.info };
|
||||||
@@ -208,7 +374,7 @@ impl SmtpTransportBuilder {
|
|||||||
|
|
||||||
/// Build client
|
/// Build client
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SmtpClient {
|
pub(super) struct SmtpClient {
|
||||||
info: SmtpInfo,
|
info: SmtpInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,11 +382,11 @@ 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 fn connection(&self) -> Result<SmtpConnection, Error> {
|
pub(super) 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-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
Tls::Wrapper(tls_parameters) => Some(tls_parameters),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,14 +399,14 @@ impl SmtpClient {
|
|||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
match self.info.tls {
|
match &self.info.tls {
|
||||||
Tls::Opportunistic(ref tls_parameters) => {
|
Tls::Opportunistic(tls_parameters) => {
|
||||||
if conn.can_starttls() {
|
if conn.can_starttls() {
|
||||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tls::Required(ref tls_parameters) => {
|
Tls::Required(tls_parameters) => {
|
||||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
@@ -252,3 +418,78 @@ impl SmtpClient {
|
|||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
transport::smtp::{authentication::Credentials, client::Tls},
|
||||||
|
SmtpTransport,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transport_from_url() {
|
||||||
|
let builder = SmtpTransport::from_url("smtp://127.0.0.1:2525").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(builder.info.port, 2525);
|
||||||
|
assert!(matches!(builder.info.tls, Tls::None));
|
||||||
|
assert_eq!(builder.info.server, "127.0.0.1");
|
||||||
|
|
||||||
|
let builder =
|
||||||
|
SmtpTransport::from_url("smtps://username:password@smtp.example.com:465").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(builder.info.port, 465);
|
||||||
|
assert_eq!(
|
||||||
|
builder.info.credentials,
|
||||||
|
Some(Credentials::new(
|
||||||
|
"username".to_owned(),
|
||||||
|
"password".to_owned()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||||
|
assert_eq!(builder.info.server, "smtp.example.com");
|
||||||
|
|
||||||
|
let builder = SmtpTransport::from_url(
|
||||||
|
"smtps://user%40example.com:pa$$word%3F%22!@smtp.example.com:465",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(builder.info.port, 465);
|
||||||
|
assert_eq!(
|
||||||
|
builder.info.credentials,
|
||||||
|
Some(Credentials::new(
|
||||||
|
"user@example.com".to_owned(),
|
||||||
|
"pa$$word?\"!".to_owned()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||||
|
assert_eq!(builder.info.server, "smtp.example.com");
|
||||||
|
|
||||||
|
let builder =
|
||||||
|
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(builder.info.port, 587);
|
||||||
|
assert_eq!(
|
||||||
|
builder.info.credentials,
|
||||||
|
Some(Credentials::new(
|
||||||
|
"username".to_owned(),
|
||||||
|
"password".to_owned()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
assert!(matches!(builder.info.tls, Tls::Required(_)));
|
||||||
|
|
||||||
|
let builder = SmtpTransport::from_url(
|
||||||
|
"smtp://username:password@smtp.example.com:587?tls=opportunistic",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(builder.info.port, 587);
|
||||||
|
assert!(matches!(builder.info.tls, Tls::Opportunistic(_)));
|
||||||
|
|
||||||
|
let builder = SmtpTransport::from_url("smtps://smtp.example.com").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(builder.info.port, 465);
|
||||||
|
assert_eq!(builder.info.credentials, None);
|
||||||
|
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 struct XText<'a>(pub &'a str);
|
pub(crate) struct XText<'a>(pub(crate) &'a str);
|
||||||
|
|
||||||
impl<'a> Display for XText<'a> {
|
impl Display for XText<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
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,9 +38,7 @@ 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
//! ```rust
|
//! ```rust
|
||||||
//! # #[cfg(feature = "builder")]
|
//! # #[cfg(feature = "builder")]
|
||||||
//! # {
|
//! # {
|
||||||
//! use lettre::{transport::stub::StubTransport, Message, Transport};
|
//! use lettre::{
|
||||||
|
//! 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>> {
|
||||||
@@ -20,11 +22,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();
|
||||||
//! let result = sender.send(&email);
|
//! sender.send(&email)?;
|
||||||
//! assert!(result.is_ok());
|
|
||||||
//! assert_eq!(
|
//! assert_eq!(
|
||||||
//! sender.messages(),
|
//! sender.messages(),
|
||||||
//! vec![(
|
//! vec![(
|
||||||
|
|||||||
Reference in New Issue
Block a user