Compare commits

...

84 Commits

Author SHA1 Message Date
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
Paolo Barbolini
47eda90433 Prepare 0.11.1 (#910) 2023-10-24 23:47:49 +02:00
Paolo Barbolini
46ea8c48ac Ignore rustls deprecation warning 2023-10-24 22:55:16 +02:00
Paolo Barbolini
5f7063fdc3 Fix accidental disabling of webpki-roots setup (#909) 2023-10-24 22:54:22 +02:00
Paolo Barbolini
61c1f6bc6f Fix date in changelog 2023-10-15 17:22:48 +02:00
Paolo Barbolini
283e21f8d6 Prepare 0.11.0 (#899) 2023-10-15 16:21:32 +02:00
Paolo Barbolini
20c3701eb0 Fix doctests (#906) 2023-10-15 16:04:12 +02:00
Paolo Barbolini
74117d5cc6 ci: fix MSRV job (#905) 2023-09-23 09:16:42 +02:00
Marlon
bb49e0a46b Construct a SmtpTransport from a connection URL (#901) 2023-09-23 08:48:36 +02:00
Paolo Barbolini
42365478c2 Revert "Added Address:new_unchecked (#887)" (#904)
This reverts commit 7e6ffe8aea.
2023-09-01 16:06:48 +02:00
Hugo
94769242d1 docs: improve documentation for AsyncSmtpConnection (#903)
Closes: #902

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2023-08-25 10:55:56 +02:00
Wyatt Herkamp
7e6ffe8aea Added Address:new_unchecked (#887) 2023-08-19 00:11:47 +02:00
Luc Lenôtre
16c35ef583 added headers_mut function on Message (#889)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2023-08-18 23:39:38 +02:00
Alexis Mousset
bbab86b484 A few spelling and doc fixes (#900) 2023-08-16 21:56:33 +02:00
Paolo Barbolini
b5652f18b7 Fix -Z minimal-versions (#898) 2023-08-15 11:43:44 +02:00
Paolo Barbolini
c2f2b907a9 Bump boring crates to v3 (#897) 2023-08-15 11:26:08 +02:00
Edwin
a1cc770613 Fix RUSTSEC-2022-0093 (#896) 2023-08-15 10:50:03 +02:00
Paolo Barbolini
57886c367d Fix latest clippy warnings (#891) 2023-07-27 20:32:47 +02:00
Paolo Barbolini
f3a469431e Bump webpki-roots 0.25 (#890) 2023-07-27 08:02:31 +02:00
Paolo Barbolini
9b48ef355b Bump webpki-roots to v0.24 (#884) 2023-07-07 08:14:18 +02:00
Paolo Barbolini
7fee8dc5a8 ci: bump rustfmt (#883) 2023-06-23 09:12:28 +02:00
Paolo Barbolini
7e9fff9bd0 Bump dependencies (#882) 2023-06-23 09:12:09 +02:00
Paolo Barbolini
92f5460132 Bump MSRV to 1.65 (#881) 2023-06-23 07:11:57 +00:00
tecc
cd0c032f71 change: Add From<Address> implementation for Mailbox (#879)
from-address: It's a simple implementation - it uses the address as the address and uses `None` for the name parameter.
2023-06-22 10:22:41 +02:00
Paolo Barbolini
f41c9c19ab Cherry-pick 0.10.4 release changelog 2023-04-02 11:47:26 +02:00
Paolo Barbolini
cb6a7178d9 Bump socket2 to 0.5 (#868) 2023-04-02 11:34:05 +02:00
Paolo Barbolini
2bfc759aa3 ci: remove async-global-executor workaround (#870) 2023-04-02 11:30:50 +02:00
Paolo Barbolini
89673d0eb2 Bump MSRV to 1.63 (#869) 2023-04-02 11:20:51 +02:00
Paolo Barbolini
8b588cf275 Bump rustls to 0.21 (#867) 2023-04-02 10:53:54 +02:00
Clément DOUIN
5f37b66352 Improve mailbox parsing using chumsky (#839) 2023-02-20 14:09:23 +01:00
Paolo Barbolini
69e5974024 Hide internal optional dependencies using cargo's 1.60 dep: syntax (#861) 2023-02-20 12:00:32 +01:00
51 changed files with 4629 additions and 632 deletions

View File

@@ -13,16 +13,16 @@ env:
jobs:
rustfmt:
name: rustfmt / nightly-2022-11-12
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-2022-11-12
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,7 +50,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: rustup update --no-self-update stable
@@ -75,12 +75,12 @@ jobs:
rust: stable
- name: beta
rust: beta
- name: 1.60.0
rust: 1.60.0
- name: '1.70'
rust: '1.70'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Install rust
run: |
@@ -112,9 +112,6 @@ jobs:
- name: Install dkimverify
run: sudo apt -y install python3-dkim
- name: Work around early dependencies MSRV bump
run: cargo update -p async-global-executor --precise 2.0.4
- name: Test with no default features
run: cargo test --no-default-features
@@ -122,16 +119,16 @@ jobs:
run: cargo test
- name: Test with all features (-native-tls)
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-native-certs,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tracing
- name: Test with all features (-boring-tls)
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-native-certs,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls-tls,tracing
# coverage:
# 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,209 @@
<a name="v0.11.10"></a>
### v0.11.10 (2024-10-23)
#### Bug fixes
* Ignore disconnect errors when `pool` feature of SMTP transport is disabled ([#999])
* Use case insensitive comparisons for matching login challenge requests ([#1000])
[#999]: https://github.com/lettre/lettre/pull/999
[#1000]: https://github.com/lettre/lettre/pull/1000
<a name="v0.11.9"></a>
### v0.11.9 (2024-09-13)
#### Bug fixes
* Fix feature gate for `accept_invalid_hostnames` for rustls ([#988])
* Fix parsing `Mailbox` with trailing spaces ([#986])
#### Misc
* Bump `rustls-native-certs` to v0.8 ([#992])
* Make getting started example in readme complete ([#990])
[#988]: https://github.com/lettre/lettre/pull/988
[#986]: https://github.com/lettre/lettre/pull/986
[#990]: https://github.com/lettre/lettre/pull/990
[#992]: https://github.com/lettre/lettre/pull/992
<a name="v0.11.8"></a>
### v0.11.8 (2024-09-03)
#### Features
* Add mTLS support ([#974])
* Implement `accept_invalid_hostnames` for rustls ([#977])
* Provide certificate chain for peer certificates when using `rustls` or `boring-tls` ([#976])
#### Changes
* Make `HeaderName` comparisons via `PartialEq` case insensitive ([#980])
#### Misc
* Fix clippy warnings ([#979])
* Replace manual impl of `#[non_exhaustive]` for `InvalidHeaderName` ([#981])
[#974]: https://github.com/lettre/lettre/pull/974
[#976]: https://github.com/lettre/lettre/pull/976
[#977]: https://github.com/lettre/lettre/pull/977
[#980]: https://github.com/lettre/lettre/pull/980
[#981]: https://github.com/lettre/lettre/pull/981
<a name="v0.11.7"></a>
### v0.11.7 (2024-04-23)
#### Misc
* Bump `hostname` to v0.4 ([#956])
* Fix `tracing` message consistency ([#960])
* Bump minimum required `rustls` to v0.23.5 ([#958])
* Dropped use of `ref` syntax in the entire project ([#959])
[#956]: https://github.com/lettre/lettre/pull/956
[#958]: https://github.com/lettre/lettre/pull/958
[#959]: https://github.com/lettre/lettre/pull/959
[#960]: https://github.com/lettre/lettre/pull/960
<a name="v0.11.6"></a>
### v0.11.6 (2024-03-28)
#### Bug fixes
* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952])
#### Misc
* Updated copyright year in license ([#954])
[#952]: https://github.com/lettre/lettre/pull/952
[#954]: https://github.com/lettre/lettre/pull/954
<a name="v0.11.5"></a>
### v0.11.5 (2024-03-25)
#### Features
* Support SMTP SASL draft login challenge ([#911])
* Add conversion from SMTP response code to integer ([#941])
#### Misc
* Upgrade `rustls` to v0.23 ([#950])
* Bump `base64` to v0.22 ([#945])
* Fix typos in documentation ([#943], [#944])
* Add `Cargo.lock` ([#942])
[#911]: https://github.com/lettre/lettre/pull/911
[#941]: https://github.com/lettre/lettre/pull/941
[#942]: https://github.com/lettre/lettre/pull/942
[#943]: https://github.com/lettre/lettre/pull/943
[#944]: https://github.com/lettre/lettre/pull/944
[#945]: https://github.com/lettre/lettre/pull/945
[#950]: https://github.com/lettre/lettre/pull/950
<a name="v0.11.4"></a>
### v0.11.4 (2024-01-28)
#### Bug fixes
* Percent decode credentials in SMTP connect URL ([#932], [#934])
* Fix mimebody DKIM body-hash computation ([#923])
[#923]: https://github.com/lettre/lettre/pull/923
[#932]: https://github.com/lettre/lettre/pull/932
[#934]: https://github.com/lettre/lettre/pull/934
<a name="v0.11.3"></a>
### v0.11.3 (2024-01-02)
#### Features
* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924])
* Derive `Debug` for `SmtpTransport` ([#925])
#### Misc
* Upgrade `rustls` to v0.22 ([#921])
* Drop once_cell dependency in favor of OnceLock from std ([#928])
[#921]: https://github.com/lettre/lettre/pull/921
[#924]: https://github.com/lettre/lettre/pull/924
[#925]: https://github.com/lettre/lettre/pull/925
[#928]: https://github.com/lettre/lettre/pull/928
<a name="v0.11.2"></a>
### v0.11.2 (2023-11-23)
#### Upgrade notes
* MSRV is now 1.70 ([#916])
#### Misc
* Bump `idna` to v0.5 ([#918])
* Bump `boring` and `tokio-boring` to v4 ([#915])
[#915]: https://github.com/lettre/lettre/pull/915
[#916]: https://github.com/lettre/lettre/pull/916
[#918]: https://github.com/lettre/lettre/pull/918
<a name="v0.11.1"></a>
### v0.11.1 (2023-10-24)
#### Bug fixes
* Fix `webpki-roots` certificate store setup ([#909])
[#909]: https://github.com/lettre/lettre/pull/909
<a name="v0.11.0"></a>
### v0.11.0 (2023-10-15)
While this release technically contains breaking changes, we expect most projects
to be able to upgrade by only bumping the version in `Cargo.toml`.
#### Upgrade notes
* MSRV is now 1.65 ([#869] and [#881])
* `AddressError` is now marked as `#[non_exhaustive]` ([#839])
#### Features
* Improve mailbox parsing ([#839])
* Add construction of SMTP transport from URL ([#901])
* Add `From<Address>` implementation for `Mailbox` ([#879])
#### Misc
* Bump `socket2` to v0.5 ([#868])
* Bump `idna` to v0.4, `fastrand` to v2, `quoted_printable` to v0.5, `rsa` to v0.9 ([#882])
* Bump `webpki-roots` to v0.25 ([#884] and [#890])
* Bump `ed25519-dalek` to v2 fixing RUSTSEC-2022-0093 ([#896])
* Bump `boring`ssl crates to v3 ([#897])
[#839]: https://github.com/lettre/lettre/pull/839
[#868]: https://github.com/lettre/lettre/pull/868
[#869]: https://github.com/lettre/lettre/pull/869
[#879]: https://github.com/lettre/lettre/pull/879
[#881]: https://github.com/lettre/lettre/pull/881
[#882]: https://github.com/lettre/lettre/pull/882
[#884]: https://github.com/lettre/lettre/pull/884
[#890]: https://github.com/lettre/lettre/pull/890
[#896]: https://github.com/lettre/lettre/pull/896
[#897]: https://github.com/lettre/lettre/pull/897
[#901]: https://github.com/lettre/lettre/pull/901
<a name="v0.10.4"></a>
### v0.10.4 (2023-04-02)
#### Misc
* Bumped rustls to 0.21 and all related dependencies ([#867])
[#867]: https://github.com/lettre/lettre/pull/867
<a name="v0.10.3"></a>
### v0.10.3 (2023-02-20)
@@ -143,7 +349,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>

View File

@@ -1,6 +1,6 @@
## Contributing to Lettre
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
The following guidelines are inspired by the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
### Code formatting

2710
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.10.3"
version = "0.11.10"
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.60"
rust-version = "1.70"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -19,17 +19,17 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
[dependencies]
idna = "0.3"
once_cell = { version = "1", optional = true }
chumsky = "0.9"
idna = "1"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder
httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true }
fastrand = { version = "1.4", optional = true }
quoted_printable = { version = "0.4.6", optional = true }
base64 = { version = "0.21", optional = true }
email-encoding = { version = "0.2", optional = true }
fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.22", optional = true }
email-encoding = { version = "0.3", optional = true }
# file transport
uuid = { version = "1", features = ["v4"], optional = true }
@@ -38,16 +38,19 @@ serde_json = { version = "1", optional = true }
# smtp-transport
nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.4.4", optional = true }
hostname = { version = "0.4", optional = true } # feature
socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true }
percent-encoding = { version = "2.3", optional = true }
## tls
native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
rustls-pemfile = { version = "1", optional = true }
rustls-native-certs = { version = "0.6.2", optional = true }
webpki-roots = { version = "0.22", optional = true }
boring = { version = "2.0.0", optional = true }
native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
rustls-pemfile = { version = "2", optional = true }
rustls-native-certs = { version = "0.8", optional = true }
rustls-pki-types = { version = "1.7", optional = true }
webpki-roots = { version = "0.26", optional = true }
boring = { version = "4", optional = true }
# async
futures-io = { version = "0.3.7", optional = true }
@@ -56,26 +59,25 @@ 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.22", optional = true }
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], 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.23", optional = true }
tokio1_boring = { package = "tokio-boring", version = "2.1.4", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
## dkim
sha2 = { version = "0.10", optional = true, features = ["oid"] }
rsa = { version = "0.8", optional = true }
ed25519-dalek = { version = "1.0.1", 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 }
[dev-dependencies]
pretty_assertions = "1"
criterion = "0.4"
criterion = "0.5"
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
tracing-subscriber = "0.3"
glob = "0.3"
@@ -83,39 +85,45 @@ 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.24"
maud = "0.26"
[[bench]]
harness = false
name = "transport_smtp"
[[bench]]
harness = false
name = "mailbox_parsing"
[features]
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"]
mime03 = ["mime"]
builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
mime03 = ["dep:mime"]
# transports
file-transport = ["uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "serde_json", "file-transport"]
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 = ["base64", "nom", "socket2", "once_cell", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
pool = ["futures-util"]
pool = ["dep:futures-util"]
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"]
rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"]
boring-tls = ["boring"]
boring-tls = ["dep:boring"]
# async
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"]
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
tokio1-boring-tls = ["tokio1", "boring-tls", "tokio1_boring"]
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
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-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
dkim = ["base64", "sha2", "rsa", "ed25519-dalek"]
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(lettre_ignore_tls_mismatch)'] }
[package.metadata.docs.rs]
all-features = true

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-2024 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.10.3">
<img src="https://deps.rs/crate/lettre/0.10.3/status.svg"
<a href="https://deps.rs/crate/lettre/0.11.10">
<img src="https://deps.rs/crate/lettre/0.11.10/status.svg"
alt="dependency status" />
</a>
</div>
@@ -53,17 +53,17 @@ 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.60, but this could change at any time either from
the minimum supported Rust version is 1.70, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example
This library requires Rust 1.60 or newer.
This library requires Rust 1.70 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.10"
lettre = "0.11"
```
```rust,no_run
@@ -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("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();
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:?}"),
}
}
```

View File

@@ -2,7 +2,7 @@
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
should not be reported via the public Github Issue tracker.
should not be reported via the public GitHub Issue tracker.
## Security advisories

View File

@@ -0,0 +1,27 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::message::{Mailbox, Mailboxes};
fn bench_parse_single(mailbox: &str) {
assert!(mailbox.parse::<Mailbox>().is_ok());
}
fn bench_parse_multiple(mailboxes: &str) {
assert!(mailboxes.parse::<Mailboxes>().is_ok());
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("parse single mailbox", |b| {
b.iter(|| bench_parse_single(black_box("\"Benchmark test\" <test@mail.local>")))
});
c.bench_function("parse multiple mailboxes", |b| {
b.iter(|| {
bench_parse_multiple(black_box(
"\"Benchmark test\" <test@mail.local>, Test <test@mail.local>, <test@mail.local>, test@mail.local",
))
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

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 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.
///

View File

@@ -15,7 +15,7 @@ use idna::domain_to_ascii;
///
/// This type contains email in canonical form (_user@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
@@ -227,6 +227,7 @@ fn check_address(val: &str) -> Result<usize, AddressError> {
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
/// Errors in email addresses parsing
pub enum AddressError {
/// Missing domain or user
@@ -237,6 +238,8 @@ pub enum AddressError {
InvalidUser,
/// Invalid email domain
InvalidDomain,
/// Invalid input found
InvalidInput,
}
impl Error for AddressError {}
@@ -248,6 +251,7 @@ impl Display for AddressError {
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidInput => f.write_str("Invalid input"),
}
}
}

View File

@@ -134,7 +134,7 @@ impl Executor for Tokio1Executor {
#[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()),
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
@@ -149,12 +149,12 @@ impl Executor for Tokio1Executor {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
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?;
}
_ => (),
@@ -216,7 +216,7 @@ impl Executor for AsyncStd1Executor {
#[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep {
let fut = async move { async_std::task::sleep(duration).await };
let fut = async_std::task::sleep(duration);
Box::pin(fut)
}
@@ -230,8 +230,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")]
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
@@ -243,14 +243,14 @@ impl Executor for AsyncStd1Executor {
)
.await?;
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
#[cfg(feature = "async-std1-rustls-tls")]
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?;
}
_ => (),

View File

@@ -6,7 +6,7 @@
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.60 or newer.
//! Lettre requires Rust 1.70 or newer.
//!
//! ## Features
//!
@@ -109,7 +109,7 @@
//! [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.10.3")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.10")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
@@ -174,21 +174,7 @@ mod compiletime_checks {
If you'd like to use `boring-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
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.
@@ -219,11 +205,11 @@ use std::error::Error as StdError;
#[cfg(feature = "async-std1")]
pub use self::executor::AsyncStd1Executor;
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::executor::Executor;
#[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor;
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[doc(inline)]
pub use self::transport::AsyncTransport;
pub use crate::address::Address;

View File

@@ -13,9 +13,9 @@ pub struct Attachment {
#[derive(Clone)]
enum Disposition {
/// file name
/// File name
Attached(String),
/// content id
/// Content id
Inline(String),
}

View File

@@ -496,7 +496,7 @@ mod test {
#[test]
fn base64_encode_bytes_wrapping() {
let encoded = Body::new_with_encoding(
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
ContentTransferEncoding::Base64,
)
.unwrap();

View File

@@ -2,7 +2,6 @@ use std::{
borrow::Cow,
error::Error as StdError,
fmt::{self, Display},
iter::IntoIterator,
time::SystemTime,
};
@@ -108,7 +107,7 @@ pub struct DkimSigningKey(InnerDkimSigningKey);
#[derive(Debug)]
enum InnerDkimSigningKey {
Rsa(RsaPrivateKey),
Ed25519(ed25519_dalek::Keypair),
Ed25519(ed25519_dalek::SigningKey),
}
impl DkimSigningKey {
@@ -121,14 +120,18 @@ impl DkimSigningKey {
RsaPrivateKey::from_pkcs1_pem(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
),
DkimSigningAlgorithm::Ed25519 => InnerDkimSigningKey::Ed25519(
ed25519_dalek::Keypair::from_bytes(
&crate::base64::decode(private_key).map_err(|err| {
DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err))
})?,
)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
),
DkimSigningAlgorithm::Ed25519 => {
InnerDkimSigningKey::Ed25519(ed25519_dalek::SigningKey::from_bytes(
&crate::base64::decode(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)))?
.try_into()
.map_err(|_| {
DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(
ed25519_dalek::ed25519::Error::new(),
))
})?,
))
}
}))
}
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
@@ -140,19 +143,18 @@ impl DkimSigningKey {
}
/// A struct to describe Dkim configuration applied when signing a message
/// selector: the name of the key publied in DNS
/// domain: the domain for which we sign the message
/// private_key: private key in PKCS1 string format
/// headers: a list of headers name to be included in the signature. Signing of more than one
/// header with same name is not supported
/// canonicalization: the canonicalization to be applied on the message
/// pub signing_algorithm: the signing algorithm to be used when signing
#[derive(Debug)]
pub struct DkimConfig {
/// The name of the key published in DNS
selector: String,
/// The domain for which we sign the message
domain: String,
/// The private key in PKCS1 string format
private_key: DkimSigningKey,
/// A list of header names to be included in the signature. Signing of more than one
/// header with the same name is not supported
headers: Vec<HeaderName>,
/// The signing algorithm to be used when signing
canonicalization: DkimCanonicalization,
}
@@ -341,7 +343,7 @@ fn dkim_canonicalize_headers<'a>(
}
}
/// Sign with Dkim a message by adding Dkim-Signture header created with configuration expressed by
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
/// dkim_config
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {

View File

@@ -13,12 +13,14 @@ use crate::BoxError;
/// use-caches this header shouldn't be set manually.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub enum ContentTransferEncoding {
/// ASCII
SevenBit,
/// Quoted-Printable encoding
QuotedPrintable,
/// base64 encoding
#[default]
Base64,
/// Requires `8BITMIME`
EightBit,
@@ -67,12 +69,6 @@ impl FromStr for ContentTransferEncoding {
}
}
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::Base64
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;

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;
@@ -22,7 +22,7 @@ impl ContentDisposition {
}
/// An attachment which should be displayed inline into the message, but that also
/// species the filename in case it were to be downloaded
/// species the filename in case it is downloaded
pub fn inline_with_name(file_name: &str) -> Self {
Self::with_name("inline", file_name)
}
@@ -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

@@ -11,12 +11,12 @@ use crate::BoxError;
/// `Content-Type` of the body
///
/// This struct can represent any valid [mime type], which can be parsed via
/// This struct can represent any valid [MIME type], which can be parsed via
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
///
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
///
/// [mime type]: https://www.iana.org/assignments/media-types/media-types.xhtml
/// [MIME type]: https://www.iana.org/assignments/media-types/media-types.xhtml
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType(Mime);

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");
}
@@ -306,14 +306,30 @@ mod test {
#[test]
fn parse_multi_with_name_containing_comma() {
let from: Vec<Mailbox> = vec![
"Test, test <1@example.com>".parse().unwrap(),
"Test2, test2 <2@example.com>".parse().unwrap(),
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, test2\" <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_owned(),
"\"Test, test\" <1@example.com>, \"Test2, test2\" <2@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name_containing_double_quotes() {
let from: Vec<Mailbox> = vec![
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, \"test2\"\" <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"Test, test\" <1@example.com>, \"Test2, \"test2\"\" <2@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -324,9 +340,20 @@ mod test {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2".to_owned(),
"\"Test, test\" <1@example.com>, \"Test2, test2\"".to_owned(),
));
assert_eq!(headers.get::<From>(), None);
}
#[test]
fn mailbox_format_address_with_angle_bracket() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("<3".into()), "i@love.example".parse().unwrap())
),
r#""<3" <i@love.example>"#
);
}
}

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::*,
@@ -66,7 +66,7 @@ impl Headers {
}
}
/// Returns a copy of an `Header` present in `Headers`
/// Returns a copy of a `Header` present in `Headers`
///
/// Returns `None` if `Header` isn't present in `Headers`.
pub fn get<H: Header>(&self) -> Option<H> {
@@ -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)
}
}
@@ -310,7 +290,7 @@ impl HeaderValue {
/// acceptable for use if `encoded_value` contains only ascii
/// printable characters and is already line folded.
///
/// When in doubt use [`HeaderValue::new`].
/// When in doubt, use [`HeaderValue::new`].
pub fn dangerous_new_pre_encoded(
name: HeaderName,
raw_value: String,
@@ -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,
@@ -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

@@ -1,3 +1,4 @@
mod parsers;
#[cfg(feature = "serde")]
mod serde;
mod types;

View File

@@ -0,0 +1,5 @@
mod rfc2234;
mod rfc2822;
mod rfc5336;
pub(crate) use rfc2822::{mailbox, mailbox_list};

View File

@@ -0,0 +1,32 @@
//! Partial parsers implementation of [RFC2234]: Augmented BNF for
//! Syntax Specifications: ABNF.
//!
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
use chumsky::{error::Cheap, prelude::*};
// 6.1 Core Rules
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_alphabetic())
}
// DIGIT = %x30-39
// ; 0-9
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.is_ascii_digit())
}
// DQUOTE = %x22
// ; " (Double Quote)
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
just('"')
}
// WSP = SP / HTAB
// ; white space
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
choice((just(' '), just('\t')))
}

View File

@@ -0,0 +1,250 @@
//! Partial parsers implementation of [RFC2822]: Internet Message
//! Format.
//!
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
use chumsky::{error::Cheap, prelude::*};
use super::{rfc2234, rfc5336};
// 3.2.1. Primitive Tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
// NO-WS-CTL = %d1-8 / ; US-ASCII control characters
// %d11 / ; that do not include the
// %d12 / ; carriage return, line feed,
// %d14-31 / ; and white space characters
// %d127
fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c| matches!(u32::from(*c), 1..=8 | 11 | 12 | 14..=31 | 127))
}
// text = %d1-9 / ; Characters excluding CR and LF
// %d11 /
// %d12 /
// %d14-127 /
// obs-text
fn text() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c| matches!(u32::from(*c), 1..=9 | 11 | 12 | 14..=127))
}
// 3.2.2. Quoted characters
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
// quoted-pair = ("\" text) / obs-qp
fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
just('\\').ignore_then(text())
}
// 3.2.3. Folding white space and comments
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.3
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
// obs-FWS
pub fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
rfc2234::wsp()
.or_not()
.then_ignore(rfc2234::wsp().ignored().repeated())
}
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
pub fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
// TODO: comment are not currently supported, so for now a cfws is
// the same as a fws.
fws()
}
// 3.2.4. Atom
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
// atext = ALPHA / DIGIT / ; Any character except controls,
// "!" / "#" / ; SP, and specials.
// "$" / "%" / ; Used for atoms
// "&" / "'" /
// "*" / "+" /
// "-" / "/" /
// "=" / "?" /
// "^" / "_" /
// "`" / "{" /
// "|" / "}" /
// "~"
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
choice((
rfc2234::alpha(),
rfc2234::digit(),
filter(|c| {
matches!(
*c,
'!' | '#'
| '$'
| '%'
| '&'
| '\''
| '*'
| '+'
| '-'
| '/'
| '='
| '?'
| '^'
| '_'
| '`'
| '{'
| '|'
| '}'
| '~'
)
}),
// also allow non ASCII UTF8 chars
rfc5336::utf8_non_ascii(),
))
}
// atom = [CFWS] 1*atext [CFWS]
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(atext().repeated().at_least(1))
}
// dot-atom = [CFWS] dot-atom-text [CFWS]
pub fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
cfws().chain(dot_atom_text())
}
// dot-atom-text = 1*atext *("." 1*atext)
pub fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atext().repeated().at_least(1).chain(
just('.')
.chain(atext().repeated().at_least(1))
.repeated()
.at_least(1)
.flatten(),
)
}
// 3.2.5. Quoted strings
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
// qtext = NO-WS-CTL / ; Non white space controls
//
// %d33 / ; The rest of the US-ASCII
// %d35-91 / ; characters not including "\"
// %d93-126 ; or the quote character
fn qtext() -> impl Parser<char, char, Error = Cheap<char>> {
choice((
filter(|c| matches!(u32::from(*c), 33 | 35..=91 | 93..=126)),
no_ws_ctl(),
))
}
// qcontent = qtext / quoted-pair
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> {
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii()))
}
// quoted-string = [CFWS]
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
// [CFWS]
fn quoted_string() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
rfc2234::dquote()
.ignore_then(fws().chain(qcontent()).repeated().flatten())
.then_ignore(text::whitespace())
.then_ignore(rfc2234::dquote())
}
// 3.2.6. Miscellaneous tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
// word = atom / quoted-string
fn word() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((quoted_string(), atom()))
}
// phrase = 1*word / obs-phrase
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((obs_phrase(), word().repeated().at_least(1).flatten()))
}
// 3.4. Address Specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
// mailbox = name-addr / addr-spec
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>>
{
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
.padded()
.then_ignore(end())
}
// name-addr = [display-name] angle-addr
fn name_addr() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> {
display_name().collect().or_not().then(angle_addr())
}
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
fn angle_addr() -> impl Parser<char, (String, String), Error = Cheap<char>> {
addr_spec()
.delimited_by(just('<').ignored(), just('>').ignored())
.padded()
}
// display-name = phrase
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
phrase()
}
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
pub(crate) fn mailbox_list(
) -> impl Parser<char, Vec<(Option<String>, (String, String))>, Error = Cheap<char>> {
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
.separated_by(just(',').padded())
.then_ignore(end())
}
// 3.4.1. Addr-spec specification
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
// addr-spec = local-part "@" domain
pub fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
local_part()
.collect()
.then_ignore(just('@'))
.then(domain().collect())
}
// local-part = dot-atom / quoted-string / obs-local-part
pub fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
choice((dot_atom(), quoted_string(), obs_local_part()))
}
// domain = dot-atom / domain-literal / obs-domain
pub fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// NOTE: omitting domain-literal since it may never be used
choice((dot_atom(), obs_domain()))
}
// 4.1. Miscellaneous obsolete tokens
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.1
// obs-phrase = word *(word / "." / CFWS)
fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
// NOTE: the CFWS is already captured by the word, no need to add
// it there.
word().chain(
choice((word(), just('.').repeated().exactly(1)))
.repeated()
.flatten(),
)
}
// 4.4. Obsolete Addressing
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
// obs-local-part = word *("." word)
pub fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
word().chain(just('.').chain(word()).repeated().flatten())
}
// obs-domain = atom *("." atom)
pub fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
atom().chain(just('.').chain(atom()).repeated().flatten())
}

View File

@@ -0,0 +1,17 @@
//! Partial parsers implementation of [RFC5336]: SMTP Extension for
//! Internationalized Email Addresses.
//!
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
use chumsky::{error::Cheap, prelude::*};
// 3.3. Extended Mailbox Address Syntax
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
// UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
// UTF8-2 = <See Section 4 of RFC 3629>
// UTF8-3 = <See Section 4 of RFC 3629>
// UTF8-4 = <See Section 4 of RFC 3629>
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> {
filter(|c: &char| c.len_utf8() > 1)
}

View File

@@ -179,7 +179,7 @@ mod test {
}
#[test]
fn parse_mailbox_object_address_stirng() {
fn parse_mailbox_object_address_string() {
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
}
@@ -198,7 +198,7 @@ mod test {
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
assert_eq!(
m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse()
.unwrap()
);
@@ -211,7 +211,7 @@ mod test {
.unwrap();
assert_eq!(
m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse()
.unwrap()
);

View File

@@ -5,15 +5,17 @@ use std::{
str::FromStr,
};
use email_encoding::headers::EmailWriter;
use chumsky::prelude::*;
use email_encoding::headers::writer::EmailWriter;
use super::parsers;
use crate::address::{Address, AddressError};
/// Represents an email address with an optional name for the sender/recipient.
///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
@@ -70,7 +72,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('<')?;
}
@@ -86,7 +88,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)?;
@@ -108,40 +110,24 @@ impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
}
}
/*
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
type Error = AddressError;
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
let (name, address) = header;
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
}
}*/
impl FromStr for Mailbox {
type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
match (src.find('<'), src.find('>')) {
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => {
let name = src.split_at(addr_open).0;
let addr_open = addr_open + 1;
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
let addr = addr.parse()?;
let name = name.trim();
let name = if name.is_empty() {
None
} else {
Some(name.into())
};
Ok(Mailbox::new(name, addr))
}
(Some(_), _) => Err(AddressError::Unbalanced),
_ => {
let addr = src.parse()?;
Ok(Mailbox::new(None, addr))
}
}
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
// TODO: improve error management
AddressError::InvalidInput
})?;
let mailbox = Mailbox::new(name, Address::new(user, domain)?);
Ok(mailbox)
}
}
impl From<Address> for Mailbox {
fn from(value: Address) -> Self {
Self::new(None, value)
}
}
@@ -149,7 +135,7 @@ impl FromStr for Mailbox {
///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailboxes(Vec<Mailbox>);
@@ -275,7 +261,7 @@ impl Mailboxes {
for mailbox in self.iter() {
if !mem::take(&mut first) {
w.write_char(',')?;
w.optional_breakpoint();
w.space();
}
mailbox.encode(w)?;
@@ -356,34 +342,16 @@ impl Display for Mailboxes {
impl FromStr for Mailboxes {
type Err = AddressError;
fn from_str(mut src: &str) -> Result<Self, Self::Err> {
fn from_str(src: &str) -> Result<Self, Self::Err> {
let mut mailboxes = Vec::new();
if !src.is_empty() {
// n-1 elements
let mut skip = 0;
while let Some(i) = src[skip..].find(',') {
let left = &src[..skip + i];
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
// TODO: improve error management
AddressError::InvalidInput
})?;
match left.trim().parse() {
Ok(mailbox) => {
mailboxes.push(mailbox);
src = &src[left.len() + ",".len()..];
skip = 0;
}
Err(AddressError::MissingParts) => {
skip = left.len() + ",".len();
}
Err(err) => {
return Err(err);
}
}
}
// last element
let mailbox = src.trim().parse()?;
mailboxes.push(mailbox);
for (name, (user, domain)) in parsed_mailboxes {
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
}
Ok(Mailboxes(mailboxes))
@@ -436,7 +404,7 @@ fn is_valid_atom_char(c: u8) -> bool {
b'}' |
b'~' |
// Not techically allowed but will be escaped into allowed characters.
// Not technically allowed but will be escaped into allowed characters.
128..=255)
}
@@ -476,8 +444,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;
@@ -590,6 +556,14 @@ mod test {
);
}
#[test]
fn parse_address_only_trim() {
assert_eq!(
" kayo@example.com ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_name() {
assert_eq!(
@@ -601,6 +575,17 @@ mod test {
);
}
#[test]
fn parse_address_with_name_trim() {
assert_eq!(
" K. <kayo@example.com> ".parse(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_address_with_empty_name() {
assert_eq!(
@@ -612,7 +597,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 {
@@ -100,14 +110,14 @@ impl SinglePart {
SinglePartBuilder::new()
}
/// Directly create a `SinglePart` from an plain UTF-8 content
/// Directly create a `SinglePart` from a plain UTF-8 content
pub fn plain<T: IntoBody>(body: T) -> Self {
Self::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(body)
}
/// Directly create a `SinglePart` from an UTF-8 HTML content
/// Directly create a `SinglePart` from a UTF-8 HTML content
pub fn html<T: IntoBody>(body: T) -> Self {
Self::builder()
.header(header::ContentType::TEXT_HTML)
@@ -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);
}
}
@@ -149,17 +164,17 @@ impl EmailFormat for SinglePart {
pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts
///
/// For example this kind can be used to mix email message and attachments.
/// For example, this kind can be used to mix an email message and attachments.
Mixed,
/// Alternative kind to join several variants of same email contents.
///
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message.
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into a single email message.
Alternative,
/// Related kind to mix content and related resources.
///
/// For example, you can include images into HTML content using that.
/// For example, you can include images in HTML content using that.
Related,
/// Encrypted kind for encrypted messages
@@ -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

@@ -388,13 +388,13 @@ impl MessageBuilder {
/// Keep the `Bcc` header
///
/// By default the `Bcc` header is removed from the email after
/// By default, the `Bcc` header is removed from the email after
/// using it to generate the message envelope. In some cases though,
/// like when saving the email as an `.eml`, or sending through
/// some transports (like the Gmail API) that don't take a separate
/// envelope value, it becomes necessary to keep the `Bcc` header.
///
/// Calling this method overrides the default behaviour.
/// Calling this method overrides the default behavior.
pub fn keep_bcc(mut self) -> Self {
self.drop_bcc = false;
self
@@ -503,6 +503,11 @@ impl Message {
&self.headers
}
/// Get a mutable reference to the headers
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
/// Get `Message` envelope
pub fn envelope(&self) -> &Envelope {
&self.envelope
@@ -520,7 +525,7 @@ 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");
@@ -630,7 +635,7 @@ mod test {
}
#[test]
fn email_miminal_message() {
fn email_minimal_message() {
assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("NoBody <nobody@domain.tld>".parse().unwrap())

View File

@@ -54,7 +54,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);
}
@@ -70,7 +70,7 @@ impl fmt::Display for Error {
Kind::Envelope => f.write_str("internal client error")?,
};
if let Some(ref e) = self.inner.source {
if let Some(e) = &self.inner.source {
write!(f, ": {e}")?;
}

View File

@@ -157,7 +157,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 {
@@ -167,7 +167,7 @@ pub struct FileTransport {
}
/// Asynchronously writes the content and the envelope information to a file
#[derive(Debug)]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]

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
//!
@@ -75,7 +75,7 @@
//! // Send the email
//! match mailer.send(&email) {
//! Ok(_) => println!("Email sent successfully!"),
//! Err(e) => panic!("Could not send email: {:?}", e),
//! Err(e) => panic!("Could not send email: {e:?}"),
//! }
//! # Ok(())
//! # }
@@ -97,6 +97,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;

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);
}
@@ -67,7 +67,7 @@ impl fmt::Display for 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

@@ -45,7 +45,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.quit().await?;
conn.abort().await;
Ok(result)
}
@@ -82,7 +82,6 @@ where
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
@@ -103,7 +102,7 @@ where
.tls(Tls::Wrapper(tls_parameters)))
}
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
/// Simple and secure transport, using STARTTLS to obtain encrypted connections
///
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections.
@@ -117,7 +116,6 @@ where
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
@@ -151,28 +149,114 @@ where
///
/// * No authentication
/// * No TLS
/// * A 60 seconds timeout for smtp commands
/// * A 60-seconds timeout for smtp commands
/// * Port 25
///
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
let info = SmtpInfo {
server: server.into(),
..Default::default()
};
AsyncSmtpTransportBuilder {
info,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
AsyncSmtpTransportBuilder::new(server)
}
/// 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`.
///
/// <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>
///
/// ```rust,no_run
/// use lettre::{
/// message::header::ContentType, transport::smtp::authentication::Credentials,
/// AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
/// };
/// # use tokio1_crate as tokio;
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let email = Message::builder()
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
/// .to("Hei <hei@domain.tld>".parse().unwrap())
/// .subject("Happy new year")
/// .header(ContentType::TEXT_PLAIN)
/// .body(String::from("Be happy!"))
/// .unwrap();
///
/// // Open a remote connection to gmail
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
/// "smtps://username:password@smtp.example.com:465",
/// )
/// .unwrap()
/// .build();
///
/// // Send the email
/// match mailer.send(email).await {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// # Ok(())
/// # }
/// ```
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
super::connection_url::from_connection_url(connection_url)
}
/// Tests the SMTP connection
///
/// `test_connection()` tests the connection by using the SMTP NOOP command.
/// The connection is closed afterwards if a connection pool is not used.
/// The connection is closed afterward if a connection pool is not used.
pub async fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection().await?;
@@ -219,6 +303,20 @@ pub struct AsyncSmtpTransportBuilder {
/// Builder for the SMTP `AsyncSmtpTransport`
impl AsyncSmtpTransportBuilder {
// Create new builder with default parameters
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
let info = SmtpInfo {
server: server.into(),
..Default::default()
};
AsyncSmtpTransportBuilder {
info,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name;
@@ -253,7 +351,6 @@ impl AsyncSmtpTransportBuilder {
#[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(

View File

@@ -51,7 +51,7 @@ pub enum Mechanism {
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
Plain,
/// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365)
/// Obsolete but needed for some providers (like Office 365)
///
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
Login,
@@ -71,7 +71,7 @@ impl Display for Mechanism {
}
impl Mechanism {
/// Does the mechanism supports initial response
/// Does the mechanism support initial response?
pub fn supports_initial_response(self) -> bool {
match self {
Mechanism::Plain | Mechanism::Xoauth2 => true,
@@ -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

@@ -62,7 +62,35 @@ impl AsyncSmtpConnection {
/// Connects to the configured server
///
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
/// referred to as `SMTPS`). See also [`AsyncSmtpConnection::starttls`].
///
/// If `local_address` is `Some`, then the address provided shall be used to bind the
/// connection to a specific local address using [`tokio1_crate::net::TcpSocket::bind`].
///
/// Sends EHLO and parses server information
///
/// # Example
///
/// ```no_run
/// # use std::time::Duration;
/// # use lettre::transport::smtp::{client::{AsyncSmtpConnection, TlsParameters}, extension::ClientId};
/// # use tokio1_crate::{self as tokio, net::ToSocketAddrs as _};
/// #
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let connection = AsyncSmtpConnection::connect_tokio1(
/// ("example.com", 465),
/// Some(Duration::from_secs(60)),
/// &ClientId::default(),
/// Some(TlsParameters::new("example.com".to_owned())?),
/// None,
/// )
/// .await
/// .unwrap();
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "tokio1")]
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
server: T,
@@ -132,7 +160,7 @@ impl AsyncSmtpConnection {
mail_options.push(MailParameter::SmtpUtfEight);
}
// Check for non-ascii content in message
// Check for non-ascii content in the message
if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(error::client(
@@ -172,6 +200,12 @@ impl AsyncSmtpConnection {
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
}
/// Upgrade the connection using `STARTTLS`.
///
/// As described in [rfc3207]. Note that this mechanism has been deprecated in [rfc8314].
///
/// [rfc3207]: https://www.rfc-editor.org/rfc/rfc3207
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
#[allow(unused_variables)]
pub async fn starttls(
&mut self,
@@ -226,7 +260,7 @@ impl AsyncSmtpConnection {
self.command(Noop).await.is_ok()
}
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
/// Sends an AUTH command with the given mechanism, and handles the challenge if needed
pub async fn auth(
&mut self,
mechanisms: &[Mechanism],
@@ -339,4 +373,10 @@ impl AsyncSmtpConnection {
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
}
}

View File

@@ -6,8 +6,6 @@ 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::{
@@ -16,6 +14,8 @@ use futures_io::{
};
#[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(any(feature = "tokio1-rustls-tls", feature = "async-std1-rustls-tls"))]
use rustls::pki_types::ServerName;
#[cfg(feature = "tokio1-boring-tls")]
use tokio1_boring::SslStream as Tokio1SslStream;
#[cfg(feature = "tokio1")]
@@ -34,7 +34,6 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
use super::InnerTlsParameters;
@@ -84,9 +83,6 @@ 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>),
/// Can't be built
@@ -104,23 +100,21 @@ 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(),
InnerAsyncNetworkStream::Tokio1RustlsTls(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(),
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Err(IoError::new(
@@ -208,9 +202,9 @@ impl AsyncNetworkStream {
timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
// Unfortunately there doesn't currently seem to be a way to set the local address
// Unfortunately, there doesn't currently seem to be a way to set the local address.
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
// connected which is a blocking operation.
// been connected, which is a blocking operation.
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
server: T,
timeout: Duration,
@@ -286,16 +280,13 @@ 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-tls")))]
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-tls feature");
}
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
// get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
@@ -350,7 +341,6 @@ impl AsyncNetworkStream {
#[cfg(feature = "tokio1-rustls-tls")]
return {
use rustls::ServerName;
use tokio1_rustls::TlsConnector;
let domain = ServerName::try_from(domain.as_str())
@@ -358,7 +348,7 @@ 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))
@@ -384,11 +374,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-tls")]
async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters,
@@ -399,22 +385,6 @@ impl AsyncNetworkStream {
#[cfg(feature = "native-tls")]
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) => {
@@ -424,14 +394,13 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls-tls")]
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))
@@ -445,7 +414,7 @@ impl AsyncNetworkStream {
}
pub fn is_encrypted(&self) -> bool {
match self.inner {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
#[cfg(feature = "tokio1-native-tls")]
@@ -456,14 +425,54 @@ impl AsyncNetworkStream {
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,
InnerAsyncNetworkStream::None => false,
}
}
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
.peer_cert_chain()
.unwrap()
.iter()
.map(|c| c.to_der().map_err(error::tls))
.collect::<Result<Vec<_>, _>>()?),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted"))
}
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
.get_ref()
.1
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
#[cfg(feature = "tokio1")]
@@ -486,8 +495,7 @@ impl AsyncNetworkStream {
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
@@ -499,8 +507,6 @@ 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
.get_ref()
@@ -509,8 +515,7 @@ impl AsyncNetworkStream {
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
@@ -522,9 +527,9 @@ impl FuturesAsyncRead for AsyncNetworkStream {
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 +538,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())),
@@ -542,7 +547,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
}
}
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => {
InnerAsyncNetworkStream::Tokio1RustlsTls(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 +556,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 +565,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)
}
InnerAsyncNetworkStream::AsyncStd1Tcp(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::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
@@ -583,25 +582,19 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
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),
InnerAsyncNetworkStream::Tokio1NativeTls(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::Tokio1RustlsTls(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)
}
InnerAsyncNetworkStream::AsyncStd1Tcp(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::AsyncStd1RustlsTls(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 +603,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),
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-boring-tls")]
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),
InnerAsyncNetworkStream::AsyncStd1Tcp(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::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
@@ -633,21 +624,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),
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-boring-tls")]
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),
InnerAsyncNetworkStream::AsyncStd1Tcp(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::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))

View File

@@ -100,7 +100,7 @@ impl SmtpConnection {
mail_options.push(MailParameter::SmtpUtfEight);
}
// Check for non-ascii content in message
// Check for non-ascii content in the message
if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(error::client(
@@ -207,7 +207,7 @@ impl SmtpConnection {
self.command(Noop).is_ok()
}
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
/// Sends an AUTH command with the given mechanism, and handles the challenge if needed
pub fn auth(
&mut self,
mechanisms: &[Mechanism],
@@ -307,4 +307,10 @@ impl SmtpConnection {
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate()
}
/// All the X509 certificates of the chain (DER encoded)
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
self.stream.get_ref().certificate_chain()
}
}

View File

@@ -38,7 +38,7 @@ pub(super) use self::tls::InnerTlsParameters;
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"))]
@@ -139,7 +139,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

@@ -12,7 +12,7 @@ use boring::ssl::SslStream;
#[cfg(feature = "native-tls")]
use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConnection, ServerName, StreamOwned};
use rustls::{pki_types::ServerName, ClientConnection, StreamOwned};
use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -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(),
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::RustlsTls(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),
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::RustlsTls(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(())
@@ -189,7 +189,7 @@ impl NetworkStream {
InnerTlsParameters::RustlsTls(connector) => {
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(connector), domain.to_owned())
.map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream)
@@ -208,7 +208,7 @@ impl NetworkStream {
}
pub fn is_encrypted(&self) -> bool {
match self.inner {
match &self.inner {
InnerNetworkStream::Tcp(_) => false,
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true,
@@ -223,6 +223,32 @@ impl NetworkStream {
}
}
#[cfg(any(feature = "rustls-tls", feature = "boring-tls"))]
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(stream) => Ok(stream
.conn
.peer_certificates()
.unwrap()
.iter()
.map(|c| c.to_vec())
.collect()),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
.peer_cert_chain()
.unwrap()
.iter()
.map(|c| c.to_der().map_err(error::tls))
.collect::<Result<Vec<_>, _>>()?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner {
@@ -241,8 +267,7 @@ impl NetworkStream {
.unwrap()
.first()
.unwrap()
.clone()
.0),
.to_vec()),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
@@ -255,20 +280,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)
}
InnerNetworkStream::NativeTls(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::RustlsTls(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 +297,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)
}
InnerNetworkStream::NativeTls(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::RustlsTls(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 +316,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),
InnerNetworkStream::NativeTls(s) => s.read(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
InnerNetworkStream::RustlsTls(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 +334,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),
InnerNetworkStream::NativeTls(s) => s.write(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
InnerNetworkStream::RustlsTls(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 +350,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(),
InnerNetworkStream::NativeTls(s) => s.flush(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
InnerNetworkStream::RustlsTls(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(())
@@ -354,7 +367,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 to the default
/// 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
fn bind_local_address(

View File

@@ -1,9 +1,10 @@
use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")]
use std::{sync::Arc, time::SystemTime};
use std::{io, sync::Arc};
#[cfg(feature = "boring-tls")]
use boring::{
pkey::PKey,
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
@@ -11,8 +12,12 @@ use boring::{
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::{
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
ClientConfig, Error as TlsError, RootCertStore, ServerName,
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
crypto::WebPkiSupportedAlgorithms,
crypto::{verify_tls12_signature, verify_tls13_signature},
pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime},
server::ParsedCertificate,
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
};
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
@@ -47,9 +52,9 @@ pub enum TlsVersion {
Tlsv12,
/// TLS 1.3
///
/// The most secure option, altough not supported by all SMTP servers.
/// The most secure option, although not supported by all SMTP servers.
///
/// Altough it is technically supported by all TLS backends,
/// Although it is technically supported by all TLS backends,
/// trying to set it for `native-tls` will give a runtime error.
Tlsv13,
}
@@ -87,11 +92,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", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"),
}
}
@@ -99,33 +104,28 @@ impl Debug for Tls {
/// Source for the base set of root certificates to trust.
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
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
/// For rustls, this will also use the system store if the `rustls-native-certs` feature is
/// enabled, or will fall back to `webpki-roots`.
///
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
#[default]
Default,
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
///
/// This option is only available in the rustls backend.
#[cfg(feature = "webpki-roots")]
#[cfg(feature = "rustls-tls")]
WebpkiRoots,
/// Don't use any system certificates.
None,
}
impl Default for CertificateStore {
fn default() -> Self {
CertificateStore::Default
}
}
/// Parameters to use for secure clients
#[derive(Clone)]
pub struct TlsParameters {
@@ -142,6 +142,7 @@ 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"))]
@@ -155,6 +156,7 @@ 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"))]
@@ -170,12 +172,20 @@ 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
///
/// Defaults to `false`.
@@ -187,10 +197,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-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
@@ -278,6 +289,10 @@ 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),
@@ -320,6 +335,15 @@ impl TlsParametersBuilder {
}
}
if let Some(identity) = self.identity {
tls_builder
.set_certificate(identity.boring_tls.0.as_ref())
.map_err(error::tls)?;
tls_builder
.set_private_key(identity.boring_tls.1.as_ref())
.map_err(error::tls)?;
}
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
@@ -342,8 +366,6 @@ impl TlsParametersBuilder {
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
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 => {
@@ -356,75 +378,74 @@ impl TlsParametersBuilder {
TlsVersion::Tlsv13 => just_version3,
};
let tls = tls
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_protocol_versions(supported_versions)
.map_err(error::tls)?;
let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
let provider = rustls::crypto::CryptoProvider::get_default()
.cloned()
.unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider()));
let tls = if self.accept_invalid_certs {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
// Build TLS config
let signature_algorithms = provider.signature_verification_algorithms;
let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
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"
);
Ok(())
}
#[cfg(feature = "rustls-tls")]
fn load_webpki_roots(store: &mut RootCertStore) {
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store)?;
#[cfg(not(feature = "rustls-native-certs"))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(feature = "rustls-tls")]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(rustls_cert).map_err(error::tls)?;
}
}
let tls = if self.accept_invalid_certs || self.accept_invalid_hostnames {
let verifier = InvalidCertsVerifier {
ignore_invalid_hostnames: self.accept_invalid_hostnames,
ignore_invalid_certs: self.accept_invalid_certs,
roots: root_cert_store,
signature_algorithms,
};
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 = "webpki-roots")]
fn load_webpki_roots(store: &mut RootCertStore) {
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
}
match self.cert_store {
CertificateStore::Default => {
#[cfg(feature = "rustls-native-certs")]
load_native_roots(&mut root_cert_store)?;
#[cfg(all(not(feature = "rustls-native-certs"), feature = "webpki-roots"))]
load_webpki_roots(&mut root_cert_store);
}
#[cfg(feature = "webpki-roots")]
CertificateStore::WebpkiRoots => {
load_webpki_roots(&mut root_cert_store);
}
CertificateStore::None => {}
}
for cert in self.root_certs {
for rustls_cert in cert.rustls {
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
}
}
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
root_cert_store,
None,
)))
tls.with_root_certificates(root_cert_store)
};
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()
};
let tls = tls.with_no_client_auth();
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
@@ -489,14 +510,14 @@ 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)]
pub struct Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>,
rustls: Vec<CertificateDer<'static>>,
#[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509,
}
@@ -515,7 +536,7 @@ impl Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)],
rustls: vec![der.into()],
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
})
@@ -535,10 +556,8 @@ impl Certificate {
let mut pem = Cursor::new(pem);
rustls_pemfile::certs(&mut pem)
.collect::<io::Result<Vec<_>>>()
.map_err(|_| error::tls("invalid certificates"))?
.into_iter()
.map(rustls::Certificate)
.collect::<Vec<_>>()
};
Ok(Self {
@@ -558,20 +577,143 @@ impl Debug for Certificate {
}
}
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
#[allow(missing_copy_implementations)]
pub struct Identity {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Identity,
#[cfg(feature = "rustls-tls")]
rustls_tls: (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>),
#[cfg(feature = "boring-tls")]
boring_tls: (boring::x509::X509, PKey<boring::pkey::Private>),
}
impl Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity").finish()
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
Identity {
#[cfg(feature = "native-tls")]
native_tls: self.native_tls.clone(),
#[cfg(feature = "rustls-tls")]
rustls_tls: (self.rustls_tls.0.clone(), self.rustls_tls.1.clone_key()),
#[cfg(feature = "boring-tls")]
boring_tls: (self.boring_tls.0.clone(), self.boring_tls.1.clone()),
}
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
impl Identity {
pub fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: Identity::from_pem_native_tls(pem, key)?,
#[cfg(feature = "rustls-tls")]
rustls_tls: Identity::from_pem_rustls_tls(pem, key)?,
#[cfg(feature = "boring-tls")]
boring_tls: Identity::from_pem_boring_tls(pem, key)?,
})
}
#[cfg(feature = "native-tls")]
fn from_pem_native_tls(pem: &[u8], key: &[u8]) -> Result<native_tls::Identity, Error> {
native_tls::Identity::from_pkcs8(pem, key).map_err(error::tls)
}
#[cfg(feature = "rustls-tls")]
fn from_pem_rustls_tls(
pem: &[u8],
key: &[u8],
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Error> {
let mut key = key;
let key = rustls_pemfile::private_key(&mut key).unwrap().unwrap();
Ok((vec![pem.to_owned().into()], key))
}
#[cfg(feature = "boring-tls")]
fn from_pem_boring_tls(
pem: &[u8],
key: &[u8],
) -> Result<(boring::x509::X509, PKey<boring::pkey::Private>), Error> {
let cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
let key = boring::pkey::PKey::private_key_from_pem(key).map_err(error::tls)?;
Ok((cert, key))
}
}
#[cfg(feature = "rustls-tls")]
struct InvalidCertsVerifier;
#[derive(Debug)]
struct InvalidCertsVerifier {
ignore_invalid_hostnames: bool,
ignore_invalid_certs: bool,
roots: RootCertStore,
signature_algorithms: WebPkiSupportedAlgorithms,
}
#[cfg(feature = "rustls-tls")]
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.signature_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,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, TlsError> {
verify_tls13_signature(
message,
cert,
dss,
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
)
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
rustls::crypto::ring::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}

View File

@@ -0,0 +1,129 @@
use url::Url;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::client::{Tls, TlsParameters};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use super::AsyncSmtpTransportBuilder;
use super::{
authentication::Credentials, error, extension::ClientId, Error, SmtpTransportBuilder,
SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT,
};
pub(crate) trait TransportBuilder {
fn new<T: Into<String>>(server: T) -> Self;
fn tls(self, tls: super::Tls) -> Self;
fn port(self, port: u16) -> Self;
fn credentials(self, credentials: Credentials) -> Self;
fn hello_name(self, name: ClientId) -> Self;
}
impl TransportBuilder for SmtpTransportBuilder {
fn new<T: Into<String>>(server: T) -> Self {
Self::new(server)
}
fn tls(self, tls: super::Tls) -> Self {
self.tls(tls)
}
fn port(self, port: u16) -> Self {
self.port(port)
}
fn credentials(self, credentials: Credentials) -> Self {
self.credentials(credentials)
}
fn hello_name(self, name: ClientId) -> Self {
self.hello_name(name)
}
}
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
impl TransportBuilder for AsyncSmtpTransportBuilder {
fn new<T: Into<String>>(server: T) -> Self {
Self::new(server)
}
fn tls(self, tls: super::Tls) -> Self {
self.tls(tls)
}
fn port(self, port: u16) -> Self {
self.port(port)
}
fn credentials(self, credentials: Credentials) -> Self {
self.credentials(credentials)
}
fn hello_name(self, name: ClientId) -> Self {
self.hello_name(name)
}
}
/// Create a new SmtpTransportBuilder or AsyncSmtpTransportBuilder from a connection URL
pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) -> Result<B, Error> {
let connection_url = Url::parse(connection_url).map_err(error::connection)?;
let tls: Option<String> = connection_url
.query_pairs()
.find(|(k, _)| k == "tls")
.map(|(_, v)| v.to_string());
let host = connection_url
.host_str()
.ok_or_else(|| error::connection("smtp host undefined"))?;
let mut builder = B::new(host);
match (connection_url.scheme(), tls.as_deref()) {
("smtp", None) => {
builder = builder.port(connection_url.port().unwrap_or(SMTP_PORT));
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtp", Some("required")) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
.tls(Tls::Required(TlsParameters::new(host.into())?))
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtp", Some("opportunistic")) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?))
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
("smtps", _) => {
builder = builder
.port(connection_url.port().unwrap_or(SUBMISSIONS_PORT))
.tls(Tls::Wrapper(TlsParameters::new(host.into())?))
}
(scheme, tls) => {
return Err(error::connection(format!(
"Unknown scheme '{scheme}' or tls parameter '{tls:?}', note that a transport with TLS requires one of the TLS features"
)))
}
};
// use the path segment of the URL as name in the name in the HELO / EHLO command
if connection_url.path().len() > 1 {
let name = connection_url.path().trim_matches('/').to_owned();
builder = builder.hello_name(ClientId::Domain(name));
}
if let Some(password) = connection_url.password() {
let percent_decode = |s: &str| {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.map(|cow| cow.into_owned())
.map_err(error::connection)
};
let credentials = Credentials::new(
percent_decode(connection_url.username())?,
percent_decode(password)?,
);
builder = builder.credentials(credentials);
}
Ok(builder)
}

View File

@@ -119,7 +119,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 +129,22 @@ 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"))]
Kind::Tls => f.write_str("tls error")?,
Kind::Transient(ref code) => {
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}")?;
}

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}"),
}
}
}
@@ -190,7 +189,7 @@ impl ServerInfo {
.contains(&Extension::Authentication(mechanism))
}
/// Gets a compatible mechanism from list
/// Gets a compatible mechanism from a list
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
for mechanism in mechanisms {
if self.supports_auth_mechanism(*mechanism) {
@@ -227,16 +226,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 +276,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),
} => write!(f, "{}={}", keyword, XText(value)),
keyword,
value: Some(value),
} => write!(f, "{keyword}={}", XText(value)),
RcptParameter::Other {
ref keyword,
keyword,
value: None,
} => f.write_str(keyword),
}
@@ -292,14 +291,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

@@ -154,6 +154,7 @@ mod async_transport;
pub mod authentication;
pub mod client;
pub mod commands;
mod connection_url;
mod error;
pub mod extension;
#[cfg(feature = "pool")]

View File

@@ -2,7 +2,7 @@ use std::{
fmt::{self, Debug},
mem,
ops::{Deref, DerefMut},
sync::Arc,
sync::{Arc, OnceLock},
time::{Duration, Instant},
};
@@ -10,7 +10,6 @@ use futures_util::{
lock::Mutex,
stream::{self, StreamExt},
};
use once_cell::sync::OnceCell;
use super::{
super::{client::AsyncSmtpConnection, Error},
@@ -22,7 +21,7 @@ pub struct Pool<E: Executor> {
config: PoolConfig,
connections: Mutex<Vec<ParkedConnection>>,
client: AsyncSmtpClient<E>,
handle: OnceCell<E::Handle>,
handle: OnceLock<E::Handle>,
}
struct ParkedConnection {
@@ -41,7 +40,7 @@ impl<E: Executor> Pool<E> {
config,
connections: Mutex::new(Vec::new()),
client,
handle: OnceCell::new(),
handle: OnceLock::new(),
});
{

View File

@@ -5,7 +5,6 @@ use std::{
fmt::{Display, Formatter, Result},
result,
str::FromStr,
string::ToString,
};
use nom::{
@@ -19,7 +18,7 @@ use nom::{
use crate::transport::smtp::{error, Error};
/// First digit indicates severity
/// The first digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity {
@@ -132,6 +131,12 @@ impl Code {
}
}
impl From<Code> for u16 {
fn from(code: Code) -> Self {
code.detail as u16 + 10 * code.category as u16 + 100 * code.severity as u16
}
}
/// Contains an SMTP reply, with separated code and message
///
/// The text message is optional, only the code is mandatory
@@ -317,6 +322,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,6 +1,6 @@
#[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;
@@ -32,12 +32,20 @@ impl Transport for SmtpTransport {
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "pool"))]
conn.quit()?;
conn.abort();
Ok(result)
}
}
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 {
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
///
@@ -58,7 +66,7 @@ impl SmtpTransport {
.tls(Tls::Wrapper(tls_parameters)))
}
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
/// Simple and secure transport, using STARTTLS to obtain encrypted connections
///
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections.
@@ -95,29 +103,106 @@ impl SmtpTransport {
///
/// * No authentication
/// * No TLS
/// * A 60 seconds timeout for smtp commands
/// * A 60-seconds timeout for smtp commands
/// * Port 25
///
/// Consider using [`SmtpTransport::relay`](#method.relay) or
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let new = SmtpInfo {
server: server.into(),
..Default::default()
};
SmtpTransportBuilder::new(server)
}
SmtpTransportBuilder {
info: new,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
/// 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`.
///
/// <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>
///
/// ```rust,no_run
/// use lettre::{
/// message::header::ContentType, transport::smtp::authentication::Credentials, 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();
///
/// // Open a remote connection to example
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")
/// .unwrap()
/// .build();
///
/// // Send the email
/// match mailer.send(&email) {
/// Ok(_) => println!("Email sent successfully!"),
/// Err(e) => panic!("Could not send email: {e:?}"),
/// }
/// ```
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
super::connection_url::from_connection_url(connection_url)
}
/// Tests the SMTP connection
///
/// `test_connection()` tests the connection by using the SMTP NOOP command.
/// The connection is closed afterwards if a connection pool is not used.
/// The connection is closed afterward if a connection pool is not used.
pub fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection()?;
@@ -141,6 +226,20 @@ pub struct SmtpTransportBuilder {
/// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder {
// Create new builder with default parameters
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
let new = SmtpInfo {
server: server.into(),
..Default::default()
};
Self {
info: new,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name;
@@ -194,7 +293,7 @@ impl SmtpTransportBuilder {
/// Build the transport
///
/// If the `pool` feature is enabled an `Arc` wrapped pool is be created.
/// If the `pool` feature is enabled, an `Arc` wrapped pool is created.
/// Defaults can be found at [`PoolConfig`]
pub fn build(self) -> SmtpTransport {
let client = SmtpClient { info: self.info };
@@ -218,9 +317,9 @@ impl SmtpClient {
/// Handles encryption and authentication
pub fn connection(&self) -> Result<SmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match self.info.tls {
let tls_parameters = match &self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
Tls::Wrapper(tls_parameters) => Some(tls_parameters),
_ => None,
};
@@ -234,13 +333,13 @@ impl SmtpClient {
)?;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
match self.info.tls {
Tls::Opportunistic(ref tls_parameters) => {
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)?;
}
_ => (),
@@ -252,3 +351,78 @@ impl SmtpClient {
Ok(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{
transport::smtp::{authentication::Credentials, client::Tls},
SmtpTransport,
};
#[test]
fn transport_from_url() {
let builder = SmtpTransport::from_url("smtp://127.0.0.1:2525").unwrap();
assert_eq!(builder.info.port, 2525);
assert!(matches!(builder.info.tls, Tls::None));
assert_eq!(builder.info.server, "127.0.0.1");
let builder =
SmtpTransport::from_url("smtps://username:password@smtp.example.com:465").unwrap();
assert_eq!(builder.info.port, 465);
assert_eq!(
builder.info.credentials,
Some(Credentials::new(
"username".to_owned(),
"password".to_owned()
))
);
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
assert_eq!(builder.info.server, "smtp.example.com");
let builder = SmtpTransport::from_url(
"smtps://user%40example.com:pa$$word%3F%22!@smtp.example.com:465",
)
.unwrap();
assert_eq!(builder.info.port, 465);
assert_eq!(
builder.info.credentials,
Some(Credentials::new(
"user@example.com".to_owned(),
"pa$$word?\"!".to_owned()
))
);
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
assert_eq!(builder.info.server, "smtp.example.com");
let builder =
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
.unwrap();
assert_eq!(builder.info.port, 587);
assert_eq!(
builder.info.credentials,
Some(Credentials::new(
"username".to_owned(),
"password".to_owned()
))
);
assert!(matches!(builder.info.tls, Tls::Required(_)));
let builder = SmtpTransport::from_url(
"smtp://username:password@smtp.example.com:587?tls=opportunistic",
)
.unwrap();
assert_eq!(builder.info.port, 587);
assert!(matches!(builder.info.tls, Tls::Opportunistic(_)));
let builder = SmtpTransport::from_url("smtps://smtp.example.com").unwrap();
assert_eq!(builder.info.port, 465);
assert_eq!(builder.info.credentials, None);
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
}
}