Compare commits

..

122 Commits

Author SHA1 Message Date
Paolo Barbolini
e347cf9ae2 fix 2025-07-05 18:09:14 +02:00
Paolo Barbolini
4c738085e5 clippy 2025-07-05 17:37:35 +02:00
Paolo Barbolini
b752170bd8 refactor: replace chumsky with nom 2025-07-05 17:24:42 +02:00
Paolo Barbolini
b073df7666 build(deps): upgrade socket2 to v0.6 (#1098) 2025-07-05 12:09:17 +02:00
Paolo Barbolini
cf6b767a9c Prepare v0.11.17 (#1093) 2025-06-05 21:41:17 +02:00
Paolo Barbolini
d3d8e24824 feat: add rustls-platform-verifier support (#1081) 2025-06-02 10:43:17 +02:00
Paolo Barbolini
c4df9730aa refactor(smtp/pool): remove duplicate abort_concurrent implementation (#1092) 2025-05-24 16:37:54 +02:00
Paolo Barbolini
bfed19e6ad refactor(stub): always use std Mutex (#1091) 2025-05-24 14:34:09 +00:00
David Campbell
629967ac98 docs: use Mailbox::new rather than string parsing (#1090) 2025-05-24 16:21:15 +02:00
Paolo Barbolini
06e381ec9c Prepare v0.11.16 (#1089) 2025-05-12 11:16:14 +02:00
Paolo Barbolini
d9ce9a6e47 chore: deprecate ungated TLS types _when_ no TLS backend is enabled (#1084) 2025-05-11 09:36:47 +02:00
Paolo Barbolini
e892b55b6b build(deps): upgrade webpki-roots to v1 (#1088) 2025-05-06 12:37:39 +00:00
Paolo Barbolini
771d212198 build: gate web-time behind cfg(target_arch = "wasm32") (#1086) 2025-05-01 18:32:26 +02:00
Paolo Barbolini
83ba93944d docs: add missing doc(cfg(...)) attributes (#1085) 2025-05-01 18:16:40 +02:00
Paolo Barbolini
de3ab006e2 fix: feature gate internal TransportBuilder::tls to avoid recursive call site (#1083) 2025-05-01 15:09:07 +02:00
Paolo Barbolini
9504b7f45c refactor: cleanup internal TlsParameters and (Async)NetworkStream config (#1082) 2025-05-01 14:00:56 +02:00
dependabot[bot]
c91b356a96 build(deps): bump tokio from 1.44.1 to 1.44.2 (#1080)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.1 to 1.44.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.1...tokio-1.44.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.44.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 06:57:33 +02:00
dependabot[bot]
118c1ad47f build(deps): bump openssl from 0.10.71 to 0.10.72 (#1079)
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.71 to 0.10.72.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-06 11:49:28 +02:00
Paolo Barbolini
8bf4d3a9c1 style: fix clippy::io_other_error (#1078) 2025-04-03 10:28:06 +00:00
Paolo Barbolini
1fcff673ba fix: remove E: Clone bound from AsyncFileTransport Clone impl (#1075) 2025-04-03 08:05:39 +00:00
Paolo Barbolini
8c70c0cfb4 build(deps): upgrade semver compatible dependencies (#1076) 2025-04-03 07:54:06 +00:00
Paolo Barbolini
63d8d30088 fix: let cannot be used for global variables (#1077) 2025-04-03 09:50:06 +02:00
Paolo Barbolini
6c0be84817 Prepare v0.11.15 (#1070) 2025-03-10 17:26:17 +01:00
Paolo Barbolini
6059cb04d6 build(deps): upgrade email-encoding to v0.4 (#1069) 2025-03-09 07:22:20 +00:00
Paolo Barbolini
fdf0346556 style: fix rustdoc::broken_intra_doc_links (#1068) 2025-03-09 07:55:15 +01:00
Paolo Barbolini
0f9455715c build(deps): upgrade semver compatible locked dependencies (#1067) 2025-03-08 11:52:38 +00:00
Popax21
0b3a1ed278 feat: add controlled shutdown methods (#1045) 2025-03-08 12:43:05 +01:00
Paolo Barbolini
76bf68268f build(deps): bump minimum supported serde to v1.0.110 (#1064) 2025-03-04 13:06:17 +01:00
Paolo Barbolini
99a86c0fac build(deps): bump minimum supported rustls to v0.23.18 (#1063) 2025-03-04 12:55:43 +01:00
Paolo Barbolini
f0de9ef02c style: deny unreachable_pub lint (#1058) 2025-02-23 10:17:17 +01:00
Paolo Barbolini
b4ddcbdcfc build: bump MSRV to 1.74 (#1060) 2025-02-23 10:16:57 +01:00
Paolo Barbolini
1e22bcd527 Prepare v0.11.14 (#1056) 2025-02-23 10:06:28 +01:00
Paolo Barbolini
75716ca269 feat: make crypto and TLS certificate verify backends opt-in (#1054) 2025-02-23 09:32:47 +01:00
Paolo Barbolini
8a6f1dab0e deprecate: having made AsyncNetworkStream public (#1059) 2025-02-22 21:22:30 +01:00
Paolo Barbolini
621853e2e3 chore(license): bump my copyright year (#1057) 2025-02-22 20:27:07 +01:00
Paolo Barbolini
5e4cb2d1b5 fix: use the same rustls crypto provider everywhere (#1055) 2025-02-22 19:14:28 +01:00
Paolo Barbolini
b4abd40698 style: fix rustls-native-tls warnings when tracing is disabled (#1053) 2025-02-22 17:39:39 +00:00
Paolo Barbolini
2d1ccda2ef style(clippy): ban direct use of std::time::SystemTime::now (#1043) 2025-02-22 08:32:38 +00:00
Paolo Barbolini
54934e1492 build(deps): drop direct dependency on rustls-pki-types (#1051) 2025-02-22 08:07:23 +00:00
Paolo Barbolini
cfa29743a8 refactor: replace rustls-pemfile with rustls-pki-types (#1050) 2025-02-22 08:59:14 +01:00
Paolo Barbolini
4a4a96d805 refactor: remove artifact from web-time refactor (#1049) 2025-02-22 07:44:56 +00:00
Paolo Barbolini
f0b8052a52 build(deps): upgrade nom to v8 (#1048) 2025-02-22 08:36:56 +01:00
Paolo Barbolini
655cd8a140 style: cleanup Cargo.toml (#1047) 2025-02-22 08:30:01 +01:00
Paolo Barbolini
dabc88a053 Prepare v0.11.13 (#1044) 2025-02-17 11:48:42 +01:00
Paolo Barbolini
9cdefcea09 refactor: simplify handling of WASM web-time (#1042) 2025-02-17 09:05:22 +00:00
Abid Omar
5748af4c98 feat: add WASM support via web-time (#1037)
Support WASM environments by using web-time.
This was tested on a Cloudflare worker environment.
2025-02-17 09:53:19 +01:00
André Cruz
3e9b1876d9 feat: add method to obtain TLS result (#1039)
Some TLS toolkits export a result that can be checked afterwards even if
the TLS negotation returned successfully. This can be used for example
if you disabled certificate checks by default, but then want to check
the outcome.

Currently this is only supported on boring TLS.
2025-02-17 09:51:45 +01:00
Popax21
795bedae76 fix: synchronous pool shutdowns being arbitrarily delayed (#1041)
Previously, the connection pool thread did not drop its upgraded `Arc` pool reference while sleeping until the next idle duration check. This causes a drop of the `SmtpTransport` to not shut down any connections until said thread wakes up again (since it still holds a reference to the pool), which can take up to 60s with default settings. In practice, this means that connections will most likely not be properly closed before the program exists, (since the `SmtpTransport` is most likely dropped when the program shuts down) which violates the SMTP specification which states that:
> The sender MUST NOT intentionally close the channel until it sends a QUIT command, and it SHOULD wait until it receives the reply (even if there was an error response to a command).
2025-02-07 08:29:06 +01:00
dependabot[bot]
891dd521ab build(deps): bump openssl from 0.10.68 to 0.10.70 (#1038)
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.68 to 0.10.70.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.68...openssl-v0.10.70)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-03 20:51:45 +01:00
Paolo Barbolini
0fb89e23ad Prepare v0.11.12 (#1034) 2025-02-02 14:05:41 +01:00
Paolo Barbolini
097f7d5aaa docs: fix broken doc link (#1036) 2025-02-02 13:49:52 +01:00
Paolo Barbolini
32e066464a docs: improve SMTP transport from_url (#1032) 2025-02-02 13:38:03 +01:00
Paolo Barbolini
55c7f57f25 docs: improve transport::smtp (#1031) 2025-02-02 13:37:25 +01:00
Paolo Barbolini
3f7a57a417 docs: replace assert! with ? operator for send examples (#1033) 2025-02-02 13:33:34 +01:00
Paolo Barbolini
bb64baec67 style: warn on more pedantic clippy lints and fix them (#1035) 2025-02-02 12:50:15 +01:00
Paolo Barbolini
5f13636b49 docs: document OpenSSL system dependencies (#1030) 2025-02-01 22:14:14 +01:00
Paolo Barbolini
4513e602d6 docs: fix credentials copy-paste error (#1019) 2024-12-25 09:05:34 +01:00
Paolo Barbolini
382e15013a docs: document (Async)SmtpTransport (#1018) 2024-12-25 09:05:24 +01:00
Paolo Barbolini
3ce31c5a6a docs: add missing ContentType to email building (#1017) 2024-12-23 22:08:08 +01:00
Paolo Barbolini
a48cf92a5b docs: fix rustdoc warnings (#1016) 2024-12-23 17:19:24 +01:00
Paolo Barbolini
43f6f139d2 docs: improve docs for Tls enum (#1015) 2024-12-23 17:01:24 +01:00
Paolo Barbolini
fd1425666d docs: warn against manually configuring port and tls configs (#1014) 2024-12-23 16:43:39 +01:00
Paolo Barbolini
de075153b0 Prepare v0.11.11 (#1013) 2024-12-05 20:48:10 +01:00
Paolo Barbolini
02dfc7dd4a fix: off-by-one error reaching the min number of pooled connections (#1012) 2024-12-05 19:19:54 +01:00
Paolo Barbolini
83ce5872d7 Fix some clippy warnings (#1009) 2024-11-28 15:48:08 +00:00
Paolo Barbolini
272efeca74 build(deps): bump locked dependencies (#1006) 2024-11-27 10:21:13 +01:00
Paolo Barbolini
ec6f5f3920 build: bump MSRV to 1.71 (#1008) 2024-11-27 10:15:59 +01:00
Paolo Barbolini
b62d23bd87 build: fix -Zminimal-versions build (#1007) 2024-11-27 10:11:49 +01:00
Paolo Barbolini
51794aa912 Prepare v0.11.10 (#1002) 2024-10-23 23:04:45 +02:00
Paolo Barbolini
eb42651401 Use case insensitive comparisons for login challenge requests (#1000) 2024-10-23 20:01:00 +02:00
Paolo Barbolini
99c6dc2a87 Replace quit with abort in transport connection drop code (#999) 2024-10-23 20:00:31 +02:00
Paolo Barbolini
b6babbce00 Prepare v0.11.9 (#991) 2024-09-13 15:48:32 +02:00
Paolo Barbolini
c9895c52de readme: add fn main to getting started example (#990) 2024-09-13 15:41:46 +02:00
Paolo Barbolini
575492b9ed Bump rustls-native-certs to v0.8 (#992) 2024-09-13 15:41:18 +02:00
Paolo Barbolini
ad665cd01e chore: bump locked dependencies (#989)
And then downgrades:

cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
cargo update -p anstyle --precise 1.0.2
2024-09-13 02:37:04 +02:00
Arnaud de Bossoreille
e2ac5dadfb Fix parsing Mailbox with spaces (#986) 2024-09-12 17:26:43 +02:00
Max Breitenfeldt
1c6a348eb8 Enable accept_invalid_hostnames for rustls (#988)
With #977 dangerous_accept_invalid_hostnames is implemented for rustls. This add the config flag so that the feature can actually be used when rustls-tls is enabled.
2024-09-10 09:58:48 +02:00
Paolo Barbolini
e8b2498ad7 Prepare v0.11.8 (#985) 2024-09-03 15:43:04 +02:00
Paolo Barbolini
bf48bd6b96 ci: bump dependencies (#984) 2024-09-02 17:37:44 +02:00
André Cruz
fa6191983a feat(tls-peer-certificates): Provide peer certs (#976)
Add a method that, when using a TLS toolkit that supports it, returns
the entire certificate chain. This is useful, for example, when
implementing DANE support which has directives that apply to the issuer
and not just to the leaf certificate.

Not all TLS toolkits support this, so the code will panic if the method
is called when using a TLS toolkit that has no way to return these
certificates.
2024-09-02 17:26:46 +02:00
Paolo Barbolini
ca405040ae chore: replace manual impl of #[non_exhaustive] for InvalidHeaderName (#981) 2024-08-29 05:43:23 +02:00
Paolo Barbolini
f7a1b790df Make HeaderName comparisons case insensitive (#980) 2024-08-29 05:43:14 +02:00
Paolo Barbolini
caff354cbf chore: bump vulnerable dependencies 2024-08-23 09:28:21 +02:00
Paolo Barbolini
a81401c4cb Fix latest clippy warnings (#979) 2024-08-23 09:24:05 +02:00
Jonas Osburg
54df594d6c Implement accept_invalid_hostnames for rustls (#977)
Fixes #957

Co-authored-by: Paolo Barbolini <paolo.barbolini@m4ss.net>
2024-08-21 10:29:10 +02:00
Felix Rodemund
cada01d039 Add mTLS Support (#974)
This adds support for mutual authentication to transport layer secured
connections used to deliver mails.

Client authentication requires the client certificate and the
corresponding private key in pem format to be passed to
Identity::from_pem. The resulting Identity needs then to be provided to
TlsParametersBuilder::identify_with.
2024-07-30 21:28:34 +02:00
Paolo Barbolini
0132bee59d Bump idna to v1 (#966) 2024-06-11 18:12:49 +02:00
Paolo Barbolini
acdf189717 Fix clippy warnings (#967) 2024-06-11 18:12:32 +02:00
Ilka Schulz
3aea65315f resolve #806: forbid empty forward path when deserializing address::Envelop (#964) 2024-06-11 09:48:30 +00:00
Paolo Barbolini
9d3ebfab1a Prepare 0.11.7 (#961) 2024-04-23 16:04:19 +02:00
rezabet
6fb69086fb style: Maintain message consistency (#960) 2024-04-21 16:10:32 +02:00
Paolo Barbolini
dfdf3a61d2 Drop ref syntax (#959) 2024-04-21 11:22:07 +02:00
dependabot[bot]
e30ac2dbff Bump rustls from 0.23.3 to 0.23.5 (#958)
Bumps [rustls](https://github.com/rustls/rustls) from 0.23.3 to 0.23.5.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.23.3...v/0.23.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-21 10:28:08 +02:00
Paolo Barbolini
22dca340a7 Bump hostname to v0.4 (#956) 2024-04-01 21:36:57 +02:00
Paolo Barbolini
c7d1f35676 Prepare 0.11.6 (#955) 2024-03-28 16:19:10 +01:00
Paolo Barbolini
eebea56f16 Upgrade email-encoding to v0.3 (#952) 2024-03-28 15:22:55 +01:00
Paolo Barbolini
851d6ae164 Bump license year (#954) 2024-03-28 14:50:53 +01:00
Paolo Barbolini
6f38e6b9a9 Fix latest clippy warnings (#953) 2024-03-28 05:12:13 +01:00
Paolo Barbolini
c40af78809 Prepare 0.11.5 (#951) 2024-03-25 17:59:47 +01:00
Paolo Barbolini
6d2e0d5046 Bump rustls to v0.23 (#950) 2024-03-22 20:09:27 +01:00
dependabot[bot]
c64cb0ff2e Bump mio from 0.8.10 to 0.8.11 (#946)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 22:38:54 +01:00
Paolo Barbolini
10d7b197ed chore(cargo): bump base64 to v0.22 (#945) 2024-03-02 11:34:57 +01:00
Viktor Szépe
fb54855d5f Fix typos (#944) 2024-02-21 14:49:42 +01:00
ciffelia
157c4fb5ae docs(transport): fix error in "Available transports" table (#943) 2024-02-18 17:19:27 +01:00
Hodu Mayo
1196e332ee feat(transport-smtp): Support to SASL draft login challenge (#911) 2024-02-14 21:18:56 +01:00
Alexis Mousset
75770f7bc6 Add conversion from SMTP code to integer (#941) 2024-02-14 21:01:21 +01:00
Alexis Mousset
76d0929c94 Add a Cargo.lock (#942) 2024-02-14 20:50:54 +01:00
Paolo Barbolini
c3d00051b2 Prepare 0.11.4 (#936) 2024-01-28 08:08:26 +01:00
Birk Tjelmeland
12580d82f4 style(email): Change Part::body_raw to Part::format_body 2024-01-28 07:54:16 +01:00
Birk Tjelmeland
f7849078b8 fix(email): Fix mimebody DKIM body-hash computation 2024-01-28 07:54:16 +01:00
Paolo Barbolini
f2c94cdf4d chore(cargo): bump maud to v0.26 (#935) 2024-01-25 20:08:32 +01:00
Paolo Barbolini
74f64b81ab test(transport/smtp): test credentials percent decoding from URL (#934) 2024-01-25 20:05:31 +01:00
42triangles
39c71dbfd2 transport/smtp: percent decode credentials in URL (#932) 2024-01-12 11:43:30 +00:00
Paolo Barbolini
c1bf5dfda1 Prepare 0.11.3 (#929) 2024-01-02 18:45:34 +01:00
Paolo Barbolini
1c1fef8055 Drop once_cell dependency in favor of OnceLock from std (#928) 2024-01-02 11:53:47 +01:00
Paolo Barbolini
1540f16015 Upgrade rustls to v0.22 (#921) 2024-01-02 11:41:16 +01:00
Tobias Bieniek
330daa1173 transport/smtp: Implement Debug trait (#925) 2023-12-17 09:20:51 +01:00
Tobias Bieniek
47f2fe0750 transport/file: Derive Clone impls (#924) 2023-12-16 21:30:57 +01:00
Paolo Barbolini
8b6cee30ee Prepare 0.11.2 (#919) 2023-11-23 09:49:21 +01:00
Paolo Barbolini
62c16e90ef Bump idna to v0.5 (#918) 2023-11-23 08:23:25 +00:00
Paolo Barbolini
e0494a5f9d Bump boringssl crates to v4 (#915) 2023-11-19 11:49:43 +01:00
Paolo Barbolini
8c3bffa728 Bump MSRV to 1.70 (#916) 2023-11-19 11:42:49 +01:00
53 changed files with 5617 additions and 1114 deletions

View File

@@ -13,16 +13,16 @@ env:
jobs:
rustfmt:
name: rustfmt / nightly-2023-06-22
name: rustfmt / nightly-2024-09-01
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: |
rustup default nightly-2023-06-22
rustup default nightly-2024-09-01
rustup component add rustfmt
- name: cargo fmt
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: |
@@ -50,19 +50,19 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: rustup update --no-self-update stable
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install cargo hack
run: cargo install cargo-hack --debug
- name: Check with cargo hack
run: cargo hack check --feature-powerset --depth 3
run: cargo hack check --feature-powerset --depth 3 --at-least-one-of aws-lc-rs,ring --at-least-one-of rustls-native-certs,webpki-roots
test:
name: test / ${{ matrix.name }}
@@ -75,12 +75,12 @@ jobs:
rust: stable
- name: beta
rust: beta
- name: 1.65.0
rust: 1.65.0
- name: '1.74'
rust: '1.74'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: |
@@ -90,7 +90,7 @@ jobs:
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install postfix
- name: Install postfix
run: |
DEBIAN_FRONTEND=noninteractive sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix
@@ -112,12 +112,6 @@ jobs:
- name: Install dkimverify
run: sudo apt -y install python3-dkim
- name: Work around early dependencies MSRV bump
run: |
cargo update -p anstyle --precise 1.0.2
cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
- name: Test with no default features
run: cargo test --no-default-features
@@ -125,16 +119,16 @@ jobs:
run: cargo test
- name: Test with all features (-native-tls)
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tracing
run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls,tracing
- name: Test with all features (-boring-tls)
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls-tls,tracing
run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls,tracing
# coverage:
# name: Coverage
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - uses: actions/checkout@v4
# - uses: actions-rs/toolchain@v1
# with:
# toolchain: nightly

1
.gitignore vendored
View File

@@ -4,4 +4,3 @@
lettre.sublime-*
lettre.iml
target/
/Cargo.lock

View File

@@ -1,3 +1,353 @@
<a name="v0.11.17"></a>
### v0.11.17 (2025-06-06)
#### Features
* Add support for `rustls-platform-verifier` ([#1081])
#### Misc
* Change readme example to use `Mailbox::new` instead of string parsing ([#1090])
* Replace futures-util `Mutex` with std `Mutex` in `AsyncStubTransport` ([#1091])
* Avoid duplicate `abort_concurrent` implementation ([#1092])
[#1081]: https://github.com/lettre/lettre/pull/1081
[#1090]: https://github.com/lettre/lettre/pull/1090
[#1091]: https://github.com/lettre/lettre/pull/1091
[#1092]: https://github.com/lettre/lettre/pull/1092
<a name="v0.11.16"></a>
### v0.11.16 (2025-05-12)
#### Features
* Always implement `Clone` for `AsyncFileTransport` ([#1075])
#### Changes
* `Tls`, `CertificateStore`, `TlsParameters`, `TlsParametersBuilder`, `Certificate` and `Identity`
are now marked as deprecated when no TLS backend is enabled. They will be properly feature gated
in lettre v0.12 ([#1084])
#### Misc
* Gate `web-time` behind `cfg(target_arch = "wasm32")]` ([#1086])
* Add missing `#[doc(cfg(...))]` attributes ([#1086])
* Upgrade `webpki-roots` to v1 ([#1088])
* Cleanup internal `TlsParameters` and `(Async)NetworkStream` structures ([#1082])
* Feature gate internal `TransportBuilder::tls` to avoid recursive call site warnings ([#1083])
* Fix workaround for embedding cargo script in rustdoc output ([#1077])
* Fix `clippy::io_other_error` warnings ([#1078])
* Upgrade semver compatible dependencies ([#1076], [#1079], [#1080])
[#1075]: https://github.com/lettre/lettre/pull/1075
[#1076]: https://github.com/lettre/lettre/pull/1076
[#1077]: https://github.com/lettre/lettre/pull/1077
[#1078]: https://github.com/lettre/lettre/pull/1078
[#1079]: https://github.com/lettre/lettre/pull/1079
[#1080]: https://github.com/lettre/lettre/pull/1080
[#1082]: https://github.com/lettre/lettre/pull/1082
[#1083]: https://github.com/lettre/lettre/pull/1083
[#1084]: https://github.com/lettre/lettre/pull/1084
[#1086]: https://github.com/lettre/lettre/pull/1086
[#1088]: https://github.com/lettre/lettre/pull/1088
<a name="v0.11.15"></a>
### v0.11.15 (2025-03-10)
#### Upgrade notes
* MSRV is now 1.74 ([#1060])
#### Features
* Add controlled shutdown methods ([#1045], [#1068])
#### Misc
* Deny `unreachable_pub` lint ([#1058])
* Bump minimum supported `rustls` ([#1063])
* Bump minimum supported `serde` ([#1064])
* Upgrade semver compatible dependencies ([#1067])
* Upgrade `email-encoding` to v0.4 ([#1069])
[#1045]: https://github.com/lettre/lettre/pull/1045
[#1058]: https://github.com/lettre/lettre/pull/1058
[#1060]: https://github.com/lettre/lettre/pull/1060
[#1063]: https://github.com/lettre/lettre/pull/1063
[#1064]: https://github.com/lettre/lettre/pull/1064
[#1067]: https://github.com/lettre/lettre/pull/1067
[#1068]: https://github.com/lettre/lettre/pull/1068
[#1069]: https://github.com/lettre/lettre/pull/1069
<a name="v0.11.14"></a>
### v0.11.14 (2025-02-23)
This release deprecates the `rustls-tls`, `tokio1-rustls-tls` and `async-std1-rustls-tls`
features, which will be removed in lettre v0.12.
rustls users should start migrating to the `rustls`, `tokio1-rustls` and
`async-std1-rustls` features. Unlike the deprecated _*rustls-tls_ features,
which automatically enabled the `ring` and `webpki-roots` backends, the new
features do not. To complete the migration, users must either enable the
`aws-lc-rs` or the `ring` feature. Additionally, those who rely on `webpki-roots`
for TLS certificate verification must now explicitly enable its feature.
Users of `rustls-native-certs` do not need to enable `webpki-roots`.
Find out more about the new features via the [lettre rustls docs].
#### Features
* Make it possible to use different `rustls` crypto providers and TLS verifiers ([#1054])
#### Bug fixes
* Use the same `rustls` crypto provider everywhere ([#1055])
#### Misc
* Deprecate `AsyncNetworkStream` being public ([#1059])
* Upgrade `nom` to v8 ([#1048])
* Drop `rustls-pemfile` in favor of `rustls-pki-types` APIs ([#1050])
* Ban direct use of `std::time::SystemTime::now` via clippy ([#1043])
* Drop direct dependency on `rustls-pki-types` ([#1051])
* Remove artifact from `web-time` refactor ([#1049])
* Fix warnings with `rustls-native-certs` when `tracing` is disabled ([#1053])
* Bump license year ([#1057])
* Cleanup `Cargo.toml` style ([#1047])
[lettre rustls docs]: https://docs.rs/lettre/0.11.14/lettre/index.html#smtp-over-tls-via-the-rustls-crate
[#1043]: https://github.com/lettre/lettre/pull/1043
[#1047]: https://github.com/lettre/lettre/pull/1047
[#1048]: https://github.com/lettre/lettre/pull/1048
[#1049]: https://github.com/lettre/lettre/pull/1049
[#1050]: https://github.com/lettre/lettre/pull/1050
[#1051]: https://github.com/lettre/lettre/pull/1051
[#1053]: https://github.com/lettre/lettre/pull/1053
[#1054]: https://github.com/lettre/lettre/pull/1054
[#1055]: https://github.com/lettre/lettre/pull/1055
[#1057]: https://github.com/lettre/lettre/pull/1057
[#1059]: https://github.com/lettre/lettre/pull/1059
<a name="v0.11.13"></a>
### v0.11.13 (2025-02-17)
#### Features
* Add WASM support ([#1037], [#1042])
* Add method to get the TLS verify result with BoringSSL ([#1039])
#### Bug fixes
* Synchronous pool shutdowns being arbitrarily delayed ([#1041])
[#1037]: https://github.com/lettre/lettre/pull/1037
[#1039]: https://github.com/lettre/lettre/pull/1039
[#1041]: https://github.com/lettre/lettre/pull/1041
[#1042]: https://github.com/lettre/lettre/pull/1042
<a name="v0.11.12"></a>
### v0.11.12 (2025-02-02)
#### Misc
* Warn against manually configuring `port` and `tls` on SMTP transport builder ([#1014])
* Document variants of `Tls` enum ([#1015])
* Fix rustdoc warnings ([#1016])
* Add `ContentType::TEXT_PLAIN` to `Message` builder examples ([#1017])
* Document `SmtpTransport` and `AsyncSmtpTransport` ([#1018])
* Fix typo in transport builder `credentials` method ([#1019])
* Document required system dependencies for OpenSSL ([#1030])
* Improve docs for the `transport::smtp` module ([#1031])
* Improve docs for smtp transport builder `from_url` ([#1032])
* Replace `assert!` with `?` on `send` examples ([#1033])
* Warn on more pedantic clippy lints and fix them ([#1035], [#1036])
[#1014]: https://github.com/lettre/lettre/pull/1014
[#1015]: https://github.com/lettre/lettre/pull/1015
[#1016]: https://github.com/lettre/lettre/pull/1016
[#1017]: https://github.com/lettre/lettre/pull/1017
[#1018]: https://github.com/lettre/lettre/pull/1018
[#1019]: https://github.com/lettre/lettre/pull/1019
[#1030]: https://github.com/lettre/lettre/pull/1030
[#1031]: https://github.com/lettre/lettre/pull/1031
[#1032]: https://github.com/lettre/lettre/pull/1032
[#1033]: https://github.com/lettre/lettre/pull/1033
[#1035]: https://github.com/lettre/lettre/pull/1035
[#1036]: https://github.com/lettre/lettre/pull/1036
<a name="v0.11.11"></a>
### v0.11.11 (2024-12-05)
#### Upgrade notes
* MSRV is now 1.71 ([#1008])
#### Bug fixes
* Fix off-by-one error reaching the minimum number of configured pooled connections ([#1012])
#### Misc
* Fix clippy warnings ([#1009])
* Fix `-Zminimal-versions` build ([#1007])
[#1007]: https://github.com/lettre/lettre/pull/1007
[#1008]: https://github.com/lettre/lettre/pull/1008
[#1009]: https://github.com/lettre/lettre/pull/1009
[#1012]: https://github.com/lettre/lettre/pull/1012
<a name="v0.11.10"></a>
### v0.11.10 (2024-10-23)
#### Bug fixes
* Ignore disconnect errors when `pool` feature of SMTP transport is disabled ([#999])
* Use case insensitive comparisons for matching login challenge requests ([#1000])
[#999]: https://github.com/lettre/lettre/pull/999
[#1000]: https://github.com/lettre/lettre/pull/1000
<a name="v0.11.9"></a>
### v0.11.9 (2024-09-13)
#### Bug fixes
* Fix feature gate for `accept_invalid_hostnames` for rustls ([#988])
* Fix parsing `Mailbox` with trailing spaces ([#986])
#### Misc
* Bump `rustls-native-certs` to v0.8 ([#992])
* Make getting started example in readme complete ([#990])
[#988]: https://github.com/lettre/lettre/pull/988
[#986]: https://github.com/lettre/lettre/pull/986
[#990]: https://github.com/lettre/lettre/pull/990
[#992]: https://github.com/lettre/lettre/pull/992
<a name="v0.11.8"></a>
### v0.11.8 (2024-09-03)
#### Features
* Add mTLS support ([#974])
* Implement `accept_invalid_hostnames` for rustls ([#977])
* Provide certificate chain for peer certificates when using `rustls` or `boring-tls` ([#976])
#### Changes
* Make `HeaderName` comparisons via `PartialEq` case insensitive ([#980])
#### Misc
* Fix clippy warnings ([#979])
* Replace manual impl of `#[non_exhaustive]` for `InvalidHeaderName` ([#981])
[#974]: https://github.com/lettre/lettre/pull/974
[#976]: https://github.com/lettre/lettre/pull/976
[#977]: https://github.com/lettre/lettre/pull/977
[#980]: https://github.com/lettre/lettre/pull/980
[#981]: https://github.com/lettre/lettre/pull/981
<a name="v0.11.7"></a>
### v0.11.7 (2024-04-23)
#### Misc
* Bump `hostname` to v0.4 ([#956])
* Fix `tracing` message consistency ([#960])
* Bump minimum required `rustls` to v0.23.5 ([#958])
* Dropped use of `ref` syntax in the entire project ([#959])
[#956]: https://github.com/lettre/lettre/pull/956
[#958]: https://github.com/lettre/lettre/pull/958
[#959]: https://github.com/lettre/lettre/pull/959
[#960]: https://github.com/lettre/lettre/pull/960
<a name="v0.11.6"></a>
### v0.11.6 (2024-03-28)
#### Bug fixes
* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952])
#### Misc
* Updated copyright year in license ([#954])
[#952]: https://github.com/lettre/lettre/pull/952
[#954]: https://github.com/lettre/lettre/pull/954
<a name="v0.11.5"></a>
### v0.11.5 (2024-03-25)
#### Features
* Support SMTP SASL draft login challenge ([#911])
* Add conversion from SMTP response code to integer ([#941])
#### Misc
* Upgrade `rustls` to v0.23 ([#950])
* Bump `base64` to v0.22 ([#945])
* Fix typos in documentation ([#943], [#944])
* Add `Cargo.lock` ([#942])
[#911]: https://github.com/lettre/lettre/pull/911
[#941]: https://github.com/lettre/lettre/pull/941
[#942]: https://github.com/lettre/lettre/pull/942
[#943]: https://github.com/lettre/lettre/pull/943
[#944]: https://github.com/lettre/lettre/pull/944
[#945]: https://github.com/lettre/lettre/pull/945
[#950]: https://github.com/lettre/lettre/pull/950
<a name="v0.11.4"></a>
### 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)
@@ -198,7 +548,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
* Update `hostname` to 0.3
* Update to `nom` 6
* Replace `log` with `tracing`
* Move CI to Github Actions
* Move CI to GitHub Actions
* Use criterion for benchmarks
<a name="v0.9.2"></a>
@@ -368,6 +718,6 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
* multipart support
* add non-consuming methods for Email builders
* `add_header` does not return the builder anymore,
* `add_header` does not return the builder anymore,
for consistency with other methods. Use the `header`
method instead

2986
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.11.1"
version = "0.11.17"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021"
rust-version = "1.65"
rust-version = "1.74"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -19,37 +19,39 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
[dependencies]
chumsky = "0.9"
idna = "0.4"
once_cell = { version = "1", optional = true }
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
email_address = { version = "0.2.1", default-features = false }
nom = "8"
idna = "1"
## tracing support
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true }
# builder
httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true }
fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.21", optional = true }
email-encoding = { version = "0.2", optional = true }
base64 = { version = "0.22", optional = true }
email-encoding = { version = "0.4", optional = true }
# file transport
uuid = { version = "1", features = ["v4"], optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
serde = { version = "1.0.110", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
# smtp-transport
nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.5.1", optional = true }
hostname = { version = "0.4", optional = true } # feature
socket2 = { version = "0.6", optional = true }
url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }
## tls
native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true }
rustls-pemfile = { version = "1", optional = true }
rustls-native-certs = { version = "0.6.2", optional = true }
webpki-roots = { version = "0.25", optional = true }
boring = { version = "3", optional = true }
native-tls = { version = "0.2.9", optional = true } # feature
rustls = { version = "0.23.18", default-features = false, features = ["logging", "std", "tls12"], optional = true }
rustls-platform-verifier = { version = "0.6.0", optional = true }
rustls-native-certs = { version = "0.8", optional = true }
webpki-roots = { version = "1.0.0", optional = true }
boring = { version = "4", optional = true }
# async
futures-io = { version = "0.3.7", optional = true }
@@ -58,22 +60,22 @@ async-trait = { version = "0.1", optional = true }
## async-std
async-std = { version = "1.8", optional = true }
#async-native-tls = { version = "0.3.3", optional = true }
futures-rustls = { version = "0.24", optional = true }
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true }
## tokio
tokio1_crate = { package = "tokio", version = "1", optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.24", optional = true }
tokio1_boring = { package = "tokio-boring", version = "3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true }
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
## dkim
sha2 = { version = "0.10", optional = true, features = ["oid"] }
sha2 = { version = "0.10", features = ["oid"], optional = true }
rsa = { version = "0.9", optional = true }
ed25519-dalek = { version = "2", optional = true }
# email formats
email_address = { version = "0.2.1", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
## web-time for wasm support
web-time = { version = "1.1.0", optional = true }
[dev-dependencies]
pretty_assertions = "1"
@@ -85,7 +87,7 @@ walkdir = "2"
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1"
maud = "0.25"
maud = "0.26"
[[bench]]
harness = false
@@ -104,25 +106,40 @@ mime03 = ["dep:mime"]
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:once_cell", "dep:url", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
smtp-transport = ["dep:base64", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
pool = ["dep:futures-util"]
rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep: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 = ["dep:boring"]
# async
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "dep:async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
async-std1-rustls = ["async-std1", "rustls", "dep:futures-rustls"]
# deprecated
async-std1-rustls-tls = ["async-std1-rustls", "rustls-tls"]
tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
tokio1-rustls = ["tokio1", "rustls", "dep:tokio1_rustls"]
# deprecated
tokio1-rustls-tls = ["tokio1-rustls", "rustls-tls"]
tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
# wasm support
web = ["dep:web-time"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(lettre_ignore_tls_mismatch)'] }
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]

View File

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

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.11.1">
<img src="https://deps.rs/crate/lettre/0.11.1/status.svg"
<a href="https://deps.rs/crate/lettre/0.11.17">
<img src="https://deps.rs/crate/lettre/0.11.17/status.svg"
alt="dependency status" />
</a>
</div>
@@ -53,12 +53,12 @@ Lettre does not provide (for now):
## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.65, but this could change at any time either from
the minimum supported Rust version is 1.74, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example
This library requires Rust 1.65 or newer.
This library requires Rust 1.74 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
@@ -71,27 +71,29 @@ use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
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();
fn main() {
let email = Message::builder()
.from(Mailbox::new("NoBody".to_owned(), "nobody@domain.tld".parse().unwrap()))
.reply_to(Mailbox::new("Yuin".to_owned(), "yuin@domain.tld".parse().unwrap()))
.to(Mailbox::new("Hei".to_owned(), "hei@domain.tld".parse().unwrap()))
.subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.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
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {e:?}"),
}
}
```
@@ -105,7 +107,7 @@ cargo run --example autoconfigure SMTP_HOST
## Testing
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
Alternatively only unit tests can be run by doing `cargo test --lib`.

View File

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

3
clippy.toml Normal file
View File

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

View File

@@ -41,7 +41,7 @@ fn main() {
// Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS
{
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)
.expect("build SmtpTransport::starttls_relay")

View File

@@ -14,11 +14,71 @@ pub struct Envelope {
/// The envelope recipient's addresses
///
/// This can not be empty.
#[cfg_attr(
feature = "serde",
serde(deserialize_with = "serde_forward_path::deserialize")
)]
forward_path: Vec<Address>,
/// The envelope sender 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 {
/// Creates a new envelope, which may fail if `to` is empty.
///
@@ -103,6 +163,7 @@ impl Envelope {
}
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
impl TryFrom<&Headers> for Envelope {
type Error = Error;

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.65 or newer.
//! Lettre requires Rust 1.74 or newer.
//!
//! ## Features
//!
@@ -34,13 +34,25 @@
//!
//! _Secure SMTP connections using TLS from the `native-tls` crate_
//!
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL
//! on all other platforms.
//!
//! * **native-tls** 📫: TLS support for the synchronous version of the API
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! ##### Building lettre with OpenSSL
//!
//! When building lettre with native-tls on a system that makes
//! use of OpenSSL, the following packages will need to be installed
//! in order for the build and the compiled program to run properly.
//!
//! | Distro | Build-time packages | Runtime packages |
//! | ------------ | -------------------------- | ---------------------------- |
//! | Debian | `pkg-config`, `libssl-dev` | `libssl3`, `ca-certificates` |
//! | Alpine Linux | `pkgconf`, `openssl-dev` | `libssl3`, `ca-certificates` |
//!
//! #### SMTP over TLS via the boring crate (Boring TLS)
//!
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
@@ -52,13 +64,49 @@
//!
//! #### SMTP over TLS via the rustls crate
//!
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
//! _Secure SMTP connections using TLS from the `rustls` crate_
//!
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
//! * **rustls**: TLS support for the synchronous version of the API
//! * **tokio1-rustls**: TLS support for the `tokio1` async version of the API
//! * **async-std1-rustls**: TLS support for the `async-std1` async version of the API
//!
//! * **rustls-tls**: TLS support for the synchronous version of the API
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
//! ##### rustls crypto backends
//!
//! _The crypto implementation to use with rustls_
//!
//! When the `rustls` feature is enabled, one of the following crypto backends MUST also
//! be enabled.
//!
//! * **aws-lc-rs**: use [AWS-LC] (via [`aws-lc-rs`]) as the `rustls` crypto backend
//! * **ring**: use [`ring`] as the `rustls` crypto backend
//!
//! When enabling `aws-lc-rs`, the `fips` feature can also be enabled to have
//! rustls use the FIPS certified module of AWS-LC.
//!
//! `aws-lc-rs` may require cmake on some platforms to compile.
//! `fips` always requires cmake and the Go compiler to compile.
//!
//! ##### rustls certificate verification backend
//!
//! _The TLS certificate verification backend to use with rustls_
//!
//! When the `rustls` feature is enabled, one of the following verification backends
//! MUST also be enabled.
//!
//! * **rustls-platform-verifier**: verify TLS certificate using the OS's native certificate store (see [`rustls-platform-verifier`])
//! * **rustls-native-certs**: verify TLS certificates using the platform's native certificate store (see [`rustls-native-certs`]) - when in doubt use `rustls-platform-verifier`
//! * **webpki-roots**: verify TLS certificates against Mozilla's root certificates (see [`webpki-roots`])
//!
//! The following packages will need to be installed in order for the build
//! stage and the compiled program to run properly.
//!
//! | Verification backend | Distro | Build-time packages | Runtime packages |
//! | --------------------- | ------------ | -------------------------- | ---------------------------- |
//! | `rustls-platform-verifier` | Debian | none | `ca-certificates` |
//! | `rustls-platform-verifier` | Alpine Linux | none | `ca-certificates` |
//! | `rustls-native-certs` | Debian | none | `ca-certificates` |
//! | `rustls-native-certs` | Alpine Linux | none | `ca-certificates` |
//! | `webpki-roots` | any | none | none |
//!
//! ### Sendmail transport
//!
@@ -95,6 +143,7 @@
//! * **tracing**: Logging using the `tracing` crate
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
//! * **dkim**: Add support for signing email with DKIM
//! * **web**: WebAssembly support using the `web-time` crate for time operations
//!
//! [`SMTP`]: crate::transport::smtp
//! [`sendmail`]: crate::transport::sendmail
@@ -102,18 +151,23 @@
//! [`ContentType`]: crate::message::header::ContentType
//! [tokio]: https://docs.rs/tokio/1
//! [async-std]: https://docs.rs/async-std/1
//! [ring]: https://github.com/briansmith/ring#ring
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
//! [AWS-LC]: https://github.com/aws/aws-lc
//! [`aws-lc-rs`]: https://crates.io/crates/aws-lc-rs
//! [`ring`]: https://crates.io/crates/ring
//! [`rustls-platform-verifier`]: https://crates.io/crates/rustls-platform-verifier
//! [`rustls-native-certs`]: https://crates.io/crates/rustls-native-certs
//! [`webpki-roots`]: https://crates.io/crates/webpki-roots
//! [Tokio 1.x]: https://docs.rs/tokio/1
//! [async-std 1.x]: https://docs.rs/async-std/1
//! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.1")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.17")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
#![deny(
unreachable_pub,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
@@ -137,12 +191,36 @@
clippy::wildcard_imports,
clippy::str_to_string,
clippy::empty_structs_with_brackets,
clippy::zero_sized_map_values
clippy::zero_sized_map_values,
clippy::manual_let_else,
clippy::semicolon_if_nothing_returned,
clippy::unnecessary_wraps,
clippy::doc_markdown,
clippy::explicit_iter_loop,
clippy::redundant_closure_for_method_calls,
// Rust 1.86: clippy::unnecessary_semicolon,
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(not(lettre_ignore_tls_mismatch))]
mod compiletime_checks {
#[cfg(all(feature = "rustls", not(feature = "aws-lc-rs"), not(feature = "ring")))]
compile_error!(
"feature `rustls` also requires either the `aws-lc-rs` or the `ring` feature to
be enabled"
);
#[cfg(all(
feature = "rustls",
not(feature = "rustls-platform-verifier"),
not(feature = "rustls-native-certs"),
not(feature = "webpki-roots")
))]
compile_error!(
"feature `rustls` also requires either the `rustls-platform-verifier`, the `rustls-native-certs`
or the `webpki-roots` feature to be enabled"
);
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]
compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
the executable will fail to link.");
@@ -153,16 +231,12 @@ mod compiletime_checks {
not(feature = "tokio1-native-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
If you were trying to opt into `rustls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "tokio1",
feature = "rustls-tls",
not(feature = "tokio1-rustls-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `rustls-tls` features, but the `tokio1-rustls-tls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
#[cfg(all(feature = "tokio1", feature = "rustls", not(feature = "tokio1-rustls")))]
compile_error!("Lettre is being built with the `tokio1` and the `rustls` features, but the `tokio1-rustls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
@@ -171,36 +245,22 @@ mod compiletime_checks {
not(feature = "tokio1-boring-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `boring-tls` features, but the `tokio1-boring-tls` feature hasn't been turned on.
If you'd like to use `boring-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
If you'd like to use `boring-tls` make sure that the `rustls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
/*
#[cfg(all(
feature = "async-std1",
feature = "native-tls",
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")
))]
#[cfg(all(feature = "async-std1", feature = "native-tls"))]
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
If you were trying to opt into `rustls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "async-std1",
feature = "rustls-tls",
not(feature = "async-std1-rustls-tls")
feature = "rustls",
not(feature = "async-std1-rustls")
))]
compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
compile_error!("Lettre is being built with the `async-std1` and the `rustls` features, but the `async-std1-rustls` feature hasn't been turned on.
If you'd like to use `native-tls` make sure that the `rustls` hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
}
@@ -213,6 +273,9 @@ mod executor;
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub mod message;
#[cfg(feature = "rustls")]
mod rustls_crypto;
mod time;
pub mod transport;
use std::error::Error as StdError;

View File

@@ -2,7 +2,6 @@ use std::{
borrow::Cow,
error::Error as StdError,
fmt::{self, Display},
iter::IntoIterator,
time::SystemTime,
};
@@ -70,7 +69,7 @@ impl Display for DkimSigningAlgorithm {
}
}
/// Describe DkimSigning key error
/// Describe [`DkimSigningKey`] key error
#[derive(Debug)]
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
@@ -101,7 +100,7 @@ impl StdError for DkimSigningKeyError {
}
}
/// Describe a signing key to be carried by DkimConfig struct
/// Describe a signing key to be carried by [`DkimConfig`] struct
#[derive(Debug)]
pub struct DkimSigningKey(InnerDkimSigningKey);
@@ -184,7 +183,7 @@ impl DkimConfig {
}
}
/// Create a DkimConfig
/// Create a [`DkimConfig`]
pub fn new(
selector: String,
domain: String,
@@ -284,19 +283,19 @@ fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
// End of header.
[b'\r', b'\n', ..] => {
*out += "\r\n";
name(&h[2..], out)
name(&h[2..], out);
}
// Sequential whitespace.
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
// All whitespace becomes spaces.
[b'\t', ..] => {
out.push(' ');
value(&h[1..], out)
value(&h[1..], out);
}
[_, ..] => {
let mut chars = h.chars();
out.push(chars.next().unwrap());
value(chars.as_str(), out)
value(chars.as_str(), out);
}
[] => {}
}
@@ -318,7 +317,7 @@ fn dkim_canonicalize_header_tag(
}
}
/// Canonicalize signed headers passed as headers_list among mail_headers using canonicalization
/// Canonicalize signed headers passed as `headers_list` among `mail_headers` using canonicalization
fn dkim_canonicalize_headers<'a>(
headers_list: impl IntoIterator<Item = &'a str>,
mail_headers: &Headers,
@@ -345,10 +344,9 @@ fn dkim_canonicalize_headers<'a>(
}
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
/// dkim_config
/// `dkim_config`
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
dkim_sign_fixed_time(message, dkim_config, crate::time::now());
}
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
@@ -379,7 +377,7 @@ fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timesta
}
let dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
let signed_headers = dkim_canonicalize_headers(
dkim_config.headers.iter().map(|h| h.as_ref()),
dkim_config.headers.iter().map(AsRef::as_ref),
headers,
dkim_config.canonicalization.header,
);
@@ -489,14 +487,14 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
fn test_headers_simple_canonicalize() {
let message = test_message();
dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n");
}
#[test]
fn test_headers_relaxed_canonicalize() {
let message = test_message();
dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n");
}
#[test]

View File

@@ -1,6 +1,6 @@
use std::fmt::Write;
use email_encoding::headers::EmailWriter;
use email_encoding::headers::writer::EmailWriter;
use super::{Header, HeaderName, HeaderValue};
use crate::BoxError;
@@ -38,10 +38,10 @@ impl ContentDisposition {
let mut encoded_value = String::new();
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_char(';').expect("writing `;` returned an error");
w.optional_breakpoint();
w.space();
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
.expect("some Write implementation returned an error");

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ use std::{
ops::Deref,
};
use email_encoding::headers::EmailWriter;
use email_encoding::headers::writer::EmailWriter;
pub use self::{
content::*,
@@ -124,22 +124,18 @@ impl Headers {
}
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers
.iter()
.find(|value| name.eq_ignore_ascii_case(&value.name))
self.headers.iter().find(|value| name == value.name)
}
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
self.headers
.iter_mut()
.find(|value| name.eq_ignore_ascii_case(&value.name))
self.headers.iter_mut().find(|value| name == value.name)
}
fn find_header_index(&self, name: &str) -> Option<usize> {
self.headers
.iter()
.enumerate()
.find(|(_i, value)| name.eq_ignore_ascii_case(&value.name))
.find(|(_i, value)| name == value.name)
.map(|(i, _)| i)
}
}
@@ -161,18 +157,9 @@ impl Display for Headers {
/// A possible error when converting a `HeaderName` from another type.
// comes from `http` crate
#[allow(missing_copy_implementations)]
#[derive(Clone)]
pub struct InvalidHeaderName {
_priv: (),
}
impl fmt::Debug for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InvalidHeaderName")
// skip _priv noise
.finish()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct InvalidHeaderName;
impl fmt::Display for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -189,14 +176,11 @@ pub struct HeaderName(Cow<'static, str>);
impl HeaderName {
/// Creates a new header name
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
if !ascii.is_empty()
&& ascii.len() <= 76
&& ascii.is_ascii()
&& !ascii.contains(|c| c == ':' || c == ' ')
if !ascii.is_empty() && ascii.len() <= 76 && ascii.is_ascii() && !ascii.contains([':', ' '])
{
Ok(Self(Cow::Owned(ascii)))
} else {
Err(InvalidHeaderName { _priv: () })
Err(InvalidHeaderName)
}
}
@@ -257,23 +241,19 @@ impl AsRef<str> for HeaderName {
impl PartialEq<HeaderName> for HeaderName {
fn eq(&self, other: &HeaderName) -> bool {
let s1: &str = self.as_ref();
let s2: &str = other.as_ref();
s1 == s2
self.eq_ignore_ascii_case(other)
}
}
impl PartialEq<&str> for HeaderName {
fn eq(&self, other: &&str) -> bool {
let s: &str = self.as_ref();
s == *other
self.eq_ignore_ascii_case(other)
}
}
impl PartialEq<HeaderName> for &str {
fn eq(&self, other: &HeaderName) -> bool {
let s: &str = other.as_ref();
*self == s
self.eq_ignore_ascii_case(other)
}
}
@@ -348,7 +328,7 @@ impl<'a> HeaderValueEncoder<'a> {
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
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 {
writer,
@@ -435,7 +415,7 @@ mod tests {
#[test]
fn empty_headername() {
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
assert!(HeaderName::new_from_ascii("".to_owned()).is_err());
}
#[test]
@@ -467,6 +447,60 @@ mod tests {
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
#[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(),
));
// 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!(
headers.to_string(),
concat!(
"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",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
)
);
}
@@ -687,9 +718,6 @@ mod tests {
"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!(
headers.to_string(),
concat!(
@@ -699,8 +727,8 @@ mod tests {
"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",
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
)

View File

@@ -3,30 +3,34 @@
//!
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
use chumsky::{error::Cheap, prelude::*};
use nom::{
branch::alt,
character::complete::{char, satisfy},
IResult, Parser,
};
// 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())
pub(super) fn alpha(input: &str) -> IResult<&str, char> {
satisfy(|c| c.is_ascii_alphabetic()).parse(input)
}
// DIGIT = %x30-39
// ; 0-9
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_digit())
pub(super) fn digit(input: &str) -> IResult<&str, char> {
satisfy(|c| c.is_ascii_digit()).parse(input)
}
// DQUOTE = %x22
// ; " (Double Quote)
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
just('"')
pub(super) fn dquote(input: &str) -> IResult<&str, char> {
char('"').parse(input)
}
// WSP = SP / HTAB
// ; white space
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
choice((just(' '), just('\t')))
pub(super) fn wsp(input: &str) -> IResult<&str, char> {
alt((char(' '), char('\t'))).parse(input)
}

View File

@@ -3,7 +3,14 @@
//!
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
use chumsky::{error::Cheap, prelude::*};
use nom::{
branch::alt,
character::complete::{char, satisfy},
combinator::{eof, map, opt},
multi::{fold_many0, fold_many1, many0, many1, separated_list0},
sequence::{delimited, pair, preceded, terminated},
IResult, Parser,
};
use super::{rfc2234, rfc5336};
@@ -15,8 +22,8 @@ use super::{rfc2234, rfc5336};
// %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))
fn no_ws_ctl(input: &str) -> IResult<&str, char> {
satisfy(|c| matches!(u32::from(c), 1..=8 | 11 | 12 | 14..=31 | 127)).parse(input)
}
// text = %d1-9 / ; Characters excluding CR and LF
@@ -24,16 +31,16 @@ fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
// %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))
fn text(input: &str) -> IResult<&str, char> {
satisfy(|c| matches!(u32::from(c), 1..=9 | 11 | 12 | 14..=127)).parse(input)
}
// 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())
fn quoted_pair(input: &str) -> IResult<&str, char> {
preceded(char('\\'), text).parse(input)
}
// 3.2.3. Folding white space and comments
@@ -41,17 +48,19 @@ fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
// obs-FWS
pub fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
rfc2234::wsp()
.or_not()
.then_ignore(rfc2234::wsp().ignored().repeated())
pub(super) fn fws(input: &str) -> IResult<&str, Option<char>> {
map(
pair(opt(rfc2234::wsp), many0(rfc2234::wsp)),
|(first, _rest)| first,
)
.parse(input)
}
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
pub(super) fn cfws(input: &str) -> IResult<&str, Option<char>> {
// TODO: comment are not currently supported, so for now a cfws is
// the same as a fws.
fws()
fws(input)
}
// 3.2.4. Atom
@@ -68,13 +77,13 @@ pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
// "`" / "{" /
// "|" / "}" /
// "~"
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
choice((
rfc2234::alpha(),
rfc2234::digit(),
filter(|c| {
pub(super) fn atext(input: &str) -> IResult<&str, char> {
alt((
rfc2234::alpha,
rfc2234::digit,
satisfy(|c| {
matches!(
*c,
c,
'!' | '#'
| '$'
| '%'
@@ -96,29 +105,64 @@ pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
)
}),
// also allow non ASCII UTF8 chars
rfc5336::utf8_non_ascii(),
rfc5336::utf8_non_ascii,
))
.parse(input)
}
// atom = [CFWS] 1*atext [CFWS]
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(atext().repeated().at_least(1))
pub(super) fn atom(input: &str) -> IResult<&str, String> {
map(
pair(
cfws,
fold_many1(atext, String::new, |mut acc, c| {
acc.push(c);
acc
}),
),
|(cfws, mut chars)| {
if let Some(cfws) = cfws {
chars.insert(0, cfws);
}
chars
},
)
.parse(input)
}
// dot-atom = [CFWS] dot-atom-text [CFWS]
pub fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(dot_atom_text())
pub(super) fn dot_atom(input: &str) -> IResult<&str, String> {
map(pair(cfws, dot_atom_text), |(_cfws, text)| text).parse(input)
}
// dot-atom-text = 1*atext *("." 1*atext)
pub 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(),
pub(super) fn dot_atom_text(input: &str) -> IResult<&str, String> {
map(
pair(
fold_many1(atext, String::new, |mut acc, c| {
acc.push(c);
acc
}),
many0(map(
pair(
char('.'),
fold_many1(atext, String::new, |mut acc, c| {
acc.push(c);
acc
}),
),
|(dot, chars)| format!("{dot}{chars}"),
)),
),
|(first, rest)| {
let mut result = first;
for part in rest {
result.push_str(&part);
}
result
},
)
.parse(input)
}
// 3.2.5. Quoted strings
@@ -129,120 +173,168 @@ pub fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// %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(),
fn qtext(input: &str) -> IResult<&str, char> {
alt((
satisfy(|c| matches!(u32::from(c), 33 | 35..=91 | 93..=126)),
no_ws_ctl,
))
.parse(input)
}
// qcontent = qtext / quoted-pair
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> {
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii()))
pub(super) fn qcontent(input: &str) -> IResult<&str, char> {
alt((qtext, quoted_pair, rfc5336::utf8_non_ascii)).parse(input)
}
// 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())
fn quoted_string(input: &str) -> IResult<&str, String> {
map(
delimited(
rfc2234::dquote,
fold_many0(
map(pair(fws, qcontent), |(_fws, c)| c),
String::new,
|mut acc, c| {
acc.push(c);
acc
},
),
preceded(many0(satisfy(char::is_whitespace)), rfc2234::dquote),
),
|s| s,
)
.parse(input)
}
// 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()))
fn word(input: &str) -> IResult<&str, String> {
alt((quoted_string, atom)).parse(input)
}
// phrase = 1*word / obs-phrase
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((obs_phrase(), word().repeated().at_least(1).flatten()))
fn phrase(input: &str) -> IResult<&str, String> {
alt((obs_phrase, map(many1(word), |words| words.join(" ")))).parse(input)
}
// 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)))).then_ignore(end())
pub(crate) fn mailbox(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
terminated(alt((name_addr, map(addr_spec, |addr| (None, addr)))), eof).parse(input)
}
// 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())
fn name_addr(input: &str) -> IResult<&str, (Option<String>, (String, String))> {
pair(opt(display_name), angle_addr).parse(input)
}
// 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()
fn angle_addr(input: &str) -> IResult<&str, (String, String)> {
delimited((cfws, char('<')), addr_spec, (char('>'), cfws)).parse(input)
}
// display-name = phrase
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
phrase()
fn display_name(input: &str) -> IResult<&str, String> {
phrase(input)
}
// 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())
#[allow(clippy::type_complexity)]
pub(crate) fn mailbox_list(input: &str) -> IResult<&str, Vec<(Option<String>, (String, String))>> {
terminated(
separated_list0(
delimited(
many0(satisfy(char::is_whitespace)),
char(','),
many0(satisfy(char::is_whitespace)),
),
alt((name_addr, map(addr_spec, |addr| (None, addr)))),
),
eof,
)
.parse(input)
}
// 3.4.1. Addr-spec specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
// addr-spec = local-part "@" domain
pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
local_part()
.collect()
.then_ignore(just('@'))
.then(domain().collect())
pub(super) fn addr_spec(input: &str) -> IResult<&str, (String, String)> {
pair(terminated(local_part, char('@')), domain).parse(input)
}
// local-part = dot-atom / quoted-string / obs-local-part
pub fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((dot_atom(), quoted_string(), obs_local_part()))
pub(super) fn local_part(input: &str) -> IResult<&str, String> {
alt((dot_atom, quoted_string, obs_local_part)).parse(input)
}
// domain = dot-atom / domain-literal / obs-domain
pub fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
pub(super) fn domain(input: &str) -> IResult<&str, String> {
// NOTE: omitting domain-literal since it may never be used
choice((dot_atom(), obs_domain()))
alt((dot_atom, obs_domain)).parse(input)
}
// 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>> {
fn obs_phrase(input: &str) -> IResult<&str, String> {
// 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(),
map(
pair(word, many0(alt((word, map(char('.'), |c| c.to_string()))))),
|(first, rest)| {
let mut result = first;
for part in rest {
result.push_str(&part);
}
result
},
)
.parse(input)
}
// 4.4. Obsolete Addressing
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
// obs-local-part = word *("." word)
pub fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
word().chain(just('.').chain(word()).repeated().flatten())
pub(super) fn obs_local_part(input: &str) -> IResult<&str, String> {
map(
pair(
word,
many0(map(pair(char('.'), word), |(dot, w)| format!("{dot}{w}"))),
),
|(first, rest)| {
let mut result = first;
for part in rest {
result.push_str(&part);
}
result
},
)
.parse(input)
}
// obs-domain = atom *("." atom)
pub fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atom().chain(just('.').chain(atom()).repeated().flatten())
pub(super) fn obs_domain(input: &str) -> IResult<&str, String> {
map(
pair(
atom,
many0(map(pair(char('.'), atom), |(dot, a)| format!("{dot}{a}"))),
),
|(first, rest)| {
let mut result = first;
for part in rest {
result.push_str(&part);
}
result
},
)
.parse(input)
}

View File

@@ -3,7 +3,7 @@
//!
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
use chumsky::{error::Cheap, prelude::*};
use nom::{character::complete::satisfy, IResult, Parser};
// 3.3. Extended Mailbox Address Syntax
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
@@ -12,6 +12,6 @@ use chumsky::{error::Cheap, prelude::*};
// 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)
pub(super) fn utf8_non_ascii(input: &str) -> IResult<&str, char> {
satisfy(|c| c.len_utf8() > 1).parse(input)
}

View File

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

View File

@@ -5,8 +5,7 @@ use std::{
str::FromStr,
};
use chumsky::prelude::*;
use email_encoding::headers::EmailWriter;
use email_encoding::headers::writer::EmailWriter;
use super::parsers;
use crate::address::{Address, AddressError};
@@ -72,7 +71,7 @@ impl Mailbox {
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
if let Some(name) = &self.name {
email_encoding::headers::quoted_string::encode(name, w)?;
w.optional_breakpoint();
w.space();
w.write_char('<')?;
}
@@ -88,7 +87,7 @@ impl Mailbox {
impl Display for Mailbox {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(ref name) = self.name {
if let Some(name) = &self.name {
let name = name.trim();
if !name.is_empty() {
write_word(f, name)?;
@@ -114,7 +113,7 @@ impl FromStr for Mailbox {
type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
let (_rest, (name, (user, domain))) = parsers::mailbox(src).map_err(|_errs| {
// TODO: improve error management
AddressError::InvalidInput
})?;
@@ -174,7 +173,7 @@ impl Mailboxes {
self
}
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
/// Adds a new [`Mailbox`] to the list, in a `Vec::push` style pattern.
///
/// # Examples
///
@@ -261,7 +260,7 @@ impl Mailboxes {
for mailbox in self.iter() {
if !mem::take(&mut first) {
w.write_char(',')?;
w.optional_breakpoint();
w.space();
}
mailbox.encode(w)?;
@@ -345,13 +344,13 @@ impl FromStr for Mailboxes {
fn from_str(src: &str) -> Result<Self, Self::Err> {
let mut mailboxes = Vec::new();
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
let (_rest, parsed_mailboxes) = parsers::mailbox_list(src).map_err(|_errs| {
// TODO: improve error management
AddressError::InvalidInput
})?;
for (name, (user, domain)) in parsed_mailboxes {
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?));
}
Ok(Mailboxes(mailboxes))
@@ -444,8 +443,6 @@ fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
#[cfg(test)]
mod test {
use std::convert::TryInto;
use pretty_assertions::assert_eq;
use super::Mailbox;
@@ -533,7 +530,7 @@ mod test {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
Mailbox::new(Some("".to_owned()), "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
@@ -558,6 +555,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]
fn parse_address_with_name() {
assert_eq!(
@@ -569,6 +574,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]
fn parse_address_with_empty_name() {
assert_eq!(
@@ -580,7 +596,7 @@ mod test {
#[test]
fn parse_address_with_empty_name_trim() {
assert_eq!(
" <kayo@example.com>".parse(),
" <kayo@example.com> ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}

View File

@@ -17,6 +17,16 @@ pub(super) enum Part {
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 {
fn format(&self, out: &mut Vec<u8>) {
match self {
@@ -132,6 +142,12 @@ impl SinglePart {
self.format(&mut 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 {
@@ -139,8 +155,7 @@ impl EmailFormat for SinglePart {
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
out.extend_from_slice(&self.body);
out.extend_from_slice(b"\r\n");
self.format_body(out);
}
}
@@ -373,14 +388,9 @@ impl MultiPart {
self.format(&mut 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();
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)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
use crate::message::header;
#[test]
fn single_part_binary() {

View File

@@ -277,7 +277,7 @@ impl MessageBuilder {
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
/// if no date has been provided.
pub fn date_now(self) -> Self {
self.date(SystemTime::now())
self.date(crate::time::now())
}
/// Set or add mailbox to `ReplyTo` header
@@ -345,7 +345,7 @@ impl MessageBuilder {
let hostname = hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
.unwrap_or_else(|()| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
@@ -457,12 +457,12 @@ impl MessageBuilder {
self.build(MessageBody::Raw(body.into_vec()))
}
/// Create message using mime body ([`MultiPart`][self::MultiPart])
/// Create message using mime body ([`MultiPart`])
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
}
/// Create message using mime body ([`SinglePart`][self::SinglePart])
/// Create message using mime body ([`SinglePart`])
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
}
@@ -525,9 +525,9 @@ impl Message {
pub(crate) fn body_raw(&self) -> Vec<u8> {
let mut out = Vec::new();
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),
};
}
out.extend_from_slice(b"\r\n");
out
}
@@ -537,7 +537,10 @@ impl Message {
/// Example:
/// ```rust
/// use lettre::{
/// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
/// message::{
/// dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
/// header::ContentType,
/// },
/// Message,
/// };
///
@@ -546,6 +549,7 @@ impl Message {
/// .reply_to("Bob <bob@example.org>".parse().unwrap())
/// .to("Carla <carla@example.net>".parse().unwrap())
/// .subject("Hello")
/// .header(ContentType::TEXT_PLAIN)
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
/// .unwrap();
/// let key = "-----BEGIN RSA PRIVATE KEY-----
@@ -601,7 +605,7 @@ impl EmailFormat for Message {
MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n");
out.extend_from_slice(r)
out.extend_from_slice(r);
}
}
}
@@ -765,7 +769,7 @@ mod test {
continue;
}
assert_eq!(line.0, line.1)
assert_eq!(line.0, line.1);
}
}

14
src/rustls_crypto.rs Normal file
View 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
View File

@@ -0,0 +1,26 @@
use std::time::SystemTime;
#[cfg(all(feature = "web", target_arch = "wasm32"))]
pub(crate) fn now() -> SystemTime {
fn to_std_systemtime(time: web_time::SystemTime) -> std::time::SystemTime {
let duration = time
.duration_since(web_time::SystemTime::UNIX_EPOCH)
.unwrap();
SystemTime::UNIX_EPOCH + duration
}
// FIXME: change to:
// #[allow(
// clippy::disallowed_methods,
// reason = "`web-time` aliases `std::time::SystemTime::now` on non-WASM platforms"
// )]
#[allow(clippy::disallowed_methods)]
to_std_systemtime(web_time::SystemTime::now())
}
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
pub(crate) fn now() -> SystemTime {
// FIXME: change to #[expect(clippy::disallowed_methods, reason = "the `web` feature is disabled")]
#[allow(clippy::disallowed_methods)]
SystemTime::now()
}

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
//!
//! * a service from your Cloud or hosting provider
//! * 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.
//!
//! 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 |
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
//! | [`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
//!
@@ -56,13 +56,17 @@
//! #
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
//! use lettre::{
//! message::header::ContentType, transport::smtp::authentication::Credentials, Message,
//! SmtpTransport, Transport,
//! };
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
@@ -97,6 +101,7 @@
//! [`FileTransport`]: crate::FileTransport
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
//! [`StubTransport`]: crate::transport::stub::StubTransport
//! [`AsyncStubTransport`]: crate::transport::stub::AsyncStubTransport
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
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>;
/// Shuts down the transport. Future calls to [`Self::send`] and
/// [`Self::send_raw`] might fail.
fn shutdown(&self) {}
}
/// 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>;
/// Shuts down the transport. Future calls to [`Self::send`] and
/// [`Self::send_raw`] might fail.
async fn shutdown(&self) {}
}

View File

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

View File

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

View File

@@ -12,6 +12,12 @@ use async_trait::async_trait;
use super::pool::async_impl::Pool;
#[cfg(feature = "pool")]
use super::PoolConfig;
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls",
feature = "async-std1-rustls"
))]
use super::Tls;
use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
};
@@ -24,6 +30,30 @@ use crate::Tokio1Executor;
use crate::{Envelope, Executor};
/// Asynchronously sends emails using the SMTP protocol
///
/// `AsyncSmtpTransport` is the primary way for communicating
/// with SMTP relay servers to send email messages. It holds the
/// client connect configuration and creates new connections
/// as necessary.
///
/// # Connection pool
///
/// When the `pool` feature is enabled (default), `AsyncSmtpTransport` maintains a
/// connection pool to manage SMTP connections. The pool:
///
/// - Establishes a new connection when sending a message.
/// - Recycles connections internally after a message is sent.
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
///
/// The connection pool can grow to hold multiple SMTP connections if multiple
/// emails are sent concurrently, as SMTP does not support multiplexing within a
/// single connection.
///
/// However, **connection reuse is not possible** if the `SyncSmtpTransport` instance
/// is dropped after every email send operation. You must reuse the instance
/// of this struct for the connection pool to be of any use.
///
/// To customize connection pool settings, use [`AsyncSmtpTransportBuilder::pool_config`].
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransport<E: Executor> {
#[cfg(feature = "pool")]
@@ -45,10 +75,15 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.quit().await?;
conn.abort().await;
Ok(result)
}
async fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown().await;
}
}
#[cfg(feature = "async-std1")]
@@ -67,6 +102,11 @@ impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
Ok(result)
}
async fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown().await;
}
}
impl<E> AsyncSmtpTransport<E>
@@ -81,16 +121,15 @@ where
/// to validate TLS certificates.
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
feature = "tokio1-rustls",
feature = "async-std1-rustls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
feature = "tokio1-rustls",
feature = "async-std1-rustls"
)))
)]
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
@@ -116,16 +155,15 @@ where
/// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
feature = "tokio1-rustls",
feature = "async-std1-rustls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
feature = "tokio1-rustls",
feature = "async-std1-rustls"
)))
)]
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
@@ -163,54 +201,72 @@ where
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL
///
/// The protocol, credentials, host and port can be provided in a single URL.
/// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
/// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
/// The path section of the url can be used to set an alternative name for
/// the HELO / EHLO command.
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
/// will set the HELO / EHLO name `client.example.com`.
/// The protocol, credentials, host, port and EHLO name can be provided
/// in a single URL. This may be simpler than having to configure SMTP
/// through multiple configuration parameters and then having to pass
/// those options to lettre.
///
/// <table>
/// <thead>
/// <tr>
/// <th>scheme</th>
/// <th>tls parameter</th>
/// <th>example</th>
/// <th>remarks</th>
/// </tr>
/// </thead>
/// <tbody>
/// <tr>
/// <td>smtps</td>
/// <td>-</td>
/// <td>smtps://smtp.example.com</td>
/// <td>SMTP over TLS, recommended method</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>required</td>
/// <td>smtp://smtp.example.com?tls=required</td>
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>opportunistic</td>
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
/// <td>
/// SMTP with optionally STARTTLS when supported by the server.
/// Caution: this method is vulnerable to a man-in-the-middle attack.
/// Not recommended for production use.
/// </td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>-</td>
/// <td>smtp://smtp.example.com</td>
/// <td>Unencrypted SMTP, not recommended for production use.</td>
/// </tr>
/// </tbody>
/// </table>
/// The URL is created in the following way:
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
///
/// `user` (Username) and `pass` (Password) are optional in case the
/// SMTP relay doesn't require authentication. When `port` is not
/// configured it is automatically determined based on the `scheme`.
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
/// command. `TLS` controls whether STARTTLS is simply enabled
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
/// or `required` (require the server to upgrade the connection to
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
///
/// Use the following table to construct your SMTP url:
///
/// | scheme | `tls` query parameter | example | default port | remarks |
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
///
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
/// be concatenated to construct the final URL because special characters
/// contained within the parameter may confuse the URL decoder.
/// Manually URL encode the parameters before concatenating them or use
/// a proper URL encoder, like the following cargo script:
///
/// ```rust
/// # const TOML: &str = r#"
/// #!/usr/bin/env cargo
///
/// //! ```cargo
/// //! [dependencies]
/// //! url = "2"
/// //! ```
/// # "#;
///
/// use url::Url;
///
/// fn main() {
/// // don't touch this line
/// let mut url = Url::parse("foo://bar").unwrap();
///
/// // configure the scheme (`smtp` or `smtps`) here.
/// url.set_scheme("smtps").unwrap();
/// // configure the username and password.
/// // remove the following two lines if unauthenticated.
/// url.set_username("username").unwrap();
/// url.set_password(Some("password")).unwrap();
/// // configure the hostname
/// url.set_host(Some("smtp.example.com")).unwrap();
/// // configure the port - only necessary if using a non-default port
/// url.set_port(Some(465)).unwrap();
/// // configure the EHLO name
/// url.set_path("ehlo-name");
///
/// println!("{url}");
/// }
/// ```
///
/// The connection URL can then be used in the following way:
///
/// ```rust,no_run
/// use lettre::{
@@ -234,22 +290,18 @@ where
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
/// "smtps://username:password@smtp.example.com:465",
/// )
/// .unwrap()
/// )?
/// .build();
///
/// // Send the email
/// match mailer.send(email).await {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// mailer.send(email).await?;
/// # Ok(())
/// # }
/// ```
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
super::connection_url::from_connection_url(connection_url)
@@ -325,7 +377,7 @@ impl AsyncSmtpTransportBuilder {
self
}
/// Set the authentication mechanism to use
/// Set the authentication credentials to use
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.info.credentials = Some(credentials);
self
@@ -338,6 +390,17 @@ impl AsyncSmtpTransportBuilder {
}
/// Set the port to use
///
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
///
/// lettre usually picks the correct `port` when building
/// [`AsyncSmtpTransport`] using [`AsyncSmtpTransport::relay`] or
/// [`AsyncSmtpTransport::starttls_relay`].
///
/// # Errors
///
/// Using the incorrect `port` and [`Self::tls`] combination may
/// lead to hard to debug IO errors coming from the TLS library.
pub fn port(mut self, port: u16) -> Self {
self.info.port = port;
self
@@ -350,21 +413,31 @@ impl AsyncSmtpTransportBuilder {
}
/// Set the TLS settings to use
///
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
///
/// By default lettre chooses the correct `tls` configuration when
/// building [`AsyncSmtpTransport`] using [`AsyncSmtpTransport::relay`] or
/// [`AsyncSmtpTransport::starttls_relay`].
///
/// # Errors
///
/// Using the incorrect [`Tls`] and [`Self::port`] combination may
/// lead to hard to debug IO errors coming from the TLS library.
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
feature = "tokio1-rustls",
feature = "async-std1-rustls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
feature = "tokio1-rustls",
feature = "async-std1-rustls"
)))
)]
pub fn tls(mut self, tls: super::Tls) -> Self {
pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls;
self
}
@@ -397,7 +470,7 @@ impl AsyncSmtpTransportBuilder {
}
/// Build client
pub struct AsyncSmtpClient<E> {
pub(super) struct AsyncSmtpClient<E> {
info: SmtpInfo,
marker_: PhantomData<E>,
}
@@ -409,7 +482,7 @@ where
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
pub(super) async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
let mut conn = E::connect(
&self.info.server,
self.info.port,

View File

@@ -98,11 +98,17 @@ impl Mechanism {
let decoded_challenge = 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());
}
if ["Password", "Password:"].contains(&decoded_challenge) {
if contains_ignore_ascii_case(
decoded_challenge,
["Password", "Password:", "Password\0"],
) {
return Ok(credentials.secret.clone());
}
@@ -119,6 +125,15 @@ impl Mechanism {
}
}
fn contains_ignore_ascii_case<'a>(
haystack: &str,
needles: impl IntoIterator<Item = &'a str>,
) -> bool {
needles
.into_iter()
.any(|item| item.eq_ignore_ascii_case(haystack))
}
#[cfg(test)]
mod test {
use super::{Credentials, Mechanism};
@@ -153,6 +168,23 @@ mod test {
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_login_case_insensitive() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
assert_eq!(
mechanism.response(&credentials, Some("username")).unwrap(),
"alice"
);
assert_eq!(
mechanism.response(&credentials, Some("password")).unwrap(),
"wonderland"
);
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;

View File

@@ -6,7 +6,8 @@ use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use super::async_net::AsyncTokioStream;
#[cfg(feature = "tracing")]
use super::escape_crlf;
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
#[allow(deprecated)]
use super::{async_net::AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
@@ -35,6 +36,7 @@ macro_rules! try_smtp (
pub struct AsyncSmtpConnection {
/// TCP stream between client and server
/// Value is None before connection
#[allow(deprecated)]
stream: BufReader<AsyncNetworkStream>,
/// Panic state
panic: bool,
@@ -52,10 +54,12 @@ impl AsyncSmtpConnection {
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_with_transport(
stream: Box<dyn AsyncTokioStream>,
hello_name: &ClientId,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(deprecated)]
let stream = AsyncNetworkStream::use_existing_tokio1(stream);
Self::connect_impl(stream, hello_name).await
}
@@ -65,7 +69,7 @@ impl AsyncSmtpConnection {
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
/// referred to as `SMTPS`). See also [`AsyncSmtpConnection::starttls`].
///
/// If `local_addres` is `Some`, then the address provided shall be used to bind the
/// 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
@@ -86,12 +90,12 @@ impl AsyncSmtpConnection {
/// Some(TlsParameters::new("example.com".to_owned())?),
/// None,
/// )
/// .await
/// .unwrap();
/// .await?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
@@ -99,6 +103,7 @@ impl AsyncSmtpConnection {
tls_parameters: Option<TlsParameters>,
local_address: Option<IpAddr>,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(deprecated)]
let stream =
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
.await?;
@@ -109,16 +114,19 @@ impl AsyncSmtpConnection {
///
/// Sends EHLO and parses server information
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(deprecated)]
let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
#[allow(deprecated)]
async fn connect_impl(
stream: AsyncNetworkStream,
hello_name: &ClientId,
@@ -246,6 +254,7 @@ impl AsyncSmtpConnection {
}
/// Sets the underlying stream
#[allow(deprecated)]
pub fn set_stream(&mut self, stream: AsyncNetworkStream) {
self.stream = BufReader::new(stream);
}
@@ -369,8 +378,36 @@ impl AsyncSmtpConnection {
}
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
/// Currently this is only avaialable when using Boring TLS and
/// returns the result of the verification of the TLS certificate
/// presented by the peer, if any. Only the last error encountered
/// during verification is presented.
/// It can be useful when you don't want to fail outright the TLS
/// negotiation, for example when a self-signed certificate is
/// encountered, but still want to record metrics or log the fact.
/// When using DANE verification, the PKI root of trust moves from
/// the CAs to DNS, so self-signed certificates are permitted as long
/// as the TLSA records match the leaf or issuer certificates.
/// It cannot be called on non Boring TLS streams.
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn tls_verify_result(&self) -> Result<(), Error> {
self.stream.get_ref().tls_verify_result()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
}
}

View File

@@ -6,16 +6,16 @@ use std::{
time::Duration,
};
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "async-std1")]
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError,
Result as IoResult,
};
#[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "async-std1-rustls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsStream;
#[cfg(any(feature = "tokio1-rustls", feature = "async-std1-rustls"))]
use rustls::pki_types::ServerName;
#[cfg(feature = "tokio1-boring-tls")]
use tokio1_boring::SslStream as Tokio1SslStream;
#[cfg(feature = "tokio1")]
@@ -27,15 +27,14 @@ use tokio1_crate::net::{
};
#[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(feature = "tokio1-rustls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsStream;
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-rustls",
feature = "tokio1-boring-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
feature = "async-std1-rustls"
))]
use super::InnerTlsParameters;
use super::TlsParameters;
@@ -45,6 +44,10 @@ use crate::transport::smtp::{error, Error};
/// A network stream
#[derive(Debug)]
#[deprecated(
since = "0.11.14",
note = "This struct was not meant to be made public"
)]
pub struct AsyncNetworkStream {
inner: InnerAsyncNetworkStream,
}
@@ -75,8 +78,8 @@ enum InnerAsyncNetworkStream {
#[cfg(feature = "tokio1-native-tls")]
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-rustls-tls")]
Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
#[cfg(feature = "tokio1-rustls")]
Tokio1Rustls(Tokio1RustlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-boring-tls")]
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
@@ -84,15 +87,13 @@ enum InnerAsyncNetworkStream {
#[cfg(feature = "async-std1")]
AsyncStd1Tcp(AsyncStd1TcpStream),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-native-tls")]
AsyncStd1NativeTls(AsyncStd1TlsStream<AsyncStd1TcpStream>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-rustls-tls")]
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
#[cfg(feature = "async-std1-rustls")]
AsyncStd1Rustls(AsyncStd1RustlsStream<AsyncStd1TcpStream>),
/// Can't be built
None,
}
#[allow(deprecated)]
impl AsyncNetworkStream {
fn new(inner: InnerAsyncNetworkStream) -> Self {
if let InnerAsyncNetworkStream::None = inner {
@@ -104,27 +105,24 @@ impl AsyncNetworkStream {
/// Returns peer's address
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
match self.inner {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
InnerAsyncNetworkStream::Tokio1Tcp(s) => s.peer_addr(),
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref s) => {
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref s) => s.get_ref().peer_addr(),
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Err(IoError::new(
ErrorKind::Other,
Err(IoError::other(
"InnerAsyncNetworkStream::None must never be built",
))
}
@@ -132,11 +130,13 @@ impl AsyncNetworkStream {
}
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
}
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
@@ -176,7 +176,7 @@ impl AsyncNetworkStream {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
))
));
}
}
} else {
@@ -203,6 +203,7 @@ impl AsyncNetworkStream {
}
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
@@ -228,7 +229,7 @@ impl AsyncNetworkStream {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
))
));
}
}
}
@@ -259,26 +260,25 @@ impl AsyncNetworkStream {
feature = "tokio1",
not(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-rustls",
feature = "tokio1-boring-tls"
))
))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls feature");
}
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-rustls",
feature = "tokio1-boring-tls"
))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
// get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
let InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) = tcp_stream else {
unreachable!()
};
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
@@ -286,22 +286,18 @@ impl AsyncNetworkStream {
.map_err(error::connection)?;
Ok(())
}
#[cfg(all(
feature = "async-std1",
not(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))
))]
#[cfg(all(feature = "async-std1", not(feature = "async-std1-rustls")))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled 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(_) => {
// get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
let InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) = tcp_stream else {
unreachable!()
};
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
@@ -316,7 +312,7 @@ impl AsyncNetworkStream {
#[allow(unused_variables)]
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-rustls",
feature = "tokio1-boring-tls"
))]
async fn upgrade_tokio1_tls(
@@ -327,7 +323,7 @@ impl AsyncNetworkStream {
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
InnerTlsParameters::NativeTls { connector } => {
#[cfg(not(feature = "tokio1-native-tls"))]
panic!("built without the tokio1-native-tls feature");
@@ -343,14 +339,13 @@ impl AsyncNetworkStream {
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio1-rustls-tls"))]
panic!("built without the tokio1-rustls-tls feature");
#[cfg(feature = "rustls")]
InnerTlsParameters::Rustls { config } => {
#[cfg(not(feature = "tokio1-rustls"))]
panic!("built without the tokio1-rustls feature");
#[cfg(feature = "tokio1-rustls-tls")]
#[cfg(feature = "tokio1-rustls")]
return {
use rustls::ServerName;
use tokio1_rustls::TlsConnector;
let domain = ServerName::try_from(domain.as_str())
@@ -358,21 +353,24 @@ impl AsyncNetworkStream {
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.connect(domain.to_owned(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
Ok(InnerAsyncNetworkStream::Tokio1Rustls(stream))
};
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
InnerTlsParameters::BoringTls {
connector,
accept_invalid_hostnames,
} => {
#[cfg(not(feature = "tokio1-boring-tls"))]
panic!("built without the tokio1-boring-tls feature");
#[cfg(feature = "tokio1-boring-tls")]
return {
let mut config = connector.configure().map_err(error::connection)?;
config.set_verify_hostname(tls_parameters.accept_invalid_hostnames);
config.set_verify_hostname(accept_invalid_hostnames);
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
.await
@@ -384,11 +382,7 @@ impl AsyncNetworkStream {
}
#[allow(unused_variables)]
#[cfg(any(
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls",
feature = "async-std1-boring-tls"
))]
#[cfg(feature = "async-std1-rustls")]
async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters,
@@ -397,73 +391,122 @@ impl AsyncNetworkStream {
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
InnerTlsParameters::NativeTls { connector } => {
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
/*
#[cfg(not(feature = "async-std1-native-tls"))]
panic!("built without the async-std1-native-tls feature");
#[cfg(feature = "async-std1-native-tls")]
return {
use async_native_tls::TlsConnector;
// TODO: fix
let connector: TlsConnector = todo!();
// let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::AsyncStd1NativeTls(stream))
};
*/
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "async-std1-rustls-tls"))]
panic!("built without the async-std1-rustls-tls feature");
#[cfg(feature = "rustls")]
InnerTlsParameters::Rustls { config } => {
#[cfg(not(feature = "async-std1-rustls"))]
panic!("built without the async-std1-rustls feature");
#[cfg(feature = "async-std1-rustls-tls")]
#[cfg(feature = "async-std1-rustls")]
return {
use futures_rustls::TlsConnector;
use rustls::ServerName;
let domain = ServerName::try_from(domain.as_str())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.connect(domain.to_owned(), tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
Ok(InnerAsyncNetworkStream::AsyncStd1Rustls(stream))
};
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
InnerTlsParameters::BoringTls { .. } => {
panic!("boring-tls isn't supported with async-std yet.");
}
}
}
pub fn is_encrypted(&self) -> bool {
match self.inner {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(_) => true,
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => true,
InnerAsyncNetworkStream::None => false,
}
}
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn tls_verify_result(&self) -> Result<(), Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => {
stream.ssl().verify_result().map_err(error::tls)
}
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => panic!("Unsupported"),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
.peer_cert_chain()
.unwrap()
.iter()
.map(|c| c.to_der().map_err(error::tls))
.collect::<Result<Vec<_>, _>>()?),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
@@ -478,16 +521,15 @@ impl AsyncNetworkStream {
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
@@ -499,32 +541,30 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(t) => panic!("Unsupported"),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
}
#[allow(deprecated)]
impl FuturesAsyncRead for AsyncNetworkStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<IoResult<usize>> {
match self.inner {
match &mut self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => {
InnerAsyncNetworkStream::Tokio1Tcp(s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -533,7 +573,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
}
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => {
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -541,8 +581,8 @@ impl FuturesAsyncRead for AsyncNetworkStream {
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => {
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -551,7 +591,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
}
}
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => {
InnerAsyncNetworkStream::Tokio1BoringTls(s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
@@ -560,15 +600,9 @@ impl FuturesAsyncRead for AsyncNetworkStream {
}
}
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
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::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_read(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
@@ -577,31 +611,26 @@ impl FuturesAsyncRead for AsyncNetworkStream {
}
}
#[allow(deprecated)]
impl FuturesAsyncWrite for AsyncNetworkStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<IoResult<usize>> {
match self.inner {
match &mut self.inner {
#[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")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
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::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
@@ -610,21 +639,19 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner {
match &mut self.inner {
#[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")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut 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::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
@@ -633,21 +660,19 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner {
match &mut self.inner {
#[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")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-rustls")]
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut 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::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-rustls")]
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))

View File

@@ -143,7 +143,7 @@ impl SmtpConnection {
hello_name: &ClientId,
) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
{
try_smtp!(self.command(Starttls), self);
self.stream.get_mut().upgrade_tls(tls_parameters)?;
@@ -153,11 +153,7 @@ impl SmtpConnection {
try_smtp!(self.ehlo(hello_name), self);
Ok(())
}
#[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
// This should never happen as `Tls` can only be created
// when a TLS library is enabled
unreachable!("TLS support required but not supported");
@@ -303,8 +299,36 @@ impl SmtpConnection {
}
/// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
/// Currently this is only avaialable when using Boring TLS and
/// returns the result of the verification of the TLS certificate
/// presented by the peer, if any. Only the last error encountered
/// during verification is presented.
/// It can be useful when you don't want to fail outright the TLS
/// negotiation, for example when a self-signed certificate is
/// encountered, but still want to record metrics or log the fact.
/// When using DANE verification, the PKI root of trust moves from
/// the CAs to DNS, so self-signed certificates are permitted as long
/// as the TLSA records match the leaf or issuer certificates.
/// It cannot be called on non Boring TLS streams.
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn tls_verify_result(&self) -> Result<(), Error> {
self.stream.get_ref().tls_verify_result()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
}
}

View File

@@ -28,17 +28,18 @@ use std::fmt::Debug;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[allow(deprecated)]
pub use self::async_net::AsyncNetworkStream;
#[cfg(feature = "tokio1")]
pub use self::async_net::AsyncTokioStream;
use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
pub(super) use self::tls::InnerTlsParameters;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
pub use self::tls::TlsVersion;
pub use self::{
connection::SmtpConnection,
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
tls::{Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder},
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
@@ -57,7 +58,7 @@ struct ClientCodec {
impl ClientCodec {
/// Creates a new client codec
pub fn new() -> Self {
pub(crate) fn new() -> Self {
Self {
status: CodecStatus::StartOfNewLine,
}
@@ -139,7 +140,7 @@ mod test {
}
#[test]
#[cfg(feature = "log")]
#[cfg(feature = "tracing")]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");

View File

@@ -1,4 +1,4 @@
#[cfg(feature = "rustls-tls")]
#[cfg(feature = "rustls")]
use std::sync::Arc;
use std::{
io::{self, Read, Write},
@@ -11,11 +11,11 @@ use std::{
use boring::ssl::SslStream;
#[cfg(feature = "native-tls")]
use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConnection, ServerName, StreamOwned};
#[cfg(feature = "rustls")]
use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
use super::InnerTlsParameters;
use super::TlsParameters;
use crate::transport::smtp::{error, Error};
@@ -36,8 +36,8 @@ enum InnerNetworkStream {
#[cfg(feature = "native-tls")]
NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream
#[cfg(feature = "rustls-tls")]
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
#[cfg(feature = "rustls")]
Rustls(StreamOwned<ClientConnection, TcpStream>),
#[cfg(feature = "boring-tls")]
BoringTls(SslStream<TcpStream>),
/// Can't be built
@@ -55,14 +55,14 @@ impl NetworkStream {
/// Returns peer's address
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
match self.inner {
InnerNetworkStream::Tcp(ref s) => s.peer_addr(),
match &self.inner {
InnerNetworkStream::Tcp(s) => s.peer_addr(),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(SocketAddr::V4(SocketAddrV4::new(
@@ -75,14 +75,14 @@ impl NetworkStream {
/// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
match self.inner {
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
match &self.inner {
InnerNetworkStream::Tcp(s) => s.shutdown(how),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(s) => s.get_ref().shutdown(how),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
@@ -119,12 +119,12 @@ impl NetworkStream {
if let Some(timeout) = timeout {
match socket.connect_timeout(&addr.into(), timeout) {
Ok(_) => return Ok(socket.into()),
Ok(()) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
} else {
match socket.connect(&addr.into()) {
Ok(_) => return Ok(socket.into()),
Ok(()) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
}
@@ -146,23 +146,18 @@ impl NetworkStream {
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
match &self.inner {
#[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
InnerNetworkStream::Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
panic!("Trying to upgrade an NetworkStream without having enabled either the `native-tls` or the `rustls` feature");
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
InnerNetworkStream::Tcp(_) => {
// get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
let InnerNetworkStream::Tcp(tcp_stream) = tcp_stream else {
unreachable!()
};
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
@@ -172,34 +167,37 @@ impl NetworkStream {
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
fn upgrade_tls_impl(
tcp_stream: TcpStream,
tls_parameters: &TlsParameters,
) -> Result<InnerNetworkStream, Error> {
Ok(match &tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
InnerTlsParameters::NativeTls { connector } => {
let stream = connector
.connect(tls_parameters.domain(), tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::NativeTls(stream)
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(connector) => {
#[cfg(feature = "rustls")]
InnerTlsParameters::Rustls { config } => {
let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connection = ClientConnection::new(Arc::clone(connector), domain)
let connection = ClientConnection::new(Arc::clone(config), domain.to_owned())
.map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream)
InnerNetworkStream::Rustls(stream)
}
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
InnerTlsParameters::BoringTls {
connector,
accept_invalid_hostnames,
} => {
let stream = connector
.configure()
.map_err(error::connection)?
.verify_hostname(tls_parameters.accept_invalid_hostnames)
.verify_hostname(*accept_invalid_hostnames)
.connect(tls_parameters.domain(), tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::BoringTls(stream)
@@ -208,12 +206,12 @@ impl NetworkStream {
}
pub fn is_encrypted(&self) -> bool {
match self.inner {
match &self.inner {
InnerNetworkStream::Tcp(_) => false,
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(_) => true,
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(_) => true,
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(_) => true,
InnerNetworkStream::None => {
@@ -223,7 +221,55 @@ impl NetworkStream {
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn tls_verify_result(&self) -> Result<(), Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(_) => panic!("Unsupported"),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => {
stream.ssl().verify_result().map_err(error::tls)
}
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
.peer_cert_chain()
.unwrap()
.iter()
.map(|c| c.to_der().map_err(error::tls))
.collect::<Result<Vec<_>, _>>()?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
@@ -234,15 +280,14 @@ impl NetworkStream {
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(stream) => Ok(stream
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
@@ -255,20 +300,14 @@ impl NetworkStream {
}
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
match &mut self.inner {
InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_read_timeout(duration),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
@@ -278,21 +317,15 @@ impl NetworkStream {
/// Set write timeout for IO calls
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
match &mut self.inner {
InnerNetworkStream::Tcp(stream) => stream.set_write_timeout(duration),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_write_timeout(duration),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
@@ -303,14 +336,14 @@ impl NetworkStream {
impl Read for NetworkStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.read(buf),
match &mut self.inner {
InnerNetworkStream::Tcp(s) => s.read(buf),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
InnerNetworkStream::NativeTls(s) => s.read(buf),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(s) => s.read(buf),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.read(buf),
InnerNetworkStream::BoringTls(s) => s.read(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
@@ -321,14 +354,14 @@ impl Read for NetworkStream {
impl Write for NetworkStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.write(buf),
match &mut self.inner {
InnerNetworkStream::Tcp(s) => s.write(buf),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
InnerNetworkStream::NativeTls(s) => s.write(buf),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(s) => s.write(buf),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.write(buf),
InnerNetworkStream::BoringTls(s) => s.write(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
@@ -337,14 +370,14 @@ impl Write for NetworkStream {
}
fn flush(&mut self) -> io::Result<()> {
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.flush(),
match &mut self.inner {
InnerNetworkStream::Tcp(s) => s.flush(),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
InnerNetworkStream::NativeTls(s) => s.flush(),
#[cfg(feature = "rustls")]
InnerNetworkStream::Rustls(s) => s.flush(),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.flush(),
InnerNetworkStream::BoringTls(s) => s.flush(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
@@ -356,7 +389,7 @@ impl Write for NetworkStream {
/// If the local address is set, binds the socket to this address.
/// If local address is not set, then destination address is required to determine the default
/// local address on some platforms.
/// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560
/// See: <https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560>
fn bind_local_address(
socket: &socket2::Socket,
dst_addr: &SocketAddr,

View File

@@ -1,27 +1,31 @@
use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")]
use std::{sync::Arc, time::SystemTime};
#[cfg(feature = "rustls")]
use std::sync::Arc;
#[cfg(feature = "boring-tls")]
use boring::{
pkey::PKey,
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
#[cfg(feature = "rustls")]
use rustls::{
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
ClientConfig, Error as TlsError, RootCertStore, ServerName,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider},
pki_types::{self, pem::PemObject, CertificateDer, PrivateKeyDer, ServerName, UnixTime},
server::ParsedCertificate,
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
use crate::transport::smtp::{error, Error};
/// TLS protocol versions.
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
pub enum TlsVersion {
/// TLS 1.0
///
@@ -54,31 +58,75 @@ pub enum TlsVersion {
Tlsv13,
}
/// How to apply TLS to a client connection
/// Specifies how to establish a TLS connection
///
/// TLDR: Use [`Tls::Wrapper`] or [`Tls::Required`] when
/// connecting to a remote server, [`Tls::None`] when
/// connecting to a local server.
#[derive(Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `Tls` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub enum Tls {
/// Insecure connection only (for testing purposes)
/// Insecure (plaintext) connection only.
///
/// This option **always** uses a plaintext connection and should only
/// be used for trusted local relays. It is **highly discouraged**
/// for remote servers, as it exposes credentials and emails to potential
/// interception.
///
/// Note: Servers requiring credentials or emails to be sent over TLS
/// may reject connections when this option is used.
None,
/// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
/// Begin with a plaintext connection and attempt to use `STARTTLS` if available.
///
/// lettre will try to upgrade to a TLS-secured connection but will fall back
/// to plaintext if the server does not support TLS. This option is provided for
/// compatibility but is **strongly discouraged**, as it exposes connections to
/// potential MITM (man-in-the-middle) attacks.
///
/// Warning: A malicious intermediary could intercept the `STARTTLS` flag,
/// causing lettre to believe the server only supports plaintext connections.
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
Opportunistic(TlsParameters),
/// Start with insecure connection and require `STARTTLS`
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
/// Begin with a plaintext connection and require `STARTTLS` for security.
///
/// lettre will upgrade plaintext TCP connections to TLS before transmitting
/// any sensitive data. If the server does not support TLS, the connection
/// attempt will fail, ensuring no credentials or emails are sent in plaintext.
///
/// Unlike [`Tls::Opportunistic`], this option is secure against MITM attacks.
/// For optimal security and performance, consider using [`Tls::Wrapper`] instead,
/// as it requires fewer roundtrips to establish a secure connection.
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
Required(TlsParameters),
/// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
/// Establish a connection wrapped in TLS from the start.
///
/// lettre connects to the server and immediately performs a TLS handshake.
/// If the handshake fails, the connection attempt is aborted without
/// transmitting any sensitive data.
///
/// This is the fastest and most secure option for establishing a connection.
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
Wrapper(TlsParameters),
}
@@ -87,11 +135,11 @@ impl Debug for Tls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::None => f.pad("None"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"),
}
}
@@ -100,14 +148,25 @@ impl Debug for Tls {
/// Source for the base set of root certificates to trust.
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug, Default)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `CertificateStore` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub enum CertificateStore {
/// Use the default for the TLS backend.
///
/// For native-tls, this will use the system certificate store on Windows, the keychain on
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
///
/// For rustls, this will also use the the system store if the `rustls-native-certs` feature is
/// enabled, or will fall back to `webpki-roots`.
/// For rustls, this will use the system certificate verifier if the `rustls-platform-verifier`
/// feature is enabled. If the `rustls-native-certs` feature is enabled, system certificate
/// store will be used. Otherwise, it will fall back to `webpki-roots`.
///
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
#[default]
@@ -115,7 +174,7 @@ pub enum CertificateStore {
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
///
/// This option is only available in the rustls backend.
#[cfg(feature = "rustls-tls")]
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
WebpkiRoots,
/// Don't use any system certificates.
None,
@@ -123,23 +182,42 @@ pub enum CertificateStore {
/// Parameters to use for secure clients
#[derive(Clone)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `TlsParameters` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub struct TlsParameters {
pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server
pub(super) domain: String,
#[cfg(feature = "boring-tls")]
pub(super) accept_invalid_hostnames: bool,
}
/// Builder for `TlsParameters`
#[derive(Debug, Clone)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `TlsParametersBuilder` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub struct TlsParametersBuilder {
domain: String,
cert_store: CertificateStore,
root_certs: Vec<Certificate>,
identity: Option<Identity>,
accept_invalid_hostnames: bool,
accept_invalid_certs: bool,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
min_tls_version: TlsVersion,
}
@@ -150,9 +228,10 @@ impl TlsParametersBuilder {
domain,
cert_store: CertificateStore::Default,
root_certs: Vec::new(),
identity: None,
accept_invalid_hostnames: false,
accept_invalid_certs: false,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
min_tls_version: TlsVersion::Tlsv12,
}
}
@@ -165,14 +244,24 @@ impl TlsParametersBuilder {
/// 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 {
self.root_certs.push(cert);
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
///
/// This option is silently disabled when using `rustls-platform-verifier`.
///
/// Defaults to `false`.
///
/// # Warning
@@ -182,10 +271,11 @@ impl TlsParametersBuilder {
/// including those from other sites, are trusted.
///
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
///
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(any(feature = "native-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
@@ -194,7 +284,11 @@ impl TlsParametersBuilder {
/// Controls which minimum TLS version is allowed
///
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
self.min_tls_version = min_tls_version;
self
@@ -223,17 +317,17 @@ impl TlsParametersBuilder {
/// Creates a new `TlsParameters` using native-tls, boring-tls or rustls
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn build(self) -> Result<TlsParameters, Error> {
#[cfg(feature = "rustls-tls")]
#[cfg(feature = "rustls")]
return self.build_rustls();
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
#[cfg(all(not(feature = "rustls"), feature = "native-tls"))]
return self.build_native();
#[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
#[cfg(all(not(feature = "rustls"), feature = "boring-tls"))]
return self.build_boring();
}
@@ -273,12 +367,14 @@ impl TlsParametersBuilder {
};
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)?;
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector),
connector: InnerTlsParameters::NativeTls { connector },
domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
}
@@ -315,6 +411,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 {
TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
@@ -327,18 +432,18 @@ impl TlsParametersBuilder {
.map_err(error::tls)?;
let connector = tls_builder.build();
Ok(TlsParameters {
connector: InnerTlsParameters::BoringTls(connector),
connector: InnerTlsParameters::BoringTls {
connector,
accept_invalid_hostnames: self.accept_invalid_hostnames,
},
domain: self.domain,
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
}
/// Creates a new `TlsParameters` using rustls with the provided configuration
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
let tls = ClientConfig::builder();
let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version {
TlsVersion::Tlsv10 => {
@@ -351,105 +456,144 @@ impl TlsParametersBuilder {
TlsVersion::Tlsv13 => just_version3,
};
let tls = tls
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
let crypto_provider = crate::rustls_crypto::crypto_provider();
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
let tls = if self.accept_invalid_certs {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
// Build TLS config
let mut root_cert_store = RootCertStore::empty();
#[cfg(all(
not(feature = "rustls-platform-verifier"),
feature = "rustls-native-certs"
))]
fn load_native_roots(store: &mut RootCertStore) {
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
let errors_len = errors.len();
let (added, ignored) = store.add_parsable_certificates(certs);
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
);
#[cfg(not(feature = "tracing"))]
let _ = (errors_len, added, ignored);
}
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
fn load_webpki_roots(store: &mut RootCertStore) {
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
#[cfg_attr(not(feature = "rustls-platform-verifier"), allow(unused_mut))]
let mut extra_roots = None::<Vec<CertificateDer<'static>>>;
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-platform-verifier")]
{
extra_roots = Some(Vec::new());
}
#[cfg(all(
not(feature = "rustls-platform-verifier"),
feature = "rustls-native-certs"
))]
load_native_roots(&mut root_cert_store);
#[cfg(all(
not(feature = "rustls-platform-verifier"),
not(feature = "rustls-native-certs"),
feature = "webpki-roots"
))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
#[cfg(feature = "rustls-platform-verifier")]
if let Some(extra_roots) = &mut extra_roots {
extra_roots.push(rustls_cert.clone());
}
root_cert_store.add(rustls_cert).map_err(error::tls)?;
}
}
let tls = if self.accept_invalid_certs
|| (extra_roots.is_none() && self.accept_invalid_hostnames)
{
let verifier = InvalidCertsVerifier {
ignore_invalid_hostnames: self.accept_invalid_hostnames,
ignore_invalid_certs: self.accept_invalid_certs,
roots: root_cert_store,
crypto_provider,
};
tls.dangerous()
.with_custom_certificate_verifier(Arc::new(verifier))
} else {
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
let mut valid_count = 0;
let mut invalid_count = 0;
for cert in native_certs {
match store.add(&rustls::Certificate(cert.0)) {
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 = "rustls-tls")]
fn load_webpki_roots(store: &mut RootCertStore) {
// TODO: handle this in the rustls 0.22 upgrade
#[allow(deprecated)]
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
#[cfg(feature = "rustls-platform-verifier")]
if let Some(extra_roots) = extra_roots {
tls.dangerous().with_custom_certificate_verifier(Arc::new(
rustls_platform_verifier::Verifier::new_with_extra_roots(
extra_roots,
crypto_provider,
)
}));
.map_err(error::tls)?,
))
} else {
tls.with_root_certificates(root_cert_store)
}
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store)?;
#[cfg(not(feature = "rustls-native-certs"))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(feature = "rustls-tls")]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
#[cfg(not(feature = "rustls-platform-verifier"))]
{
tls.with_root_certificates(root_cert_store)
}
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();
let tls = if let Some(identity) = self.identity {
let (client_certificates, private_key) = identity.rustls_tls;
tls.with_client_auth_cert(client_certificates, private_key)
.map_err(error::tls)?
} else {
tls.with_no_client_auth()
};
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
connector: InnerTlsParameters::Rustls {
config: Arc::new(tls),
},
domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
}
}
#[derive(Clone)]
#[allow(clippy::enum_variant_names)]
pub enum InnerTlsParameters {
pub(crate) enum InnerTlsParameters {
#[cfg(feature = "native-tls")]
NativeTls(TlsConnector),
#[cfg(feature = "rustls-tls")]
RustlsTls(Arc<ClientConfig>),
NativeTls { connector: TlsConnector },
#[cfg(feature = "rustls")]
Rustls { config: Arc<ClientConfig> },
#[cfg(feature = "boring-tls")]
BoringTls(SslConnector),
BoringTls {
connector: SslConnector,
accept_invalid_hostnames: bool,
},
}
impl TlsParameters {
/// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn new(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build()
@@ -468,8 +612,8 @@ impl TlsParameters {
}
/// Creates a new `TlsParameters` using rustls
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
#[cfg(feature = "rustls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn new_rustls(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_rustls()
}
@@ -486,19 +630,29 @@ 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)]
#[allow(missing_copy_implementations)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `Certificate` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub struct Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>,
#[cfg(feature = "rustls")]
rustls: Vec<CertificateDer<'static>>,
#[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509,
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
impl Certificate {
/// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
@@ -511,8 +665,8 @@ impl Certificate {
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)],
#[cfg(feature = "rustls")]
rustls: vec![der.into()],
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
})
@@ -526,22 +680,17 @@ impl Certificate {
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls-tls")]
#[cfg(feature = "rustls")]
let rustls_cert = {
use std::io::Cursor;
let mut pem = Cursor::new(pem);
rustls_pemfile::certs(&mut pem)
CertificateDer::pem_slice_iter(pem)
.collect::<Result<Vec<_>, pki_types::pem::Error>>()
.map_err(|_| error::tls("invalid certificates"))?
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>()
};
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
#[cfg(feature = "rustls")]
rustls: rustls_cert,
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
@@ -555,20 +704,159 @@ impl Debug for Certificate {
}
}
#[cfg(feature = "rustls-tls")]
struct InvalidCertsVerifier;
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
#[allow(missing_copy_implementations)]
#[cfg_attr(
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
deprecated(
note = "starting from lettre v0.12 `Identity` won't be available when none of the TLS backends are enabled"
)
)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub struct Identity {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Identity,
#[cfg(feature = "rustls")]
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 {
fn verify_server_cert(
&self,
_end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: SystemTime,
now: UnixTime,
) -> 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())
}
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()
}
}

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ use std::{
collections::HashSet,
fmt::{self, Display, Formatter},
net::{Ipv4Addr, Ipv6Addr},
result::Result,
};
use crate::transport::smtp::{
@@ -53,10 +52,10 @@ impl Default for ClientId {
impl Display for ClientId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::Domain(ref value) => f.write_str(value),
Self::Ipv4(ref value) => write!(f, "[{value}]"),
Self::Ipv6(ref value) => write!(f, "[IPv6:{value}]"),
match self {
Self::Domain(value) => f.write_str(value),
Self::Ipv4(value) => write!(f, "[{value}]"),
Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
}
}
}
@@ -93,11 +92,11 @@ pub enum Extension {
impl Display for Extension {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
match self {
Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
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 {
/// Parses a EHLO response to create a `ServerInfo`
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(error::response("Could not read server name")),
let Some(name) = response.first_word() else {
return Err(error::response("Could not read server name"));
};
let mut features: HashSet<Extension> = HashSet::new();
@@ -170,7 +168,7 @@ impl ServerInfo {
}
}
_ => (),
};
}
}
Ok(ServerInfo {
@@ -227,16 +225,16 @@ pub enum MailParameter {
impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
MailParameter::Body(ref value) => write!(f, "BODY={value}"),
match self {
MailParameter::Body(value) => write!(f, "BODY={value}"),
MailParameter::Size(size) => write!(f, "SIZE={size}"),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other {
ref keyword,
value: Some(ref value),
keyword,
value: Some(value),
} => write!(f, "{}={}", keyword, XText(value)),
MailParameter::Other {
ref keyword,
keyword,
value: None,
} => f.write_str(keyword),
}
@@ -277,13 +275,13 @@ pub enum RcptParameter {
impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
match &self {
RcptParameter::Other {
ref keyword,
value: Some(ref value),
keyword,
value: Some(value),
} => write!(f, "{keyword}={}", XText(value)),
RcptParameter::Other {
ref keyword,
keyword,
value: None,
} => f.write_str(keyword),
}
@@ -292,14 +290,8 @@ impl Display for RcptParameter {
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::*;
use crate::transport::smtp::{
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
};
use crate::transport::smtp::response::{Category, Code, Detail, Severity};
#[test]
fn test_clientid_fmt() {

View File

@@ -26,43 +26,17 @@
//!
//! The relay server can be the local email server, a specific host or a third-party service.
//!
//! #### Simple example
//! #### Simple example with authentication
//!
//! This is the most basic example of usage:
//! A good starting point for sending emails via SMTP relay is to
//! do the following:
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, SmtpTransport, Transport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")?.build();
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! #### Authentication
//!
//! Example with authentication and connection pool:
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{
//! transport::smtp::{
//! authentication::{Credentials, Mechanism},
//! PoolConfig,
//! },
//! message::header::ContentType,
//! transport::smtp::authentication::{Credentials, Mechanism},
//! Message, SmtpTransport, Transport,
//! };
//!
@@ -71,35 +45,39 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! // Create TLS transport on port 587 with STARTTLS
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Create the SMTPS transport
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! // Add credentials for authentication
//! .credentials(Credentials::new(
//! "username".to_owned(),
//! "password".to_owned(),
//! ))
//! // Configure expected authentication mechanism
//! // Optionally configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain])
//! // Connection pool settings
//! .pool_config(PoolConfig::new().max_size(20))
//! .build();
//!
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! sender.send(&email)?;
//! # Ok(())
//! # }
//! ```
//!
//! You can specify custom TLS settings:
//! #### Shortening configuration
//!
//! It can be very repetitive to ask the user for every SMTP connection parameter.
//! In some cases this can be simplified by using a connection URI instead.
//!
//! For more information take a look at [`SmtpTransport::from_url`] or [`AsyncSmtpTransport::from_url`].
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{
//! transport::smtp::client::{Tls, TlsParameters},
//! message::header::ContentType,
//! transport::smtp::authentication::{Credentials, Mechanism},
//! Message, SmtpTransport, Transport,
//! };
//!
@@ -108,25 +86,106 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_owned())
//! .dangerous_accept_invalid_certs(true)
//! // Create the SMTPS transport
//! let sender = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
//!
//! // Send the email via remote relay
//! sender.send(&email)?;
//! # Ok(())
//! # }
//! ```
//!
//! #### Advanced configuration with custom TLS settings
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use std::fs;
//!
//! use lettre::{
//! message::header::ContentType,
//! transport::smtp::client::{Certificate, Tls, TlsParameters},
//! Message, SmtpTransport, Transport,
//! };
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! // Custom TLS configuration - Use a self signed certificate
//! let cert = fs::read("self-signed.crt")?;
//! let cert = Certificate::from_pem(&cert)?;
//! let tls = TlsParameters::builder(/* TLS SNI value */ "smtp.example.com".to_owned())
//! .add_root_certificate(cert)
//! .build()?;
//!
//! // Create TLS transport on port 465
//! // Create the SMTPS transport
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! // Custom TLS configuration
//! .tls(Tls::Required(tls))
//! .tls(Tls::Wrapper(tls))
//! .build();
//!
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! sender.send(&email)?;
//! # Ok(())
//! # }
//! ```
//!
//! #### Connection pooling
//!
//! [`SmtpTransport`] and [`AsyncSmtpTransport`] store connections in
//! a connection pool by default. This avoids connecting and disconnecting
//! from the relay server for every message the application tries to send. For the connection pool
//! to work the instance of the transport **must** be reused.
//! In a webserver context it may go about this:
//!
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
//! # fn test() {
//! use lettre::{
//! message::header::ContentType,
//! transport::smtp::{authentication::Credentials, PoolConfig},
//! Message, SmtpTransport, Transport,
//! };
//! #
//! # type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
//!
//! /// The global application state
//! #[derive(Debug)]
//! struct AppState {
//! smtp: SmtpTransport,
//! // ... other global application parameters
//! }
//!
//! impl AppState {
//! pub fn new(smtp_url: &str) -> Result<Self> {
//! let smtp = SmtpTransport::from_url(smtp_url)?.build();
//! Ok(Self { smtp })
//! }
//! }
//!
//! fn handle_request(app_state: &AppState) -> Result<String> {
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?;
//!
//! // Send the email via remote relay
//! app_state.smtp.send(&email)?;
//!
//! Ok("The email has successfully been sent!".to_owned())
//! }
//! # }
//! ```
use std::time::Duration;
@@ -140,7 +199,7 @@ pub use self::{
error::Error,
transport::{SmtpTransport, SmtpTransportBuilder},
};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
use crate::transport::smtp::client::TlsParameters;
use crate::transport::smtp::{
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
string::ToString,
};
use nom::{
@@ -13,8 +12,8 @@ use nom::{
bytes::streaming::{tag, take_until},
combinator::{complete, map},
multi::many0,
sequence::{preceded, tuple},
IResult,
sequence::preceded,
IResult, Parser,
};
use crate::transport::smtp::{error, Error};
@@ -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
///
/// 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("4"), |_| Severity::TransientNegativeCompletion),
map(tag("5"), |_| Severity::PermanentNegativeCompletion),
))(i)
))
.parse(i)
}
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("4"), |_| Category::Unspecified4),
map(tag("5"), |_| Category::MailSystem),
))(i)
))
.parse(i)
}
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("8"), |_| Detail::Eight),
map(tag("9"), |_| Detail::Nine),
))(i)
))
.parse(i)
}
pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
let (i, lines) = many0(tuple((
let (i, lines) = many0((
parse_code,
preceded(tag("-"), take_until("\r\n")),
tag("\r\n"),
)))(i)?;
))
.parse(i)?;
let (i, (last_code, last_line)) =
tuple((parse_code, preceded(tag(" "), take_until("\r\n"))))(i)?;
let (i, _) = complete(tag("\r\n"))(i)?;
(parse_code, preceded(tag(" "), take_until("\r\n"))).parse(i)?;
let (i, _) = complete(tag("\r\n")).parse(i)?;
// Check that all codes are equal.
if !lines.iter().all(|&(code, _, _)| code == last_code) {
@@ -317,6 +326,17 @@ mod test {
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]
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";

View File

@@ -1,17 +1,41 @@
#[cfg(feature = "pool")]
use std::sync::Arc;
use std::time::Duration;
use std::{fmt::Debug, time::Duration};
#[cfg(feature = "pool")]
use super::pool::sync_impl::Pool;
#[cfg(feature = "pool")]
use super::PoolConfig;
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::{address::Envelope, Transport};
/// Sends emails using the SMTP protocol
/// Synchronously send emails using the SMTP protocol
///
/// `SmtpTransport` is the primary way for communicating
/// with SMTP relay servers to send email messages. It holds the
/// client connect configuration and creates new connections
/// as necessary.
///
/// # Connection pool
///
/// When the `pool` feature is enabled (default), `SmtpTransport` maintains a
/// connection pool to manage SMTP connections. The pool:
///
/// - Establishes a new connection when sending a message.
/// - Recycles connections internally after a message is sent.
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
///
/// The connection pool can grow to hold multiple SMTP connections if multiple
/// emails are sent concurrently, as SMTP does not support multiplexing within a
/// single connection.
///
/// However, **connection reuse is not possible** if the `SmtpTransport` instance
/// is dropped after every email send operation. You must reuse the instance
/// of this struct for the connection pool to be of any use.
///
/// To customize connection pool settings, use [`SmtpTransportBuilder::pool_config`].
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
#[derive(Clone)]
pub struct SmtpTransport {
@@ -32,10 +56,23 @@ impl Transport for SmtpTransport {
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "pool"))]
conn.quit()?;
conn.abort();
Ok(result)
}
fn shutdown(&self) {
#[cfg(feature = "pool")]
self.inner.shutdown();
}
}
impl Debug for SmtpTransport {
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 {
@@ -45,10 +82,10 @@ impl SmtpTransport {
///
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?;
@@ -69,10 +106,10 @@ impl SmtpTransport {
///
/// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?;
@@ -107,54 +144,72 @@ impl SmtpTransport {
/// Creates a `SmtpTransportBuilder` from a connection URL
///
/// The protocol, credentials, host and port can be provided in a single URL.
/// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
/// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
/// The path section of the url can be used to set an alternative name for
/// the HELO / EHLO command.
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
/// will set the HELO / EHLO name `client.example.com`.
/// The protocol, credentials, host, port and EHLO name can be provided
/// in a single URL. This may be simpler than having to configure SMTP
/// through multiple configuration parameters and then having to pass
/// those options to lettre.
///
/// <table>
/// <thead>
/// <tr>
/// <th>scheme</th>
/// <th>tls parameter</th>
/// <th>example</th>
/// <th>remarks</th>
/// </tr>
/// </thead>
/// <tbody>
/// <tr>
/// <td>smtps</td>
/// <td>-</td>
/// <td>smtps://smtp.example.com</td>
/// <td>SMTP over TLS, recommended method</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>required</td>
/// <td>smtp://smtp.example.com?tls=required</td>
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>opportunistic</td>
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
/// <td>
/// SMTP with optionally STARTTLS when supported by the server.
/// Caution: this method is vulnerable to a man-in-the-middle attack.
/// Not recommended for production use.
/// </td>
/// </tr>
/// <tr>
/// <td>smtp</td>
/// <td>-</td>
/// <td>smtp://smtp.example.com</td>
/// <td>Unencrypted SMTP, not recommended for production use.</td>
/// </tr>
/// </tbody>
/// </table>
/// The URL is created in the following way:
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
///
/// `user` (Username) and `pass` (Password) are optional in case the
/// SMTP relay doesn't require authentication. When `port` is not
/// configured it is automatically determined based on the `scheme`.
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
/// command. `TLS` controls whether STARTTLS is simply enabled
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
/// or `required` (require the server to upgrade the connection to
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
///
/// Use the following table to construct your SMTP url:
///
/// | scheme | `tls` query parameter | example | default port | remarks |
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
///
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
/// be concatenated to construct the final URL because special characters
/// contained within the parameter may confuse the URL decoder.
/// Manually URL encode the parameters before concatenating them or use
/// a proper URL encoder, like the following cargo script:
///
/// ```rust
/// # const TOML: &str = r#"
/// #!/usr/bin/env cargo
///
/// //! ```cargo
/// //! [dependencies]
/// //! url = "2"
/// //! ```
/// # "#;
///
/// use url::Url;
///
/// fn main() {
/// // don't touch this line
/// let mut url = Url::parse("foo://bar").unwrap();
///
/// // configure the scheme (`smtp` or `smtps`) here.
/// url.set_scheme("smtps").unwrap();
/// // configure the username and password.
/// // remove the following two lines if unauthenticated.
/// url.set_username("username").unwrap();
/// url.set_password(Some("password")).unwrap();
/// // configure the hostname
/// url.set_host(Some("smtp.example.com")).unwrap();
/// // configure the port - only necessary if using a non-default port
/// url.set_port(Some(465)).unwrap();
/// // configure the EHLO name
/// url.set_path("ehlo-name");
///
/// println!("{url}");
/// }
/// ```
///
/// The connection URL can then be used in the following way:
///
/// ```rust,no_run
/// use lettre::{
@@ -162,6 +217,7 @@ impl SmtpTransport {
/// SmtpTransport, Transport,
/// };
///
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let email = Message::builder()
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
@@ -172,20 +228,17 @@ impl SmtpTransport {
/// .unwrap();
///
/// // Open a remote connection to example
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")
/// .unwrap()
/// .build();
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
///
/// // Send the email
/// match mailer.send(&email) {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// mailer.send(&email)?;
/// # Ok(())
/// # }
/// ```
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
super::connection_url::from_connection_url(connection_url)
@@ -238,7 +291,7 @@ impl SmtpTransportBuilder {
self
}
/// Set the authentication mechanism to use
/// Set the authentication credentials to use
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.info.credentials = Some(credentials);
self
@@ -257,16 +310,38 @@ impl SmtpTransportBuilder {
}
/// Set the port to use
///
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
///
/// lettre usually picks the correct `port` when building
/// [`SmtpTransport`] using [`SmtpTransport::relay`] or
/// [`SmtpTransport::starttls_relay`].
///
/// # Errors
///
/// Using the incorrect `port` and [`Self::tls`] combination may
/// lead to hard to debug IO errors coming from the TLS library.
pub fn port(mut self, port: u16) -> Self {
self.info.port = port;
self
}
/// Set the TLS settings to use
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
///
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
///
/// By default lettre chooses the correct `tls` configuration when
/// building [`SmtpTransport`] using [`SmtpTransport::relay`] or
/// [`SmtpTransport::starttls_relay`].
///
/// # Errors
///
/// Using the wrong [`Tls`] and [`Self::port`] combination may
/// lead to hard to debug IO errors coming from the TLS library.
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
)]
pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls;
@@ -299,7 +374,7 @@ impl SmtpTransportBuilder {
/// Build client
#[derive(Debug, Clone)]
pub struct SmtpClient {
pub(super) struct SmtpClient {
info: SmtpInfo,
}
@@ -307,11 +382,11 @@ impl SmtpClient {
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
pub fn connection(&self) -> Result<SmtpConnection, Error> {
pub(super) fn connection(&self) -> Result<SmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
let tls_parameters = match &self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
Tls::Wrapper(tls_parameters) => Some(tls_parameters),
_ => None,
};
@@ -324,14 +399,14 @@ impl SmtpClient {
None,
)?;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
match self.info.tls {
Tls::Opportunistic(ref tls_parameters) => {
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
match &self.info.tls {
Tls::Opportunistic(tls_parameters) => {
if conn.can_starttls() {
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)?;
}
_ => (),
@@ -373,6 +448,22 @@ mod tests {
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();

View File

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

View File

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