Compare commits

...

91 Commits

Author SHA1 Message Date
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
Paolo Barbolini
4fb67a7da1 Prepare 0.10.3 (#860) 2023-02-20 11:56:28 +01:00
Paolo Barbolini
9041f210f4 Add Content-Type to all examples sending a basic text/plain message (#859) 2023-02-14 17:54:05 +00:00
Paolo Barbolini
77b7d40fb8 mailbox: replace serialize_str(&self.to_string()) with collect_str(self) (#858) 2023-02-14 18:35:29 +01:00
Paolo Barbolini
2b6d457f85 clippy: deny str_to_string and empty_structs_with_brackets (#857) 2023-02-14 18:33:10 +01:00
Stéphan Kochen
952c1b39df Add support for rustls-native-certs (#843) 2023-02-14 18:11:42 +01:00
Paolo Barbolini
7ecb87f9fd Prepare 0.10.2 (#853) 2023-01-29 14:58:41 +01:00
Paolo Barbolini
fd700b1717 cargo: switch to crates.io release of email-encoding v0.2 (#854) 2023-01-29 14:47:08 +01:00
Paolo Barbolini
f8f19d6af5 clippy: fix latest warnings (#855) 2023-01-29 13:46:57 +00:00
Paolo Barbolini
cc25223914 Update rsa to v0.8 (#852) 2023-01-24 10:26:25 +01:00
Paolo Barbolini
750573d38b Update base64 to v0.21 (#851) 2023-01-24 10:07:48 +01:00
finga
0734a96343 tracing: Write some logs when sending an email (#848)
Write a trace message when sending an email. Further, write a debug
message when using the sendmail transport method, containing which
program is called. And write a debug message containing the target
file name when the file transport method is used.

This should help improve #556 a tiny bit.

Co-authored-by: finga <finga@onders.org>
2023-01-24 09:14:31 +01:00
Christian Mandery
3c2f996856 Fix broken URL for IETF user-agent draft (#849) 2023-01-16 11:34:30 +00:00
lfuerderer
9cae29dd07 Add Content-Type header in documentation example (#841)
* Add Content-Type header in documentation example

In the example showing how to build a message from a pure string, set
the Content-Type to text/plain explicitly.
This header constant also includes the phrase charset=utf-8 so that
non-ascii characters will be displayed correctly.

* cargo fmt

* Fix generated email example

Co-authored-by: Paolo Barbolini <paolo.barbolini@m4ss.net>
2022-12-16 22:04:29 +00:00
Paolo Barbolini
e1a146c8f8 cargo: bump base64 to 0.20 (#840) 2022-12-10 22:29:01 +00:00
Paolo Barbolini
840a19784a cargo: require quoted_printable ^0.4.6 (#837) 2022-12-07 15:37:38 +00:00
Paolo Barbolini
5a61ba36b5 ci: bump nightly used for rustfmt (#817) 2022-11-12 19:51:24 +01:00
Paolo Barbolini
dbf0e53c31 Fix latest clippy warnings (#830) 2022-11-12 17:54:20 +00:00
Paolo Barbolini
c914a07379 Update dependencies (#829) 2022-11-12 17:45:19 +00:00
Paolo Barbolini
2c4fa39523 Use cargo weak dependency features to reduce tokio features (#785) 2022-11-12 17:34:55 +00:00
Clément DOUIN
28f0af16be Mailbox displays wrongly when containing a comma and a non-ascii char in its name (#827) 2022-11-12 17:23:06 +00:00
Paolo Barbolini
f0614be555 Bump MSRV to 1.60 (#828) 2022-11-12 17:13:14 +00:00
André Cruz
a3fcdf263d fix(transport): return whole smtp error string (#821)
We were only returning the first line of the error message,
but that may not be enough to get the whole context of the error.
Since we have already parsed the response, just return the whole error
to the user.

Related to #694
2022-09-22 16:30:04 +02:00
Paolo Barbolini
d4da2e1f14 ci: switch to Swatinem/rust-cache@v2 (#819) 2022-09-10 13:19:49 +02:00
Paolo Barbolini
5655958288 clippy: fix latest warning (#818) 2022-09-10 10:59:49 +00:00
Paolo Barbolini
11b4acf0cd Improve header encoding and wrapping (#811)
Also cleans up the encoder a lot, removing some
complicated logic introduced by the initial round
of implementation
2022-09-10 12:40:00 +02:00
Paolo Barbolini
b3b5df285a Bump idna to 0.3 (#816) 2022-09-09 07:38:59 +00:00
Tom Dryer
3c051d52e7 Remove dependency on regex crate (#815)
Replace implementation of DKIM body canonicalization to remove
dependency on the `regex` crate.

Fixes #768.
2022-08-22 09:44:10 +02:00
André Cruz
d6128a146e use a generic transport trait for async connections (#805)
Rely on a generic transport trait and allow passing in one. This
will enable use cases where we don't have a real Tokio TCP stream,
or have to bind a specific source address before establishing
the connection.
2022-07-27 09:40:13 +02:00
André Cruz
fab6680150 Fix clippy warnings (#807)
Depending on the features chosen these attributes were left
unused.
2022-07-25 18:00:17 +02:00
Paolo Barbolini
0c9fc6cb71 Prepare 0.10.1 (#804) 2022-07-20 10:44:55 +02:00
Paolo Barbolini
2228cbdf93 Fix SMTP dot stuffing (#803) 2022-07-19 23:20:44 +02:00
André Cruz
17c95b0fa8 Ensure connection is closed on abort (#801)
When aborting a connection, ensure the underlying stream is closed
before we return.
2022-07-19 21:13:51 +02:00
Paolo Barbolini
62725af00a Improve TlsVersion docs and remember to re-export it (#800) 2022-07-18 07:40:46 +00:00
André Cruz
758bf1a4a7 Configurable minimum TLS version (#799)
Added support for configuring the minimum accepted TLS version. The
supported versions differ between TLS toolkits.
2022-07-17 13:11:14 +02:00
Paolo Barbolini
054c79f914 Document the boring-tls features in lib.rs (#798) 2022-07-16 09:45:44 +00:00
André Cruz
985fa7edc4 Add support for boring TLS (#797)
In contexts where FIPS certification is mandatory, having the
ability to use the certified boring TLS library is sometimes necessary.
Added initial support for it, only one TLS toolkit can be enabled at
one time.
2022-07-16 11:28:14 +02:00
Paolo Barbolini
9004d4ccc5 Add documentation to undocumented items (#793) 2022-06-29 22:44:29 +02:00
Paolo Barbolini
10171f8c75 Prepare 0.10.0 release (#538) 2022-06-29 21:17:37 +02:00
Paolo Barbolini
99e805952d Make it possible to keep the Bcc header when building a message (#792) 2022-06-29 21:08:27 +02:00
Paolo Barbolini
2d21dde5a1 Add autoconfigure.rs example (#787) 2022-06-17 06:35:07 +00:00
Paolo Barbolini
6fec936c0c Remove useless vec! allocations (#786) 2022-06-16 18:17:19 +00:00
Paolo Barbolini
22dfa5aa96 MessageBuilder: improve order headers are defined in (#783) 2022-06-16 17:53:54 +00:00
Paolo Barbolini
44e4cfd622 clippy: make rules stricter (#784) 2022-06-16 19:42:13 +02:00
Paolo Barbolini
7ea3d38a00 Mailboxes: add FromIterator impl and optimize Extend impl (#782) 2022-06-11 18:18:55 +00:00
Paolo Barbolini
73b89f5a9f clippy: fix latest warnings (#781) 2022-06-11 16:31:12 +00:00
Paolo Barbolini
1ec1b705c9 Prepare 0.10.0-rc.7 (#777) 2022-06-04 11:47:50 +02:00
Paolo Barbolini
e4006518fe Stop using the regex crate for parsing addresses (#776) 2022-06-03 13:39:57 +00:00
Paolo Barbolini
b33dd562fc Fix and improve header wrapping (#774)
Instead of injecting spaces to ensure that lines stay under 76 characters only wrap at whitespace characters. This avoids changing the headers.

A best-effort to keep lines under 76 characters is still done, however it is only done at whitespace. Notably there is no hard wrap enforced. This means that it is possible for headers to break the 1000 character line-length limit in the specification. It is just hoped that the receiver will allow long lines in this case.

Closes #688

Co-authored-by: Kevin Cox <kevincox@kevincox.ca>
2022-06-03 15:24:53 +02:00
Paolo Barbolini
65958df14f Use pretty_assertions for all message tests (#775) 2022-06-02 12:20:06 +00:00
James Hillyerd
50628af5fd README.md: Use IPv4 notation for localhost (#771) 2022-05-30 17:20:51 +00:00
Paolo Barbolini
cf858cc682 Move most email body encoding to email-encoding (#769) 2022-05-30 15:12:42 +00:00
Paolo Barbolini
f9a4b5ba89 Work around async-global-executor bumping MSRV too early (#773) 2022-05-30 15:01:40 +00:00
Jacob Halsey
1391a834ce #715: Support setting the local IP address to connect from (#762)
This adds a `local_address: Option<IpAddr>` parameter to the synchronous, and tokio connect functions.

(As far as I can see there is no current way to support this in async-std, because the library doesn't provide any way to do an async connect for an existing socket)
2022-05-29 07:05:39 +00:00
André Cruz
e6b4529896 use email_address crate for checking formats (#763)
The email_address crate is more strict in validating user and domain
parts of email addresses. For example, it verifies the total size
of the local part, which the current method does not, and this has
caused upstream servers to fail to accept an email.
2022-05-26 19:21:14 +02:00
Kevin Cox
ca5cb3f8f7 Fix encoded header signing. (#765)
The header needs to be properly formatted so that Unicode characters are encoded the same way they will be in the final message. Previously the logical header value was being encoded.

A notable example is that a `'` in the `To:` header needs to be encoded. This was being encoded incorrectly.
2022-05-26 07:44:14 +02:00
Kevin Cox
1e2279457e Add editorconfig file. (#766)
Makes it easy for everyone to use the preferred settings.

https://editorconfig.org/
2022-05-18 13:07:38 +00:00
Kevin Cox
961364cc29 Remove unnecessary clone. (#767)
This is backwards-incompatible but hopefully is an acceptable change for a pre-release. The upgrade path is straight forward.
2022-05-18 10:51:02 +02:00
69 changed files with 2966 additions and 1339 deletions

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
root = true
[*]
insert_final_newline = true
[*.rs]
indent_size = 4
indent_style = space

View File

@@ -13,7 +13,7 @@ env:
jobs: jobs:
rustfmt: rustfmt:
name: rustfmt / nightly-2022-02-11 name: rustfmt / nightly-2023-06-22
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -22,7 +22,7 @@ jobs:
- name: Install rust - name: Install rust
run: | run: |
rustup default nightly-2022-02-11 rustup default nightly-2023-06-22
rustup component add rustfmt rustup component add rustfmt
- name: cargo fmt - name: cargo fmt
@@ -52,17 +52,11 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-check
- name: Install rust - name: Install rust
run: rustup update --no-self-update stable run: rustup update --no-self-update stable
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install cargo hack - name: Install cargo hack
run: cargo install cargo-hack --debug run: cargo install cargo-hack --debug
@@ -81,27 +75,21 @@ jobs:
rust: stable rust: stable
- name: beta - name: beta
rust: beta rust: beta
- name: 1.56.0 - name: '1.70'
rust: 1.56.0 rust: '1.70'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}
- name: Install rust - name: Install rust
run: | run: |
rustup default ${{ matrix.rust }} rustup default ${{ matrix.rust }}
rustup update --no-self-update ${{ matrix.rust }} rustup update --no-self-update ${{ matrix.rust }}
- name: Setup cache
uses: Swatinem/rust-cache@v2
- name: Install postfix - name: Install postfix
run: | run: |
DEBIAN_FRONTEND=noninteractive sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get update
@@ -124,15 +112,24 @@ jobs:
- name: Install dkimverify - name: Install dkimverify
run: sudo apt -y install python3-dkim run: sudo apt -y install python3-dkim
- name: Work around early dependencies MSRV bump
run: |
cargo update -p anstyle --precise 1.0.2
cargo update -p clap --precise 4.3.24
cargo update -p clap_lex --precise 0.5.0
- name: Test with no default features - name: Test with no default features
run: cargo test --no-default-features run: cargo test --no-default-features
- name: Test with default features - name: Test with default features
run: cargo test run: cargo test
- name: Test with all features - name: Test with all features (-native-tls)
run: cargo test --all-features 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-std1,async-std1-rustls-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls-tls,tracing
# coverage: # coverage:
# name: Coverage # name: Coverage
# runs-on: ubuntu-latest # runs-on: ubuntu-latest

View File

@@ -1,5 +1,165 @@
<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)
#### Announcements
It was found that what had been used until now as a basic lettre 0.10
`MessageBuilder::body` example failed to mention that for maximum
compatibility with various email clients a `Content-Type` header
should always be present in the message.
##### Before
```rust
Message::builder()
// [...] some headers skipped for brevity
.body(String::from("A plaintext or html body"))?
```
##### Patch
```diff
Message::builder()
// [...] some headers skipped for brevity
+ .header(ContentType::TEXT_PLAIN) // or `TEXT_HTML` if the body is html
.body(String::from("A plaintext or html body"))?
```
#### Features
* Add support for rustls-native-certs when using rustls ([#843])
[#843]: https://github.com/lettre/lettre/pull/843
<a name="v0.10.2"></a>
### v0.10.2 (2023-01-29)
#### Upgrade notes
* MSRV is now 1.60 ([#828])
#### Features
* Allow providing a custom `tokio` stream for `AsyncSmtpTransport` ([#805])
* Return whole SMTP error message ([#821])
#### Bug fixes
* Mailbox displays wrongly when containing a comma and a non-ascii char in its name ([#827])
* Require `quoted_printable` ^0.4.6 in order to fix encoding of tabs and spaces at the end of line ([#837])
#### Misc
* Increase tracing ([#848])
* Bump `idna` to 0.3 ([#816])
* Update `base64` to 0.21 ([#840] and [#851])
* Update `rsa` to 0.8 ([#829] and [#852])
[#805]: https://github.com/lettre/lettre/pull/805
[#816]: https://github.com/lettre/lettre/pull/816
[#821]: https://github.com/lettre/lettre/pull/821
[#827]: https://github.com/lettre/lettre/pull/827
[#828]: https://github.com/lettre/lettre/pull/828
[#829]: https://github.com/lettre/lettre/pull/829
[#837]: https://github.com/lettre/lettre/pull/837
[#840]: https://github.com/lettre/lettre/pull/840
[#848]: https://github.com/lettre/lettre/pull/848
[#851]: https://github.com/lettre/lettre/pull/851
[#852]: https://github.com/lettre/lettre/pull/852
<a name="v0.10.1"></a>
### v0.10.1 (2022-07-20)
#### Features
* Add `boring-tls` support for `SmtpTransport` and `AsyncSmtpTransport`. The latter is only supported with the tokio runtime. ([#797]) ([#798])
* Make the minimum TLS version configurable. ([#799]) ([#800])
#### Bug Fixes
* Ensure connections are closed on abort. ([#801])
* Fix SMTP dot stuffing. ([#803])
[#797]: https://github.com/lettre/lettre/pull/797
[#798]: https://github.com/lettre/lettre/pull/798
[#799]: https://github.com/lettre/lettre/pull/799
[#800]: https://github.com/lettre/lettre/pull/800
[#801]: https://github.com/lettre/lettre/pull/801
[#803]: https://github.com/lettre/lettre/pull/803
<a name="v0.10.0"></a> <a name="v0.10.0"></a>
### v0.10.0 (unreleased) ### v0.10.0 (2022-06-29)
#### Upgrade notes #### Upgrade notes
@@ -29,6 +189,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
* Refactor `TlsParameters` implementation to not expose the internal TLS library * Refactor `TlsParameters` implementation to not expose the internal TLS library
* `FileTransport` writes emails into `.eml` instead of `.json` * `FileTransport` writes emails into `.eml` instead of `.json`
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility) * When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
* The `sendmail` and `file` transports aren't enabled by default anymore.
* The `new` method of `ClientId` is deprecated * The `new` method of `ClientId` is deprecated
* Rename `serde-impls` feature to `serde` * Rename `serde-impls` feature to `serde`
* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of * The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of

View File

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

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge) # remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-rc.6" version = "0.11.2"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "https://lettre.rs" homepage = "https://lettre.rs"
@@ -11,7 +11,7 @@ authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo
categories = ["email", "network-programming"] categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"] keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2021" edition = "2021"
rust-version = "1.56" rust-version = "1.70"
[badges] [badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" } is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
@@ -19,33 +19,37 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[dependencies] [dependencies]
idna = "0.2" chumsky = "0.9"
once_cell = "1" idna = "0.5"
once_cell = { version = "1", optional = true }
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder # builder
httpdate = { version = "1", optional = true } httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true } mime = { version = "0.3.4", optional = true }
fastrand = { version = "1.4", optional = true } fastrand = { version = "2.0", optional = true }
quoted_printable = { version = "0.4", optional = true } quoted_printable = { version = "0.5", optional = true }
base64 = { version = "0.13", optional = true } base64 = { version = "0.21", optional = true }
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] } email-encoding = { version = "0.2", optional = true }
email-encoding = { version = "0.1", optional = true }
# file transport # file transport
uuid = { version = "1", features = ["v4"], optional = true } uuid = { version = "1", features = ["v4"], optional = true }
serde = { version = "1", optional = true, features = ["derive"] } serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
# smtp # smtp-transport
nom = { version = "7", optional = true } nom = { version = "7", optional = true }
hostname = { version = "0.3", optional = true } # feature hostname = { version = "0.3", optional = true } # feature
socket2 = { version = "0.5.1", optional = true }
url = { version = "2.4", optional = true }
## tls ## tls
native-tls = { version = "0.2", optional = true } # feature native-tls = { version = "0.2.5", optional = true } # feature
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true } rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true }
rustls-pemfile = { version = "1", optional = true } rustls-pemfile = { version = "1", optional = true }
webpki-roots = { version = "0.22", optional = true } rustls-native-certs = { version = "0.6.2", optional = true }
webpki-roots = { version = "0.25", optional = true }
boring = { version = "4", optional = true }
# async # async
futures-io = { version = "0.3.7", optional = true } futures-io = { version = "0.3.7", optional = true }
@@ -53,63 +57,80 @@ futures-util = { version = "0.3.7", default-features = false, features = ["io"],
async-trait = { version = "0.1", optional = true } async-trait = { version = "0.1", optional = true }
## async-std ## async-std
async-std = { version = "1.8", optional = true, features = ["unstable"] } async-std = { version = "1.8", optional = true }
#async-native-tls = { version = "0.3.3", optional = true } #async-native-tls = { version = "0.3.3", optional = true }
futures-rustls = { version = "0.22", optional = true } futures-rustls = { version = "0.24", optional = true }
## tokio ## tokio
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "rt", "process", "time", "net", "io-util"], optional = true } tokio1_crate = { package = "tokio", version = "1", optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true } tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.23", optional = true } tokio1_rustls = { package = "tokio-rustls", version = "0.24", optional = true }
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
## dkim ## dkim
sha2 = { version = "0.10", optional = true } sha2 = { version = "0.10", optional = true, features = ["oid"] }
rsa = { version = "0.6.0", optional = true } rsa = { version = "0.9", optional = true }
ed25519-dalek = { version = "1.0.1", optional = true } ed25519-dalek = { version = "2", optional = true }
# email formats
email_address = { version = "0.2.1", default-features = false }
[dev-dependencies] [dev-dependencies]
criterion = "0.3" pretty_assertions = "1"
criterion = "0.5"
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
glob = "0.3" glob = "0.3"
walkdir = "2" walkdir = "2"
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] } tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] } async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1" serde_json = "1"
maud = "0.23" maud = "0.25"
[[bench]] [[bench]]
harness = false harness = false
name = "transport_smtp" name = "transport_smtp"
[[bench]]
harness = false
name = "mailbox_parsing"
[features] [features]
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"] default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable", "email-encoding"] builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
mime03 = ["mime"] mime03 = ["dep:mime"]
# transports # transports
file-transport = ["uuid"] file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
file-transport-envelope = ["serde", "serde_json", "file-transport"] file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
sendmail-transport = [] sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
smtp-transport = ["base64", "nom"] smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:once_cell", "dep:url", "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"]
boring-tls = ["dep:boring"]
# async # async
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"] async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"] #async-std1-native-tls = ["async-std1", "native-tls", "dep:async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"] async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"] tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"] tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"] tokio1-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
dkim = ["sha2", "rsa", "ed25519-dalek"] dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"] rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
[[example]]
name = "autoconfigure"
required-features = ["smtp-transport", "native-tls"]
[[example]] [[example]]
name = "basic_html" name = "basic_html"
required-features = ["file-transport", "builder"] required-features = ["file-transport", "builder"]

View File

@@ -28,27 +28,14 @@
</div> </div>
<div align="center"> <div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-rc.6"> <a href="https://deps.rs/crate/lettre/0.11.2">
<img src="https://deps.rs/crate/lettre/0.10.0-rc.6/status.svg" <img src="https://deps.rs/crate/lettre/0.11.2/status.svg"
alt="dependency status" /> alt="dependency status" />
</a> </a>
</div> </div>
--- ---
**NOTE**: this readme refers to the 0.10 version of lettre, which is
in release candidate state. Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x)
branch for the previous stable release.
0.10 is already widely used and is already thought to be more reliable than 0.9, so it should generally be used
for new projects.
We'd love to hear your feedback about 0.10 design and APIs before final release!
Start a [discussion](https://github.com/lettre/lettre/discussions) in the repository, whether for
feedback or if you need help or advice using or upgrading lettre 0.10.
---
## Features ## Features
Lettre provides the following features: Lettre provides the following features:
@@ -63,18 +50,24 @@ Lettre does not provide (for now):
* Email parsing * Email parsing
## Supported Rust Versions
Lettre supports all Rust versions released in the last 6 months. At the time of writing
the minimum supported Rust version is 1.70, but this could change at any time either from
one of our dependencies bumping their MSRV or by a new patch release of lettre.
## Example ## Example
This library requires Rust 1.56.0 or newer. This library requires Rust 1.70 or newer.
To use this library, add the following to your `Cargo.toml`: To use this library, add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
lettre = "0.10.0-rc.6" lettre = "0.11"
``` ```
```rust,no_run ```rust,no_run
use lettre::message::header::ContentType;
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport}; use lettre::{Message, SmtpTransport, Transport};
@@ -83,10 +76,11 @@ let email = Message::builder()
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com") let mailer = SmtpTransport::relay("smtp.gmail.com")
@@ -97,14 +91,22 @@ let mailer = SmtpTransport::relay("smtp.gmail.com")
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
``` ```
## Not sure of which connect options to use?
Clone the lettre git repository and run the following command (replacing `SMTP_HOST` with your SMTP server's hostname)
```shell
cargo run --example autoconfigure SMTP_HOST
```
## Testing ## Testing
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
such a server can be launched with `python -m smtpd -n -c DebuggingServer localhost:2525` such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
Alternatively only unit tests can be run by doing `cargo test --lib`. Alternatively only unit tests can be run by doing `cargo test --lib`.

View File

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

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

@@ -1,6 +1,6 @@
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncTransport, Message, AsyncStd1Executor, AsyncTransport, Message,
}; };
#[async_std::main] #[async_std::main]
@@ -12,10 +12,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<AsyncStd1Executor> = let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
@@ -27,6 +28,6 @@ async fn main() {
// Send the email // Send the email
match mailer.send(email).await { match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -1,6 +1,6 @@
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
AsyncTransport, Message, AsyncStd1Executor, AsyncTransport, Message,
}; };
#[async_std::main] #[async_std::main]
@@ -12,10 +12,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer: AsyncSmtpTransport<AsyncStd1Executor> = let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
@@ -27,6 +28,6 @@ async fn main() {
// Send the email // Send the email
match mailer.send(email).await { match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

93
examples/autoconfigure.rs Normal file
View File

@@ -0,0 +1,93 @@
use std::{env, process, time::Duration};
use lettre::SmtpTransport;
fn main() {
tracing_subscriber::fmt::init();
let smtp_host = match env::args().nth(1) {
Some(smtp_host) => smtp_host,
None => {
println!("Please provide the SMTP host as the first argument to this command");
process::exit(1);
}
};
// TLS wrapped connection
{
tracing::info!(
"Trying to establish a TLS wrapped connection to {}",
smtp_host
);
let transport = SmtpTransport::relay(&smtp_host)
.expect("build SmtpTransport::relay")
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a TLS wrapped connection (SmtpTransport::relay). This is the fastest option available for connecting to an SMTP server", smtp_host);
}
Ok(false) => {
tracing::error!("Couldn't connect to {} via a TLS wrapped connection. No more information is available", smtp_host);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via a TLS wrapped connection", smtp_host);
}
}
}
println!();
// 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);
let transport = SmtpTransport::starttls_relay(&smtp_host)
.expect("build SmtpTransport::starttls_relay")
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a plaintext connection which then got upgraded to TLS via the SMTP STARTTLS extension (SmtpTransport::starttls_relay). This is the second best option after the previous TLS wrapped option", smtp_host);
}
Ok(false) => {
tracing::error!(
"Couldn't connect to {} via STARTTLS. No more information is available",
smtp_host
);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via STARTTLS", smtp_host);
}
}
}
println!();
// Plaintext connection (very insecure)
{
tracing::info!(
"Trying to establish a plaintext connection to {}",
smtp_host
);
let transport = SmtpTransport::builder_dangerous(&smtp_host)
.timeout(Some(Duration::from_secs(10)))
.build();
match transport.test_connection() {
Ok(true) => {
tracing::info!("Successfully connected to {} via a plaintext connection. This option is very insecure and shouldn't be used on the public internet (SmtpTransport::builder_dangerous)", smtp_host);
}
Ok(false) => {
tracing::error!(
"Couldn't connect to {} via a plaintext connection. No more information is available",
smtp_host
);
}
Err(err) => {
tracing::error!(err = %err, "Couldn't connect to {} via a plaintext connection", smtp_host);
}
}
}
}

View File

@@ -1,4 +1,4 @@
use lettre::{Message, SmtpTransport, Transport}; use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,6 +8,7 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
@@ -17,6 +18,6 @@ fn main() {
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -1,6 +1,7 @@
use std::fs; use std::fs;
use lettre::{ use lettre::{
message::header::ContentType,
transport::smtp::{ transport::smtp::{
authentication::Credentials, authentication::Credentials,
client::{Certificate, Tls, TlsParameters}, client::{Certificate, Tls, TlsParameters},
@@ -16,18 +17,19 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
// Use a custom certificate stored on disk to securely verify the server's certificate // Use a custom certificate stored on disk to securely verify the server's certificate
let pem_cert = fs::read("certificate.pem").unwrap(); let pem_cert = fs::read("certificate.pem").unwrap();
let cert = Certificate::from_pem(&pem_cert).unwrap(); let cert = Certificate::from_pem(&pem_cert).unwrap();
let tls = TlsParameters::builder("smtp.server.com".to_string()) let tls = TlsParameters::builder("smtp.server.com".to_owned())
.add_root_certificate(cert) .add_root_certificate(cert)
.build() .build()
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to the smtp server // Open a remote connection to the smtp server
let mailer = SmtpTransport::builder_dangerous("smtp.server.com") let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
@@ -39,6 +41,6 @@ fn main() {
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -1,4 +1,7 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,10 +11,11 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com") let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
@@ -22,6 +26,6 @@ fn main() {
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -1,4 +1,7 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; use lettre::{
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
SmtpTransport, Transport,
};
fn main() { fn main() {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@@ -8,10 +11,11 @@ fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year") .subject("Happy new year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy!")) .body(String::from("Be happy!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com") let mailer = SmtpTransport::relay("smtp.gmail.com")
@@ -22,6 +26,6 @@ fn main() {
// Send the email // Send the email
match mailer.send(&email) { match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -2,8 +2,8 @@
// since it uses Rust 2018 crate renaming to import tokio. // since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code. // Won't be needed in user's code.
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
Tokio1Executor, AsyncTransport, Message, Tokio1Executor,
}; };
use tokio1_crate as tokio; use tokio1_crate as tokio;
@@ -16,10 +16,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail using STARTTLS // Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio1Executor> = let mailer: AsyncSmtpTransport<Tokio1Executor> =
@@ -31,6 +32,6 @@ async fn main() {
// Send the email // Send the email
match mailer.send(email).await { match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -2,8 +2,8 @@
// since it uses Rust 2018 crate renaming to import tokio. // since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code. // Won't be needed in user's code.
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
Tokio1Executor, AsyncTransport, Message, Tokio1Executor,
}; };
use tokio1_crate as tokio; use tokio1_crate as tokio;
@@ -16,10 +16,11 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap()) .to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year") .subject("Happy new async year")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Be happy with async!")) .body(String::from("Be happy with async!"))
.unwrap(); .unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
// Open a remote connection to gmail // Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio1Executor> = let mailer: AsyncSmtpTransport<Tokio1Executor> =
@@ -31,6 +32,6 @@ async fn main() {
// Send the email // Send the email
match mailer.send(email).await { match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"), Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e), Err(e) => panic!("Could not send email: {e:?}"),
} }
} }

View File

@@ -8,15 +8,14 @@ use std::{
str::FromStr, str::FromStr,
}; };
use email_address::EmailAddress;
use idna::domain_to_ascii; use idna::domain_to_ascii;
use once_cell::sync::Lazy;
use regex::Regex;
/// Represents an email address with a user and a domain name. /// Represents an email address with a user and a domain name.
/// ///
/// This type contains email in canonical form (_user@domain.tld_). /// This type contains email in canonical form (_user@domain.tld_).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
/// ///
/// # Examples /// # Examples
/// ///
@@ -55,20 +54,6 @@ pub struct Address {
at_start: usize, at_start: usize,
} }
// Regex from the specs
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
// It will mark esoteric email addresses like quoted string as invalid
static USER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
)
.unwrap()
});
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
impl Address { impl Address {
/// Creates a new email address from a user and domain. /// Creates a new email address from a user and domain.
/// ///
@@ -126,7 +111,7 @@ impl Address {
} }
pub(super) fn check_user(user: &str) -> Result<(), AddressError> { pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
if USER_RE.is_match(user) { if EmailAddress::is_valid_local_part(user) {
Ok(()) Ok(())
} else { } else {
Err(AddressError::InvalidUser) Err(AddressError::InvalidUser)
@@ -142,16 +127,19 @@ impl Address {
} }
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> { fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
if DOMAIN_RE.is_match(domain) { // Domain
if EmailAddress::is_valid_domain(domain) {
return Ok(()); return Ok(());
} }
if let Some(caps) = LITERAL_RE.captures(domain) { // IP
if let Some(cap) = caps.get(1) { let ip = domain
if cap.as_str().parse::<IpAddr>().is_ok() { .strip_prefix('[')
return Ok(()); .and_then(|ip| ip.strip_suffix(']'))
} .unwrap_or(domain);
}
if ip.parse::<IpAddr>().is_ok() {
return Ok(());
} }
Err(AddressError::InvalidDomain) Err(AddressError::InvalidDomain)
@@ -196,7 +184,7 @@ where
let domain = domain.as_ref(); let domain = domain.as_ref();
Address::check_domain(domain)?; Address::check_domain(domain)?;
let serialized = format!("{}@{}", user, domain); let serialized = format!("{user}@{domain}");
Ok(Address { Ok(Address {
serialized, serialized,
at_start: user.len(), at_start: user.len(),
@@ -238,7 +226,8 @@ fn check_address(val: &str) -> Result<usize, AddressError> {
Ok(user.len()) Ok(user.len())
} }
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
/// Errors in email addresses parsing /// Errors in email addresses parsing
pub enum AddressError { pub enum AddressError {
/// Missing domain or user /// Missing domain or user
@@ -249,6 +238,8 @@ pub enum AddressError {
InvalidUser, InvalidUser,
/// Invalid email domain /// Invalid email domain
InvalidDomain, InvalidDomain,
/// Invalid input found
InvalidInput,
} }
impl Error for AddressError {} impl Error for AddressError {}
@@ -260,6 +251,7 @@ impl Display for AddressError {
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"), AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"), AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"), AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidInput => f.write_str("Invalid input"),
} }
} }
} }
@@ -269,7 +261,7 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn parse_address() { fn ascii_address() {
let addr_str = "something@example.com"; let addr_str = "something@example.com";
let addr = Address::from_str(addr_str).unwrap(); let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "example.com").unwrap(); let addr2 = Address::new("something", "example.com").unwrap();
@@ -279,4 +271,36 @@ mod tests {
assert_eq!(addr2.user(), "something"); assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "example.com"); assert_eq!(addr2.domain(), "example.com");
} }
#[test]
fn ascii_address_ipv4() {
let addr_str = "something@1.1.1.1";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "1.1.1.1").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "1.1.1.1");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "1.1.1.1");
}
#[test]
fn ascii_address_ipv6() {
let addr_str = "something@[2606:4700:4700::1111]";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "[2606:4700:4700::1111]").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "[2606:4700:4700::1111]");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "[2606:4700:4700::1111]");
}
#[test]
fn check_parts() {
assert!(Address::check_user("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err());
assert!(
Address::check_domain("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com").is_err()
);
}
} }

12
src/base64.rs Normal file
View File

@@ -0,0 +1,12 @@
use ::base64::{
engine::{general_purpose::STANDARD, Engine},
DecodeError,
};
pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
STANDARD.encode(input)
}
pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
STANDARD.decode(input)
}

View File

@@ -109,7 +109,6 @@ impl Executor for Tokio1Executor {
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
type Sleep = tokio1_crate::time::Sleep; type Sleep = tokio1_crate::time::Sleep;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle fn spawn<F>(fut: F) -> Self::Handle
where where
@@ -119,13 +118,11 @@ impl Executor for Tokio1Executor {
tokio1_crate::spawn(fut) tokio1_crate::spawn(fut)
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep { fn sleep(duration: Duration) -> Self::Sleep {
tokio1_crate::time::sleep(duration) tokio1_crate::time::sleep(duration)
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
@@ -146,6 +143,7 @@ impl Executor for Tokio1Executor {
timeout, timeout,
hello_name, hello_name,
tls_parameters, tls_parameters,
None,
) )
.await?; .await?;
@@ -165,13 +163,11 @@ impl Executor for Tokio1Executor {
Ok(conn) Ok(conn)
} }
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> { async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
tokio1_crate::fs::read(path).await tokio1_crate::fs::read(path).await
} }
#[doc(hidden)]
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> { async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
tokio1_crate::fs::write(path, contents).await tokio1_crate::fs::write(path, contents).await
@@ -209,7 +205,6 @@ impl Executor for AsyncStd1Executor {
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
type Sleep = BoxFuture<'static, ()>; type Sleep = BoxFuture<'static, ()>;
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn spawn<F>(fut: F) -> Self::Handle fn spawn<F>(fut: F) -> Self::Handle
where where
@@ -219,14 +214,12 @@ impl Executor for AsyncStd1Executor {
async_std::task::spawn(fut) async_std::task::spawn(fut)
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
fn sleep(duration: Duration) -> Self::Sleep { fn sleep(duration: Duration) -> Self::Sleep {
let fut = async move { async_std::task::sleep(duration).await }; let fut = async_std::task::sleep(duration);
Box::pin(fut) Box::pin(fut)
} }
#[doc(hidden)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
async fn connect( async fn connect(
hostname: &str, hostname: &str,
@@ -266,13 +259,11 @@ impl Executor for AsyncStd1Executor {
Ok(conn) Ok(conn)
} }
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> { async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
async_std::fs::read(path).await async_std::fs::read(path).await
} }
#[doc(hidden)]
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> { async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
async_std::fs::write(path, contents).await async_std::fs::write(path, contents).await
@@ -288,15 +279,13 @@ impl SpawnHandle for async_std::task::JoinHandle<()> {
} }
mod private { mod private {
use super::*;
pub trait Sealed {} pub trait Sealed {}
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
impl Sealed for Tokio1Executor {} impl Sealed for super::Tokio1Executor {}
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
impl Sealed for AsyncStd1Executor {} impl Sealed for super::AsyncStd1Executor {}
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))] #[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
impl Sealed for tokio1_crate::task::JoinHandle<()> {} impl Sealed for tokio1_crate::task::JoinHandle<()> {}

View File

@@ -6,7 +6,7 @@
//! * Secure defaults //! * Secure defaults
//! * Async support //! * Async support
//! //!
//! Lettre requires Rust 1.56.0 or newer. //! Lettre requires Rust 1.70 or newer.
//! //!
//! ## Features //! ## Features
//! //!
@@ -41,6 +41,15 @@
//! //!
//! NOTE: native-tls isn't supported with `async-std` //! NOTE: native-tls isn't supported with `async-std`
//! //!
//! #### SMTP over TLS via the boring crate (Boring TLS)
//!
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
//!
//! * **boring-tls**: TLS support for the synchronous version of the API
//! * **tokio1-boring-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: boring-tls isn't supported with `async-std`
//!
//! #### SMTP over TLS via the rustls crate //! #### SMTP over TLS via the rustls crate
//! //!
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_ //! _Secure SMTP connections using TLS from the `rustls-tls` crate_
@@ -100,7 +109,7 @@
//! [mime 0.3]: https://docs.rs/mime/0.3 //! [mime 0.3]: https://docs.rs/mime/0.3
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376 //! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.6")] #![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.2")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")] #![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")] #![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -112,12 +121,32 @@
unused_import_braces, unused_import_braces,
rust_2018_idioms, rust_2018_idioms,
clippy::string_add, clippy::string_add,
clippy::string_add_assign clippy::string_add_assign,
clippy::clone_on_ref_ptr,
clippy::verbose_file_reads,
clippy::unnecessary_self_imports,
clippy::string_to_string,
clippy::mem_forget,
clippy::cast_lossless,
clippy::inefficient_to_string,
clippy::inline_always,
clippy::linkedlist,
clippy::macro_use_imports,
clippy::manual_assert,
clippy::unnecessary_join,
clippy::wildcard_imports,
clippy::str_to_string,
clippy::empty_structs_with_brackets,
clippy::zero_sized_map_values
)] )]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(not(lettre_ignore_tls_mismatch))] #[cfg(not(lettre_ignore_tls_mismatch))]
mod compiletime_checks { mod compiletime_checks {
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]
compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
the executable will fail to link.");
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
feature = "native-tls", feature = "native-tls",
@@ -136,6 +165,15 @@ mod compiletime_checks {
If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake. If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate."); Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
#[cfg(all(
feature = "tokio1",
feature = "boring-tls",
not(feature = "tokio1-boring-tls")
))]
compile_error!("Lettre is being built with the `tokio1` and the `boring-tls` features, but the `tokio1-boring-tls` feature hasn't been turned on.
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( #[cfg(all(
feature = "async-std1", feature = "async-std1",
@@ -167,6 +205,8 @@ Make sure to apply the same to any of your crate dependencies that use the `lett
} }
pub mod address; pub mod address;
#[cfg(any(feature = "smtp-transport", feature = "dkim"))]
mod base64;
pub mod error; pub mod error;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod executor; mod executor;
@@ -179,11 +219,11 @@ use std::error::Error as StdError;
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
pub use self::executor::AsyncStd1Executor; pub use self::executor::AsyncStd1Executor;
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::executor::Executor; pub use self::executor::Executor;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor; pub use self::executor::Tokio1Executor;
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
#[doc(inline)] #[doc(inline)]
pub use self::transport::AsyncTransport; pub use self::transport::AsyncTransport;
pub use crate::address::Address; pub use crate::address::Address;

View File

@@ -13,9 +13,9 @@ pub struct Attachment {
#[derive(Clone)] #[derive(Clone)]
enum Disposition { enum Disposition {
/// file name /// File name
Attached(String), Attached(String),
/// content id /// Content id
Inline(String), Inline(String),
} }
@@ -96,7 +96,7 @@ impl Attachment {
builder.header(header::ContentDisposition::attachment(&filename)) builder.header(header::ContentDisposition::attachment(&filename))
} }
Disposition::Inline(content_id) => builder Disposition::Inline(content_id) => builder
.header(header::ContentId::from(format!("<{}>", content_id))) .header(header::ContentId::from(format!("<{content_id}>")))
.header(header::ContentDisposition::inline()), .header(header::ContentDisposition::inline()),
}; };
builder = builder.header(content_type); builder = builder.header(content_type);

View File

@@ -1,8 +1,4 @@
use std::{ use std::{mem, ops::Deref};
io::{self, Write},
mem,
ops::Deref,
};
use crate::message::header::ContentTransferEncoding; use crate::message::header::ContentTransferEncoding;
@@ -41,7 +37,7 @@ impl Body {
pub fn new<B: Into<MaybeString>>(buf: B) -> Self { pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
let mut buf: MaybeString = buf.into(); let mut buf: MaybeString = buf.into();
let encoding = buf.encoding(); let encoding = buf.encoding(false);
buf.encode_crlf(); buf.encode_crlf();
Self::new_impl(buf.into(), encoding) Self::new_impl(buf.into(), encoding)
} }
@@ -61,7 +57,22 @@ impl Body {
) -> Result<Self, Vec<u8>> { ) -> Result<Self, Vec<u8>> {
let mut buf: MaybeString = buf.into(); let mut buf: MaybeString = buf.into();
if !buf.is_encoding_ok(encoding) { let best_encoding = buf.encoding(true);
let ok = match (encoding, best_encoding) {
(ContentTransferEncoding::SevenBit, ContentTransferEncoding::SevenBit) => true,
(
ContentTransferEncoding::EightBit,
ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit,
) => true,
(ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit, _) => false,
(
ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64
| ContentTransferEncoding::Binary,
_,
) => true,
};
if !ok {
return Err(buf.into()); return Err(buf.into());
} }
@@ -91,36 +102,13 @@ impl Body {
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable) Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
} }
ContentTransferEncoding::Base64 => { ContentTransferEncoding::Base64 => {
let base64_len = buf.len() * 4 / 3 + 4; let len = email_encoding::body::base64::encoded_len(buf.len());
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
let mut out = Vec::with_capacity(base64_endings_len); let mut out = String::with_capacity(len);
{ email_encoding::body::base64::encode(&buf, &mut out)
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH); .expect("encode body as base64");
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails"); Self::dangerous_pre_encoded(out.into_bytes(), ContentTransferEncoding::Base64)
// modified Write::write_all to work around base64 crate bug
// TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
{
let mut buf: &[u8] = buf.as_ref();
while !buf.is_empty() {
match writer.write(buf) {
Ok(0) => {
// ignore 0 writes
}
Ok(n) => {
buf = &buf[n..];
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => panic!("base64 encoding never fails: {}", e),
}
}
}
}
Self::dangerous_pre_encoded(out, ContentTransferEncoding::Base64)
} }
} }
} }
@@ -153,21 +141,20 @@ impl Body {
impl MaybeString { impl MaybeString {
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString` /// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
/// ///
/// If the `MaybeString` was created from a `String` composed only of US-ASCII /// The `binary` encoding is never returned
/// characters, with no lines longer than 1000 characters, then 7bit fn encoding(&self, supports_utf8: bool) -> ContentTransferEncoding {
/// encoding will be used, else quoted-printable will be chosen. use email_encoding::body::Encoding;
///
/// If the `MaybeString` was instead created from a `Vec<u8>`, base64 encoding is always let output = match self {
/// chosen. Self::String(s) => Encoding::choose(s.as_str(), supports_utf8),
/// Self::Binary(b) => Encoding::choose(b.as_slice(), supports_utf8),
/// `8bit` and `binary` encodings are never returned, as they may not be };
/// supported by all SMTP servers.
pub fn encoding(&self) -> ContentTransferEncoding { match output {
match &self { Encoding::SevenBit => ContentTransferEncoding::SevenBit,
Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit, Encoding::EightBit => ContentTransferEncoding::EightBit,
// TODO: consider when base64 would be a better option because of output size Encoding::QuotedPrintable => ContentTransferEncoding::QuotedPrintable,
Self::String(_) => ContentTransferEncoding::QuotedPrintable, Encoding::Base64 => ContentTransferEncoding::Base64,
Self::Binary(_) => ContentTransferEncoding::Base64,
} }
} }
@@ -178,18 +165,6 @@ impl MaybeString {
Self::Binary(_) => {} Self::Binary(_) => {}
} }
} }
/// Returns `true` if using `encoding` to encode this `MaybeString`
/// would result into an invalid encoded body.
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool {
match encoding {
ContentTransferEncoding::SevenBit => is_7bit_encoded(self),
ContentTransferEncoding::EightBit => is_8bit_encoded(self),
ContentTransferEncoding::Binary
| ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64 => true,
}
}
} }
/// A trait for something that takes an encoded [`Body`]. /// A trait for something that takes an encoded [`Body`].
@@ -273,73 +248,6 @@ impl Deref for MaybeString {
} }
} }
/// Checks whether it contains only US-ASCII characters,
/// and no lines are longer than 1000 characters including the `\n` character.
///
/// Most efficient content encoding available
fn is_7bit_encoded(buf: &[u8]) -> bool {
buf.is_ascii() && !contains_too_long_lines(buf)
}
/// Checks that no lines are longer than 1000 characters,
/// including the `\n` character.
/// NOTE: 8bit isn't supported by all SMTP servers.
fn is_8bit_encoded(buf: &[u8]) -> bool {
!contains_too_long_lines(buf)
}
/// Checks if there are lines that are longer than 1000 characters,
/// including the `\n` character.
fn contains_too_long_lines(buf: &[u8]) -> bool {
buf.len() > 1000 && buf.split(|&b| b == b'\n').any(|line| line.len() > 999)
}
const LINE_SEPARATOR: &[u8] = b"\r\n";
const LINE_MAX_LENGTH: usize = 78 - LINE_SEPARATOR.len();
/// A `Write`r that inserts a line separator `\r\n` every `max_line_length` bytes.
struct LineWrappingWriter<'a, W> {
writer: &'a mut W,
current_line_length: usize,
max_line_length: usize,
}
impl<'a, W> LineWrappingWriter<'a, W> {
pub fn new(writer: &'a mut W, max_line_length: usize) -> Self {
Self {
writer,
current_line_length: 0,
max_line_length,
}
}
}
impl<'a, W> Write for LineWrappingWriter<'a, W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let remaining_line_len = self.max_line_length - self.current_line_length;
let write_len = std::cmp::min(buf.len(), remaining_line_len);
self.writer.write_all(&buf[..write_len])?;
if remaining_line_len == write_len {
self.writer.write_all(LINE_SEPARATOR)?;
self.current_line_length = 0;
} else {
self.current_line_length += write_len;
}
Ok(write_len)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
/// In place conversion to CRLF line endings /// In place conversion to CRLF line endings
fn in_place_crlf_line_endings(string: &mut String) { fn in_place_crlf_line_endings(string: &mut String) {
let indices = find_all_lf_char_indices(string); let indices = find_all_lf_char_indices(string);
@@ -377,6 +285,8 @@ fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding}; use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
#[test] #[test]
@@ -509,13 +419,10 @@ mod test {
#[test] #[test]
fn quoted_printable_detect() { fn quoted_printable_detect() {
let encoded = Body::new(String::from("Привет, мир!")); let encoded = Body::new(String::from("Questo messaggio è corto"));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!( assert_eq!(encoded.as_ref(), b"Questo messaggio =C3=A8 corto");
encoded.as_ref(),
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
);
} }
#[test] #[test]
@@ -547,14 +454,17 @@ mod test {
#[test] #[test]
fn quoted_printable_encode_line_wrap() { fn quoted_printable_encode_line_wrap() {
let encoded = Body::new(String::from("Текст письма в уникоде")); let encoded = Body::new(String::from(
"Se lo standard 📬 fosse stato più semplice avremmo finito molto prima.",
));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable); assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
println!("{}", std::str::from_utf8(encoded.as_ref()).unwrap());
assert_eq!( assert_eq!(
encoded.as_ref(), encoded.as_ref(),
concat!( concat!(
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n", "Se lo standard =F0=9F=93=AC fosse stato pi=C3=B9 semplice avremmo finito mo=\r\n",
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5" "lto prima."
) )
.as_bytes() .as_bytes()
); );
@@ -562,27 +472,31 @@ mod test {
#[test] #[test]
fn base64_detect() { fn base64_detect() {
let input = Body::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); let input = Body::new(vec![0; 80]);
let encoding = input.encoding(); let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::Base64); assert_eq!(encoding, ContentTransferEncoding::Base64);
} }
#[test] #[test]
fn base64_encode_bytes() { fn base64_encode_bytes() {
let encoded = Body::new_with_encoding( let encoded =
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], Body::new_with_encoding(vec![0; 80], ContentTransferEncoding::Base64).unwrap();
ContentTransferEncoding::Base64,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64); assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(encoded.as_ref(), b"AAECAwQFBgcICQ=="); assert_eq!(
encoded.as_ref(),
concat!(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
)
.as_bytes()
);
} }
#[test] #[test]
fn base64_encode_bytes_wrapping() { fn base64_encode_bytes_wrapping() {
let encoded = Body::new_with_encoding( let encoded = Body::new_with_encoding(
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
ContentTransferEncoding::Base64, ContentTransferEncoding::Base64,
) )
.unwrap(); .unwrap();

View File

@@ -1,15 +1,13 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
error::Error as StdError, error::Error as StdError,
fmt::{self, Display, Write}, fmt::{self, Display},
iter::IntoIterator, iter::IntoIterator,
time::SystemTime, time::SystemTime,
}; };
use ed25519_dalek::Signer; use ed25519_dalek::Signer;
use once_cell::sync::Lazy; use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs1v15::Pkcs1v15Sign, RsaPrivateKey};
use regex::{bytes::Regex as BRegex, Regex};
use rsa::{pkcs1::DecodeRsaPrivateKey, Hash, PaddingScheme, RsaPrivateKey};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use crate::message::{ use crate::message::{
@@ -96,9 +94,9 @@ impl Display for DkimSigningKeyError {
impl StdError for DkimSigningKeyError { impl StdError for DkimSigningKeyError {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(match &self.0 { Some(match &self.0 {
InnerDkimSigningKeyError::Base64(err) => &*err, InnerDkimSigningKeyError::Base64(err) => err,
InnerDkimSigningKeyError::Rsa(err) => &*err, InnerDkimSigningKeyError::Rsa(err) => err,
InnerDkimSigningKeyError::Ed25519(err) => &*err, InnerDkimSigningKeyError::Ed25519(err) => err,
}) })
} }
} }
@@ -110,26 +108,30 @@ pub struct DkimSigningKey(InnerDkimSigningKey);
#[derive(Debug)] #[derive(Debug)]
enum InnerDkimSigningKey { enum InnerDkimSigningKey {
Rsa(RsaPrivateKey), Rsa(RsaPrivateKey),
Ed25519(ed25519_dalek::Keypair), Ed25519(ed25519_dalek::SigningKey),
} }
impl DkimSigningKey { impl DkimSigningKey {
pub fn new( pub fn new(
private_key: String, private_key: &str,
algorithm: DkimSigningAlgorithm, algorithm: DkimSigningAlgorithm,
) -> Result<DkimSigningKey, DkimSigningKeyError> { ) -> Result<DkimSigningKey, DkimSigningKeyError> {
Ok(Self(match algorithm { Ok(Self(match algorithm {
DkimSigningAlgorithm::Rsa => InnerDkimSigningKey::Rsa( DkimSigningAlgorithm::Rsa => InnerDkimSigningKey::Rsa(
RsaPrivateKey::from_pkcs1_pem(&private_key) RsaPrivateKey::from_pkcs1_pem(private_key)
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?, .map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
), ),
DkimSigningAlgorithm::Ed25519 => { DkimSigningAlgorithm::Ed25519 => {
InnerDkimSigningKey::Ed25519( InnerDkimSigningKey::Ed25519(ed25519_dalek::SigningKey::from_bytes(
ed25519_dalek::Keypair::from_bytes(&base64::decode(private_key).map_err( &crate::base64::decode(private_key)
|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)), .map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)))?
)?) .try_into()
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?, .map_err(|_| {
) DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(
ed25519_dalek::ed25519::Error::new(),
))
})?,
))
} }
})) }))
} }
@@ -142,19 +144,18 @@ impl DkimSigningKey {
} }
/// A struct to describe Dkim configuration applied when signing a message /// A struct to describe Dkim configuration applied when signing a message
/// selector: the name of the key publied in DNS
/// domain: the domain for which we sign the message
/// private_key: private key in PKCS1 string format
/// headers: a list of headers name to be included in the signature. Signing of more than one
/// header with same name is not supported
/// canonicalization: the canonicalization to be applied on the message
/// pub signing_algorithm: the signing algorithm to be used when signing
#[derive(Debug)] #[derive(Debug)]
pub struct DkimConfig { pub struct DkimConfig {
/// The name of the key published in DNS
selector: String, selector: String,
/// The domain for which we sign the message
domain: String, domain: String,
/// The private key in PKCS1 string format
private_key: DkimSigningKey, private_key: DkimSigningKey,
/// A list of header names to be included in the signature. Signing of more than one
/// header with the same name is not supported
headers: Vec<HeaderName>, headers: Vec<HeaderName>,
/// The signing algorithm to be used when signing
canonicalization: DkimCanonicalization, canonicalization: DkimCanonicalization,
} }
@@ -219,45 +220,91 @@ fn dkim_header_format(
/// Canonicalize the body of an email /// Canonicalize the body of an email
fn dkim_canonicalize_body( fn dkim_canonicalize_body(
body: &[u8], mut body: &[u8],
canonicalization: DkimCanonicalizationType, canonicalization: DkimCanonicalizationType,
) -> Cow<'_, [u8]> { ) -> Cow<'_, [u8]> {
static RE: Lazy<BRegex> = Lazy::new(|| BRegex::new("(\r\n)+$").unwrap());
static RE_DOUBLE_SPACE: Lazy<BRegex> = Lazy::new(|| BRegex::new("[\\t ]+").unwrap());
static RE_SPACE_EOL: Lazy<BRegex> = Lazy::new(|| BRegex::new("[\t ]\r\n").unwrap());
match canonicalization { match canonicalization {
DkimCanonicalizationType::Simple => RE.replace(body, &b"\r\n"[..]), DkimCanonicalizationType::Simple => {
DkimCanonicalizationType::Relaxed => { // Remove empty lines at end
let body = RE_DOUBLE_SPACE.replace_all(body, &b" "[..]); while body.ends_with(b"\r\n\r\n") {
let body = match RE_SPACE_EOL.replace_all(&body, &b"\r\n"[..]) { body = &body[..body.len() - 2];
Cow::Borrowed(_body) => body,
Cow::Owned(body) => Cow::Owned(body),
};
match RE.replace(&body, &b"\r\n"[..]) {
Cow::Borrowed(_body) => body,
Cow::Owned(body) => Cow::Owned(body),
} }
Cow::Borrowed(body)
}
DkimCanonicalizationType::Relaxed => {
let mut out = Vec::with_capacity(body.len());
loop {
match body {
[b' ' | b'\t', b'\r', b'\n', ..] => {}
[b' ' | b'\t', b' ' | b'\t', ..] => {}
[b' ' | b'\t', ..] => out.push(b' '),
[c, ..] => out.push(*c),
[] => break,
}
body = &body[1..];
}
// Remove empty lines at end
while out.ends_with(b"\r\n\r\n") {
out.truncate(out.len() - 2);
}
Cow::Owned(out)
} }
} }
} }
/// Canonicalize the value of an header fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
fn dkim_canonicalize_header_value( let mut r = String::with_capacity(headers.len());
value: &str,
canonicalization: DkimCanonicalizationType, fn skip_whitespace(h: &str) -> &str {
) -> Cow<'_, str> { match h.as_bytes().first() {
match canonicalization { Some(b' ' | b'\t') => skip_whitespace(&h[1..]),
DkimCanonicalizationType::Simple => Cow::Borrowed(value), _ => h,
DkimCanonicalizationType::Relaxed => {
static RE_EOL: Lazy<Regex> = Lazy::new(|| Regex::new("\r\n").unwrap());
static RE_SPACES: Lazy<Regex> = Lazy::new(|| Regex::new("[\\t ]+").unwrap());
let value = RE_EOL.replace_all(value, "");
Cow::Owned(format!(
"{}\r\n",
RE_SPACES.replace_all(&value, " ").trim_end()
))
} }
} }
fn name(h: &str, out: &mut String) {
if let Some(name_end) = h.bytes().position(|c| c == b':') {
let (name, rest) = h.split_at(name_end + 1);
*out += name;
// Space after header colon is stripped.
value(skip_whitespace(rest), out);
} else {
// This should never happen.
*out += h;
}
}
fn value(h: &str, out: &mut String) {
match h.as_bytes() {
// Continuation lines.
[b'\r', b'\n', b' ' | b'\t', ..] => {
out.push(' ');
value(skip_whitespace(&h[2..]), out);
}
// End of header.
[b'\r', b'\n', ..] => {
*out += "\r\n";
name(&h[2..], out)
}
// Sequential whitespace.
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
// All whitespace becomes spaces.
[b'\t', ..] => {
out.push(' ');
value(&h[1..], out)
}
[_, ..] => {
let mut chars = h.chars();
out.push(chars.next().unwrap());
value(chars.as_str(), out)
}
[] => {}
}
}
name(headers, &mut r);
r
} }
/// Canonicalize header tag /// Canonicalize header tag
@@ -277,56 +324,44 @@ fn dkim_canonicalize_headers<'a>(
mail_headers: &Headers, mail_headers: &Headers,
canonicalization: DkimCanonicalizationType, canonicalization: DkimCanonicalizationType,
) -> String { ) -> String {
let mut covered_headers = Headers::new();
for name in headers_list {
if let Some(h) = mail_headers.find_header(name) {
let name = dkim_canonicalize_header_tag(name, canonicalization);
covered_headers.insert_raw(HeaderValue::dangerous_new_pre_encoded(
HeaderName::new_from_ascii(name.into()).unwrap(),
h.get_raw().into(),
h.get_encoded().into(),
));
}
}
let serialized = covered_headers.to_string();
match canonicalization { match canonicalization {
DkimCanonicalizationType::Simple => { DkimCanonicalizationType::Simple => serialized,
let mut signed_headers = Headers::new(); DkimCanonicalizationType::Relaxed => dkim_canonicalize_headers_relaxed(&serialized),
for h in headers_list {
let h = dkim_canonicalize_header_tag(h, canonicalization);
if let Some(value) = mail_headers.get_raw(&h) {
signed_headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii(h.into()).unwrap(),
dkim_canonicalize_header_value(value, canonicalization).to_string(),
))
}
}
signed_headers.to_string()
}
DkimCanonicalizationType::Relaxed => {
let mut signed_headers = String::new();
for h in headers_list {
let h = dkim_canonicalize_header_tag(h, canonicalization);
if let Some(value) = mail_headers.get_raw(&h) {
write!(
signed_headers,
"{}:{}",
h,
dkim_canonicalize_header_value(value, canonicalization)
)
.expect("write implementation returned an error")
}
}
signed_headers
}
} }
} }
/// Sign with Dkim a message by adding Dkim-Signture header created with configuration expressed by /// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
/// dkim_config /// dkim_config
pub(super) fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
let timestamp = SystemTime::now() pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
dkim_sign_fixed_time(message, dkim_config, SystemTime::now())
}
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
let timestamp = timestamp
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs(); .as_secs();
let headers = message.headers(); let headers = message.headers();
let body_hash = Sha256::digest(&dkim_canonicalize_body( let body_hash = Sha256::digest(dkim_canonicalize_body(
&message.body_raw(), &message.body_raw(),
dkim_config.canonicalization.body, dkim_config.canonicalization.body,
)); ));
let bh = base64::encode(body_hash); let bh = crate::base64::encode(body_hash);
let mut signed_headers_list = let mut signed_headers_list =
dkim_config dkim_config
.headers .headers
@@ -358,16 +393,13 @@ pub(super) fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
hashed_headers.update(canonicalized_dkim_header.trim_end().as_bytes()); hashed_headers.update(canonicalized_dkim_header.trim_end().as_bytes());
let hashed_headers = hashed_headers.finalize(); let hashed_headers = hashed_headers.finalize();
let signature = match &dkim_config.private_key.0 { let signature = match &dkim_config.private_key.0 {
InnerDkimSigningKey::Rsa(private_key) => base64::encode( InnerDkimSigningKey::Rsa(private_key) => crate::base64::encode(
private_key private_key
.sign( .sign(Pkcs1v15Sign::new::<Sha256>(), &hashed_headers)
PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA2_256)),
&hashed_headers,
)
.unwrap(), .unwrap(),
), ),
InnerDkimSigningKey::Ed25519(private_key) => { InnerDkimSigningKey::Ed25519(private_key) => {
base64::encode(private_key.sign(&hashed_headers).to_bytes()) crate::base64::encode(private_key.sign(&hashed_headers).to_bytes())
} }
}; };
let dkim_header = dkim_header_format( let dkim_header = dkim_header_format(
@@ -385,21 +417,47 @@ pub(super) fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::{ use pretty_assertions::assert_eq;
io::Write,
process::{Command, Stdio},
};
use super::{ use super::{
super::{ super::{
header::{HeaderName, HeaderValue}, header::{HeaderName, HeaderValue},
Header, Message, Header, Message,
}, },
dkim_canonicalize_body, dkim_canonicalize_header_value, dkim_canonicalize_headers, dkim_canonicalize_body, dkim_canonicalize_headers, dkim_sign_fixed_time,
DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm, DkimSigningKey, DkimCanonicalization, DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm,
DkimSigningKey,
}; };
use crate::StdError; use crate::StdError;
const KEY_RSA: &str = "-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAwOsW7UFcWn1ch3UM8Mll5qZH5hVHKJQ8Z0tUlebUECq0vjw6
VcsIucZ/B70VpCN63whyi7oApdCIS1o0zad7f0UaW/BfxXADqdcFL36uMaG0RHer
uSASjQGnsl9Kozt/dXiDZX5ngjr/arLJhNZSNR4/9VSwqbE2OPXaSaQ9BsqneD0P
8dCVSfkkDZCcfC2864z7hvC01lFzWQKF36ZAoGBERHScHtFMAzUOgGuqqPiP5khw
DQB3Ffccf+BsWLU2OOteshUwTGjpoangbPCYj6kckwNm440lQwuqTinpC92yyIE5
Ol8psNMW49DLowAeZb6JrjLhD+wY9bghTaOkcwIDAQABAoIBAHTZ8LkkrdvhsvoZ
XA088AwVC9fBa6iYoT2v0zw45JomQ/Q2Zt8wa8ibAradQU56byJI65jWwS2ucd+y
c+ldWOBt6tllb50XjCCDrRBnmvtVBuux0MIBOztNlVXlgj/8+ecdZ/lB51Bqi+sF
ACsF5iVmfTcMZTVjsYQu5llUseI6Lwgqpx6ktaXD2PVsVo9Gf01ssZ4GCy69wB/3
20CsOz4LEpSYkq1oE98lMMGCfD7py3L9kWHYNNisam78GM+1ynRxRGwEDUbz6pxs
fGPIAwHLaZsOmibPkBB0PJTW742w86qQ8KAqC6ZbRYOF19rSMj3oTfRnPMHn9Uu5
N8eQcoECgYEA97SMUrz2hqII5i8igKylO9kV8pjcIWKI0rdt8MKj4FXTNYjjO9I+
41ONOjhUOpFci/G3YRKi8UiwbKxIRTvIxNMh2xj6Ws3iO9gQHK1j8xTWxJdjEBEz
EuZI59Mi5H7fxSL1W+n8nS8JVsaH93rvQErngqTUAsihAzjxHWdFwm0CgYEAx2Dh
claESJP2cOKgYp+SUNwc26qMaqnl1f37Yn+AflrQOfgQqJe5TRbicEC+nFlm6XUt
3st1Nj29H0uOMmMZDmDCO+cOs5Qv5A9pG6jSC6wM+2KNHQDtrxlakBFygePEPVVy
GXaY9DRa9Q4/4ataxDR2/VvIAWfEEtMTJIBDtl8CgYAIXEuwLziS6r0qJ8UeWrVp
A7a97XLgnZbIpfBMBAXL+JmcYPZqenos6hEGOgh9wZJCFvJ9kEd3pWBvCpGV5KKu
IgIuhvVMQ06zfmNs1F1fQwDMud9aF3qF1Mf5KyMuWynqWXe2lns0QvYpu6GzNK8G
mICf5DhTr7nfhfh9aZLtMQKBgCxKsmqzG5n//MxhHB4sstVxwJtwDNeZPKzISnM8
PfBT/lQSbqj1Y73japRjXbTgC4Ore3A2JKjTGFN+dm1tJGDUT/H8x4BPWEBCyCfT
3i2noA6sewrJbQPsDvlYVubSEYNKmxlbBmmhw98StlBMv9I8kX6BSDI/uggwid0e
/WvjAoGBAKpZ0UOKQyrl9reBiUfrpRCvIMakBMd79kNiH+5y0Soq/wCAnAuABayj
XEIBhFv+HxeLEnT7YV+Zzqp5L9kKw/EU4ik3JX/XsEihdSxEuGX00ZYOw05FEfpW
cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
-----END RSA PRIVATE KEY-----";
#[derive(Clone)] #[derive(Clone)]
struct TestHeader(String); struct TestHeader(String);
@@ -417,112 +475,143 @@ mod test {
} }
} }
#[test]
fn test_body_simple_canonicalize() {
let body = b"test\r\n\r\ntest \ttest\r\n\r\n\r\n";
let expected: &[u8] = b"test\r\n\r\ntest \ttest\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Simple),
expected
)
}
#[test]
fn test_body_relaxed_canonicalize() {
let body = b"test\r\n\r\ntest \ttest\r\n\r\n\r\n";
let expected: &[u8] = b"test\r\n\r\ntest test\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Relaxed),
expected
)
}
#[test]
fn test_header_simple_canonicalize() {
let value = "test\r\n\r\ntest \ttest\r\n";
let expected = "test\r\n\r\ntest \ttest\r\n";
assert_eq!(
dkim_canonicalize_header_value(value, DkimCanonicalizationType::Simple),
expected
)
}
#[test]
fn test_header_relaxed_canonicalize() {
let value = "test\r\n\r\ntest \ttest\r\n";
let expected = "testtest test\r\n";
assert_eq!(
dkim_canonicalize_header_value(value, DkimCanonicalizationType::Relaxed),
expected
)
}
fn test_message() -> Message { fn test_message() -> Message {
Message::builder() Message::builder()
.from("Test <test+ezrz@example.net>".parse().unwrap()) .from("Test O'Leary <test+ezrz@example.net>".parse().unwrap())
.to("Test2 <test2@example.org>".parse().unwrap()) .to("Test2 <test2@example.org>".parse().unwrap())
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_string())) .date(std::time::UNIX_EPOCH)
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_owned()))
.subject("Test with utf-8 ë") .subject("Test with utf-8 ë")
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_string()).unwrap() .body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_owned()).unwrap()
} }
#[test] #[test]
fn test_headers_simple_canonicalize() { fn test_headers_simple_canonicalize() {
let message = test_message(); let message = test_message();
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple),"From: Test <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be \r\n folded to several lines \r\n") dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
} }
#[test] #[test]
fn test_headers_relaxed_canonicalize() { fn test_headers_relaxed_canonicalize() {
let message = test_message(); let message = test_message();
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:Test <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n") dbg!(message.headers.to_string());
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
} }
#[test] #[test]
fn test_signature_rsa() { fn test_body_simple_canonicalize() {
let body = b" C \r\nD \t E\r\n\r\n\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Simple).into_owned(),
b" C \r\nD \t E\r\n"
);
}
#[test]
fn test_body_relaxed_canonicalize() {
let body = b" C \r\nD \t E\r\n\tF\r\n\t\r\n\r\n\r\n";
assert_eq!(
dkim_canonicalize_body(body, DkimCanonicalizationType::Relaxed).into_owned(),
b" C\r\nD E\r\n F\r\n"
);
}
#[test]
fn test_signature_rsa_simple() {
let mut message = test_message(); let mut message = test_message();
let key = "-----BEGIN RSA PRIVATE KEY----- let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
MIIEpAIBAAKCAQEAz+FHbM8BwkBBz/Ux5OYLQ5Bp1HVuCHTP6Rr3HXTnome/2cGl dkim_sign_fixed_time(
/ze0tsmmFbCjjsS89MXbMGs9xJhjv18LmL1N0UTllblOizzVjorQyN4RwBOfG34j &mut message,
7SS56pwzrA738Ry8FAbL5InPWEgVzbOhXuTCs8yuzcqTnm4sH/csnIl7cMWeQkVn &DkimConfig::new(
1FR9LKMtUG0fjhDPkdX0jx3qTX1L3Z7a7gX6geY191yNd9i9DvE2/+wMigMYz1LA "dkimtest".to_owned(),
ts4alk2g86MQhtbjc8AOR7EC15hSw37/lmamlunYLa3wC+PzHNMA8sAfnmkgNvip "example.org".to_owned(),
ssjh8LnelD9qn+VtsjQB5ppkeQx3TcUPvz5z+QIDAQABAoIBAQCzRa5ZEbSMlumq signing_key,
s+PRaOox3CrIRHUd6c8bUlvmFVllX1++JRhInvvD3ubSMcD7cIMb/D1o5jMgheMP vec![
uKHBmQ+w91+e3W30+gOZp/EiKRDZupIuHXxSGKgUwZx2N3pvfr5b7viLIKWllpTn HeaderName::new_from_ascii_str("Date"),
DpCNy251rIDbjGX97Tk0X+8jGBVSTCxtruGJR5a+hz4t9Z7bz7JjZWcRNJC+VA+Q HeaderName::new_from_ascii_str("From"),
ATjnV7AHO1WR+0tAdPJaHsRLI7drKFSqTYq0As+MksZ40p7T6blZW8NUXA09fJRn HeaderName::new_from_ascii_str("Subject"),
3mP2TZdWjjfBXZje026v4T7TZl+TELKw5WirL/UJ8Zw8dGGV6EZvbfMacZuUB1YQ HeaderName::new_from_ascii_str("To"),
0vZnGe4BAoGBAO63xWP3OV8oLAMF90umuusPaQNSc6DnpjnP+sTAcXEYJA0Sa4YD ],
y8dpTAdFJ4YvUQhLxtbZFK5Ih3x7ZhuerLSJiZiDPC2IJJb7j/812zQQriOi4mQ8 DkimCanonicalization {
bimxM4Nzql8FKGaXMppE5grFLsy8tw7neIM9KE4uwe9ajwJrRrOTUY8ZAoGBAN7t header: DkimCanonicalizationType::Simple,
+xFeuhg4F9expyaPpCvKT2YNAdMcDzpm7GtLX292u+DQgBfg50Ur9XmbS+RPlx1W body: DkimCanonicalizationType::Simple,
r2Sw3bTjRjJU9QnSZLL2w3hiii/wdaePI4SCaydHdLi4ZGz/pNUsUY+ck2pLptS0 },
F7rL+s9MV9lUyhvX+pIh+O3idMWAdaymzs7ZlgfhAoGAVoFn2Wrscmw3Tr0puVNp ),
JudFsbt+RU/Mr+SLRiNKuKX74nTLXBwiC1hAAd5wjTK2VaBIJPEzilikKFr7TIT6 std::time::UNIX_EPOCH,
ps20e/0KoKFWSRROQTh9/+cPg8Bx88rmTNt3BGq00Ywn8M1XvAm9pyd/Zxf36kG9 );
LSnLYlGVW6xgaIsBau+2vXkCgYAeChVdxtTutIhJ8U9ju9FUcUN3reMEDnDi3sGW let signed = message.formatted();
x6ZJf8dbSN0p2o1vXbgLNejpD+x98JNbzxVg7Ysk9xu5whb9opC+ZRDX2uAPvxL7 let signed = std::str::from_utf8(&signed).unwrap();
JRPJTDCnP3mQ0nXkn78xydh3Z1BIsyfLbPcT/eaMi4dcbyL9lARWEcDIaEHzDNsr assert_eq!(
NlioIQKBgQCXIZp5IBfG5WSXzFk8xvP4BUwHKEI5bttClBmm32K+vaSz8qO6ak6G signed,
4frg+WVopFg3HBHdK9aotzPEd0eHMXJv3C06Ynt2lvF+Rgi/kwGbkuq/mFVnmYYR std::concat!(
Fz0TZ6sKrTAF3fdkN3bcQv6JG1CfnWENDGtekemwcCEA9v46/RsOfg== "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
-----END RSA PRIVATE KEY-----"; "To: Test2 <test2@example.org>\r\n",
let signing_key = DkimSigningKey::new(key.to_string(), DkimSigningAlgorithm::Rsa).unwrap(); "Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
message.sign(&DkimConfig::default_config( "Test: test test very very long with spaces and extra spaces \twill be\r\n",
"dkimtest".to_string(), " folded to several lines \r\n",
"example.org".to_string(), "Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
signing_key, "Content-Transfer-Encoding: 7bit\r\n",
)); "DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
println!("{}", std::str::from_utf8(&message.formatted()).unwrap()); " c=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To;\r\n",
let mut verify_command = Command::new("dkimverify") " bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=;\r\n",
.stdin(Stdio::piped()) " b=NhoIMMAALoSgu5lKAR0+MUQunOWnU7wpF9ORUFtpxq9sGZDo9AX43AMhFemyM5W204jpFwMU6pm7AMR1nOYBdSYye4yUALtvT2nqbJBwSh7JeYu+z22t1RFKp7qQR1il8aSrkbZuNMFHYuSEwW76QtKwcNqP4bQOzS9CzgQp0ABu8qwYPBr/EypykPTfqjtyN+ywrfdqjjGOzTpRGolH0hc3CrAETNjjHbNBgKgucXmXTN7hMRdzqWjeFPxizXwouwNAavFClPG0l33gXVArFWn+CkgA84G/s4zuJiF7QPZR87Pu4pw/vIlSXxH4a42W3tT19v9iBTH7X7ldYegtmQ==\r\n",
.spawn() "\r\n",
.expect("Fail to verify message signature"); "test\r\n",
let mut stdin = verify_command.stdin.take().expect("Failed to open stdin"); "\r\n",
std::thread::spawn(move || { "test \ttest\r\n",
stdin "\r\n",
.write_all(&message.formatted()) "\r\n",
.expect("Failed to write to stdin"); )
}); );
assert!(verify_command }
.wait()
.expect("Command did not run") #[test]
.success()); fn test_signature_rsa_relaxed() {
let mut message = test_message();
let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
dkim_sign_fixed_time(
&mut message,
&DkimConfig::new(
"dkimtest".to_owned(),
"example.org".to_owned(),
signing_key,
vec![
HeaderName::new_from_ascii_str("Date"),
HeaderName::new_from_ascii_str("From"),
HeaderName::new_from_ascii_str("Subject"),
HeaderName::new_from_ascii_str("To"),
],
DkimCanonicalization {
header: DkimCanonicalizationType::Relaxed,
body: DkimCanonicalizationType::Relaxed,
},
),
std::time::UNIX_EPOCH,
);
let signed = message.formatted();
let signed = std::str::from_utf8(&signed).unwrap();
println!("{signed}");
assert_eq!(
signed,
std::concat!(
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
"To: Test2 <test2@example.org>\r\n",
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
" folded to several lines \r\n","Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
" c=relaxed/relaxed; q=dns/txt; t=0; h=date:from:subject:to;\r\n",
" bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=;\r\n",
" b=YaVfmH8dbGEywoLJ4uhbvYqDyQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FEh6j3RQthqNbQblcfH/U8mazTuRbVjYBbTZQDaQCMPTz+8D+ZQfXo2oq6dGzTuGvmuYft0CVsq/BIp/EkhZHqiphDeVJSHD4iKW8+L2XwEWThoY92xOYc1G0TtBwz2UJgtiHX2YulH/kRBHeK3dKn9RTNVL3VZ+9ZrnFwIhET9TPGtU2I+q0EMSWF9H9bTrASMgW/U+E0VM2btqJlrTU6rQ7wlQeHdwecLnzXcyhCUInF1+veMNw==\r\n",
"\r\n",
"test\r\n",
"\r\n",
"test \ttest\r\n",
"\r\n",
"\r\n",
)
);
} }
} }

View File

@@ -13,12 +13,14 @@ use crate::BoxError;
/// use-caches this header shouldn't be set manually. /// use-caches this header shouldn't be set manually.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default)]
pub enum ContentTransferEncoding { pub enum ContentTransferEncoding {
/// ASCII /// ASCII
SevenBit, SevenBit,
/// Quoted-Printable encoding /// Quoted-Printable encoding
QuotedPrintable, QuotedPrintable,
/// base64 encoding /// base64 encoding
#[default]
Base64, Base64,
/// Requires `8BITMIME` /// Requires `8BITMIME`
EightBit, EightBit,
@@ -67,14 +69,10 @@ impl FromStr for ContentTransferEncoding {
} }
} }
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::Base64
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::ContentTransferEncoding; use super::ContentTransferEncoding;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -97,7 +95,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"7bit".to_string(), "7bit".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -107,7 +105,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_string(), "base64".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -16,13 +16,13 @@ impl ContentDisposition {
pub fn inline() -> Self { pub fn inline() -> Self {
Self(HeaderValue::dangerous_new_pre_encoded( Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(), Self::name(),
"inline".to_string(), "inline".to_owned(),
"inline".to_string(), "inline".to_owned(),
)) ))
} }
/// An attachment which should be displayed inline into the message, but that also /// An attachment which should be displayed inline into the message, but that also
/// species the filename in case it were to be downloaded /// species the filename in case it is downloaded
pub fn inline_with_name(file_name: &str) -> Self { pub fn inline_with_name(file_name: &str) -> Self {
Self::with_name("inline", file_name) Self::with_name("inline", file_name)
} }
@@ -33,17 +33,19 @@ impl ContentDisposition {
} }
fn with_name(kind: &str, file_name: &str) -> Self { fn with_name(kind: &str, file_name: &str) -> Self {
let raw_value = format!("{}; filename=\"{}\"", kind, file_name); let raw_value = format!("{kind}; filename=\"{file_name}\"");
let mut encoded_value = String::new(); let mut encoded_value = String::new();
let line_len = "Content-Disposition: ".len(); let line_len = "Content-Disposition: ".len();
let mut w = EmailWriter::new(&mut encoded_value, line_len, false); {
w.write_str(kind).expect("writing `kind` returned an error"); let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
w.write_char(';').expect("writing `;` returned an error"); w.write_str(kind).expect("writing `kind` returned an error");
w.space(); w.write_char(';').expect("writing `;` returned an error");
w.optional_breakpoint();
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w) email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
.expect("some Write implementation returned an error"); .expect("some Write implementation returned an error");
}
Self(HeaderValue::dangerous_new_pre_encoded( Self(HeaderValue::dangerous_new_pre_encoded(
Self::name(), Self::name(),
@@ -77,6 +79,8 @@ impl Header for ContentDisposition {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::ContentDisposition; use super::ContentDisposition;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -86,12 +90,12 @@ mod test {
headers.set(ContentDisposition::inline()); headers.set(ContentDisposition::inline());
assert_eq!(format!("{}", headers), "Content-Disposition: inline\r\n"); assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");
headers.set(ContentDisposition::attachment("something.txt")); headers.set(ContentDisposition::attachment("something.txt"));
assert_eq!( assert_eq!(
format!("{}", headers), format!("{headers}"),
"Content-Disposition: attachment; filename=\"something.txt\"\r\n" "Content-Disposition: attachment; filename=\"something.txt\"\r\n"
); );
} }
@@ -102,7 +106,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"), HeaderName::new_from_ascii_str("Content-Disposition"),
"inline".to_string(), "inline".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -112,7 +116,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Disposition"), HeaderName::new_from_ascii_str("Content-Disposition"),
"attachment; filename=\"something.txt\"".to_string(), "attachment; filename=\"something.txt\"".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -11,12 +11,12 @@ use crate::BoxError;
/// `Content-Type` of the body /// `Content-Type` of the body
/// ///
/// This struct can represent any valid [mime type], which can be parsed via /// This struct can represent any valid [MIME type], which can be parsed via
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types. /// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
/// ///
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5) /// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
/// ///
/// [mime type]: https://www.iana.org/assignments/media-types/media-types.xhtml /// [MIME type]: https://www.iana.org/assignments/media-types/media-types.xhtml
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentType(Mime); pub struct ContentType(Mime);
@@ -135,8 +135,7 @@ mod serde {
match ContentType::parse(mime) { match ContentType::parse(mime) {
Ok(content_type) => Ok(content_type), Ok(content_type) => Ok(content_type),
Err(_) => Err(E::custom(format!( Err(_) => Err(E::custom(format!(
"Couldn't parse the following MIME-Type: {}", "Couldn't parse the following MIME-Type: {mime}"
mime
))), ))),
} }
} }
@@ -149,6 +148,8 @@ mod serde {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::ContentType; use super::ContentType;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -177,14 +178,14 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"), HeaderName::new_from_ascii_str("Content-Type"),
"text/plain; charset=utf-8".to_string(), "text/plain; charset=utf-8".to_owned(),
)); ));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN)); assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Type"), HeaderName::new_from_ascii_str("Content-Type"),
"text/html; charset=utf-8".to_string(), "text/html; charset=utf-8".to_owned(),
)); ));
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML)); assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));

View File

@@ -8,7 +8,7 @@ use crate::BoxError;
/// Message `Date` header /// Message `Date` header
/// ///
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3) /// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Date(HttpDate); pub struct Date(HttpDate);
impl Date { impl Date {
@@ -74,6 +74,8 @@ impl From<Date> for SystemTime {
mod test { mod test {
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq;
use super::Date; use super::Date;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -88,7 +90,7 @@ mod test {
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_string() "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_owned()
); );
// Tue, 15 Nov 1994 08:12:32 GMT // Tue, 15 Nov 1994 08:12:32 GMT
@@ -108,7 +110,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"), HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:31 +0000".to_string(), "Tue, 15 Nov 1994 08:12:31 +0000".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -120,7 +122,7 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Date"), HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:32 +0000".to_string(), "Tue, 15 Nov 1994 08:12:32 +0000".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -14,7 +14,7 @@ pub trait MailboxesHeader {
macro_rules! mailbox_header { macro_rules! mailbox_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => { ($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])* $(#[$doc])*
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(Mailbox); pub struct $type_name(Mailbox);
impl Header for $type_name { impl Header for $type_name {
@@ -30,8 +30,10 @@ macro_rules! mailbox_header {
fn display(&self) -> HeaderValue { fn display(&self) -> HeaderValue {
let mut encoded_value = String::new(); let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len(); let line_len = $header_name.len() + ": ".len();
let mut w = EmailWriter::new(&mut encoded_value, line_len, false); {
self.0.encode(&mut w).expect("writing `Mailbox` returned an error"); let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
}
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value) HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
} }
@@ -56,7 +58,7 @@ macro_rules! mailbox_header {
macro_rules! mailboxes_header { macro_rules! mailboxes_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => { ($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])* $(#[$doc])*
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(pub(crate) Mailboxes); pub struct $type_name(pub(crate) Mailboxes);
impl MailboxesHeader for $type_name { impl MailboxesHeader for $type_name {
@@ -78,8 +80,10 @@ macro_rules! mailboxes_header {
fn display(&self) -> HeaderValue { fn display(&self) -> HeaderValue {
let mut encoded_value = String::new(); let mut encoded_value = String::new();
let line_len = $header_name.len() + ": ".len(); let line_len = $header_name.len() + ": ".len();
let mut w = EmailWriter::new(&mut encoded_value, line_len, false); {
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error"); let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
}
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value) HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
} }
@@ -172,6 +176,8 @@ mailboxes_header! {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::{From, Mailbox, Mailboxes}; use super::{From, Mailbox, Mailboxes};
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -246,7 +252,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"kayo@example.com".to_string(), "kayo@example.com".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from))); assert_eq!(headers.get::<From>(), Some(From(from)));
@@ -259,7 +265,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_string(), "K. <kayo@example.com>".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from))); assert_eq!(headers.get::<From>(), Some(From(from)));
@@ -275,7 +281,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"kayo@example.com, pony@domain.tld".to_string(), "kayo@example.com, pony@domain.tld".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -291,7 +297,7 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(), "K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -300,14 +306,30 @@ mod test {
#[test] #[test]
fn parse_multi_with_name_containing_comma() { fn parse_multi_with_name_containing_comma() {
let from: Vec<Mailbox> = vec![ let from: Vec<Mailbox> = vec![
"Test, test <1@example.com>".parse().unwrap(), "\"Test, test\" <1@example.com>".parse().unwrap(),
"Test2, test2 <2@example.com>".parse().unwrap(), "\"Test2, test2\" <2@example.com>".parse().unwrap(),
]; ];
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2 <2@example.com>".to_string(), "\"Test, test\" <1@example.com>, \"Test2, test2\" <2@example.com>".to_owned(),
));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
fn parse_multi_with_name_containing_double_quotes() {
let from: Vec<Mailbox> = vec![
"\"Test, test\" <1@example.com>".parse().unwrap(),
"\"Test2, \"test2\"\" <2@example.com>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"),
"\"Test, test\" <1@example.com>, \"Test2, \"test2\"\" <2@example.com>".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), Some(From(from.into()))); assert_eq!(headers.get::<From>(), Some(From(from.into())));
@@ -318,9 +340,20 @@ mod test {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"Test, test <1@example.com>, Test2, test2".to_string(), "\"Test, test\" <1@example.com>, \"Test2, test2\"".to_owned(),
)); ));
assert_eq!(headers.get::<From>(), None); assert_eq!(headers.get::<From>(), None);
} }
#[test]
fn mailbox_format_address_with_angle_bracket() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("<3".into()), "i@love.example".parse().unwrap())
),
r#""<3" <i@love.example>"#
);
}
} }

View File

@@ -3,10 +3,12 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
error::Error, error::Error,
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter, Write},
ops::Deref, ops::Deref,
}; };
use email_encoding::headers::EmailWriter;
pub use self::{ pub use self::{
content::*, content::*,
content_disposition::ContentDisposition, content_disposition::ContentDisposition,
@@ -64,7 +66,7 @@ impl Headers {
} }
} }
/// Returns a copy of an `Header` present in `Headers` /// Returns a copy of a `Header` present in `Headers`
/// ///
/// Returns `None` if `Header` isn't present in `Headers`. /// Returns `None` if `Header` isn't present in `Headers`.
pub fn get<H: Header>(&self) -> Option<H> { pub fn get<H: Header>(&self) -> Option<H> {
@@ -121,7 +123,7 @@ impl Headers {
self.find_header_index(name).map(|i| self.headers.remove(i)) self.find_header_index(name).map(|i| self.headers.remove(i))
} }
fn find_header(&self, name: &str) -> Option<&HeaderValue> { pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
self.headers self.headers
.iter() .iter()
.find(|value| name.eq_ignore_ascii_case(&value.name)) .find(|value| name.eq_ignore_ascii_case(&value.name))
@@ -275,6 +277,7 @@ impl PartialEq<HeaderName> for &str {
} }
} }
/// A safe for use header value
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct HeaderValue { pub struct HeaderValue {
name: HeaderName, name: HeaderName,
@@ -283,6 +286,12 @@ pub struct HeaderValue {
} }
impl HeaderValue { impl HeaderValue {
/// Construct a new `HeaderValue` and encode it
///
/// Takes the header `name` and the `raw_value` and encodes
/// it via `RFC2047` and line folds it.
///
/// [`RFC2047`]: https://datatracker.ietf.org/doc/html/rfc2047
pub fn new(name: HeaderName, raw_value: String) -> Self { pub fn new(name: HeaderName, raw_value: String) -> Self {
let mut encoded_value = String::with_capacity(raw_value.len()); let mut encoded_value = String::with_capacity(raw_value.len());
HeaderValueEncoder::encode(&name, &raw_value, &mut encoded_value).unwrap(); HeaderValueEncoder::encode(&name, &raw_value, &mut encoded_value).unwrap();
@@ -294,6 +303,14 @@ impl HeaderValue {
} }
} }
/// Construct a new `HeaderValue` using a pre-encoded header value
///
/// This method is _extremely_ dangerous as it opens up
/// the encoder to header injection attacks, but is sometimes
/// acceptable for use if `encoded_value` contains only ascii
/// printable characters and is already line folded.
///
/// When in doubt, use [`HeaderValue::new`].
pub fn dangerous_new_pre_encoded( pub fn dangerous_new_pre_encoded(
name: HeaderName, name: HeaderName,
raw_value: String, raw_value: String,
@@ -305,57 +322,41 @@ impl HeaderValue {
encoded_value, encoded_value,
} }
} }
#[cfg(feature = "dkim")]
pub(crate) fn get_raw(&self) -> &str {
&self.raw_value
}
#[cfg(feature = "dkim")]
pub(crate) fn get_encoded(&self) -> &str {
&self.encoded_value
}
} }
const ENCODING_START_PREFIX: &str = "=?utf-8?b?";
const ENCODING_END_SUFFIX: &str = "?=";
const MAX_LINE_LEN: usize = 76;
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder /// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
struct HeaderValueEncoder { struct HeaderValueEncoder<'a> {
line_len: usize, writer: EmailWriter<'a>,
encode_buf: String, encode_buf: String,
} }
impl HeaderValueEncoder { impl<'a> HeaderValueEncoder<'a> {
fn encode(name: &str, value: &str, f: &mut impl fmt::Write) -> fmt::Result { fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
let (words_iter, encoder) = Self::new(name, value); let encoder = Self::new(name, f);
encoder.format(words_iter, f) encoder.format(value.split_inclusive(' '))
} }
fn new<'a>(name: &str, value: &'a str) -> (WordsPlusFillIterator<'a>, Self) { fn new(name: &str, writer: &'a mut dyn Write) -> Self {
( let line_len = name.len() + ": ".len();
WordsPlusFillIterator { s: value }, let writer = EmailWriter::new(writer, line_len, 0, false, false);
Self {
line_len: name.len() + ": ".len(), Self {
encode_buf: String::new(), writer,
}, encode_buf: String::new(),
) }
} }
fn format( fn format(mut self, words_iter: impl Iterator<Item = &'a str>) -> fmt::Result {
mut self,
words_iter: WordsPlusFillIterator<'_>,
f: &mut impl fmt::Write,
) -> fmt::Result {
/// Estimate if an encoded string of `len` would fix in an empty line
fn would_fit_new_line(len: usize) -> bool {
len < (MAX_LINE_LEN - " ".len())
}
/// Estimate how long a string of `len` would be after base64 encoding plus
/// adding the encoding prefix and suffix to it
fn base64_len(len: usize) -> usize {
ENCODING_START_PREFIX.len() + (len * 4 / 3 + 4) + ENCODING_END_SUFFIX.len()
}
/// Estimate how many more bytes we can fit in the current line
fn available_len_to_max_encode_len(len: usize) -> usize {
len.saturating_sub(
ENCODING_START_PREFIX.len() + (len * 3 / 4 + 4) + ENCODING_END_SUFFIX.len(),
)
}
for next_word in words_iter { for next_word in words_iter {
let allowed = allowed_str(next_word); let allowed = allowed_str(next_word);
@@ -363,205 +364,54 @@ impl HeaderValueEncoder {
// This word only contains allowed characters // This word only contains allowed characters
// the next word is allowed, but we may have accumulated some words to encode // the next word is allowed, but we may have accumulated some words to encode
self.flush_encode_buf(f, true)?; self.flush_encode_buf()?;
if next_word.len() > self.remaining_line_len() { self.writer.folding().write_str(next_word)?;
// not enough space left on this line to encode word
if self.something_written_to_this_line() && would_fit_new_line(next_word.len())
{
// word doesn't fit this line, but something had already been written to it,
// and word would fit the next line, so go to a new line
// so go to new line
self.new_line(f)?;
} else {
// word neither fits this line and the next one, cut it
// in the middle and make it fit
let mut next_word = next_word;
while !next_word.is_empty() {
if self.remaining_line_len() == 0 {
self.new_line(f)?;
}
let len = self.remaining_line_len().min(next_word.len());
let first_part = &next_word[..len];
next_word = &next_word[len..];
f.write_str(first_part)?;
self.line_len += first_part.len();
}
continue;
}
}
// word fits, write it!
f.write_str(next_word)?;
self.line_len += next_word.len();
} else { } else {
// This word contains unallowed characters // This word contains unallowed characters
self.encode_buf.push_str(next_word);
if self.remaining_line_len() >= base64_len(self.encode_buf.len() + next_word.len())
{
// next_word fits
self.encode_buf.push_str(next_word);
continue;
}
// next_word doesn't fit this line
if would_fit_new_line(base64_len(next_word.len())) {
// ...but it would fit the next one
self.flush_encode_buf(f, false)?;
self.new_line(f)?;
self.encode_buf.push_str(next_word);
continue;
}
// ...and also wouldn't fit the next one.
// chop it up into pieces
let mut next_word = next_word;
while !next_word.is_empty() {
let mut len = available_len_to_max_encode_len(self.remaining_line_len())
.min(next_word.len());
if len == 0 {
self.flush_encode_buf(f, false)?;
self.new_line(f)?;
}
// avoid slicing on a char boundary
while !next_word.is_char_boundary(len) {
len += 1;
}
let first_part = &next_word[..len];
next_word = &next_word[len..];
self.encode_buf.push_str(first_part);
}
} }
} }
self.flush_encode_buf(f, false)?; self.flush_encode_buf()?;
Ok(()) Ok(())
} }
/// Returns the number of bytes left for the current line fn flush_encode_buf(&mut self) -> fmt::Result {
fn remaining_line_len(&self) -> usize {
MAX_LINE_LEN - self.line_len
}
/// Returns true if something has been written to the current line
fn something_written_to_this_line(&self) -> bool {
self.line_len > 1
}
fn flush_encode_buf(
&mut self,
f: &mut impl fmt::Write,
switching_to_allowed: bool,
) -> fmt::Result {
if self.encode_buf.is_empty() { if self.encode_buf.is_empty() {
// nothing to encode // nothing to encode
return Ok(()); return Ok(());
} }
let mut write_after = None; let prefix = self.encode_buf.trim_end_matches(' ');
email_encoding::headers::rfc2047::encode(prefix, &mut self.writer)?;
if switching_to_allowed { // TODO: add a better API for doing this in email-encoding
// If the next word only contains allowed characters, and the string to encode let spaces = self.encode_buf.len() - prefix.len();
// ends with a space, take the space out of the part to encode for _ in 0..spaces {
self.writer.space();
let last_char = self.encode_buf.pop().expect("self.encode_buf isn't empty");
if is_space_like(last_char) {
write_after = Some(last_char);
} else {
self.encode_buf.push(last_char);
}
}
f.write_str(ENCODING_START_PREFIX)?;
let encoded = base64::display::Base64Display::with_config(
self.encode_buf.as_bytes(),
base64::STANDARD,
);
write!(f, "{}", encoded)?;
f.write_str(ENCODING_END_SUFFIX)?;
self.line_len += ENCODING_START_PREFIX.len();
self.line_len += self.encode_buf.len() * 4 / 3 + 4;
self.line_len += ENCODING_END_SUFFIX.len();
if let Some(write_after) = write_after {
f.write_char(write_after)?;
self.line_len += 1;
} }
self.encode_buf.clear(); self.encode_buf.clear();
Ok(()) Ok(())
} }
fn new_line(&mut self, f: &mut impl fmt::Write) -> fmt::Result {
f.write_str("\r\n ")?;
self.line_len = 1;
Ok(())
}
}
/// Iterator yielding a string split space by space, but including all space
/// characters between it and the next word
struct WordsPlusFillIterator<'a> {
s: &'a str,
}
impl<'a> Iterator for WordsPlusFillIterator<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.s.is_empty() {
return None;
}
let next_word = self
.s
.char_indices()
.skip(1)
.skip_while(|&(_i, c)| !is_space_like(c))
.find(|&(_i, c)| !is_space_like(c))
.map(|(i, _)| i);
let word = &self.s[..next_word.unwrap_or(self.s.len())];
self.s = &self.s[word.len()..];
Some(word)
}
}
const fn is_space_like(c: char) -> bool {
c == ',' || c == ' '
} }
fn allowed_str(s: &str) -> bool { fn allowed_str(s: &str) -> bool {
s.chars().all(allowed_char) s.bytes().all(allowed_char)
} }
const fn allowed_char(c: char) -> bool { const fn allowed_char(c: u8) -> bool {
c >= 1 as char && c <= 9 as char c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
|| c == 11 as char
|| c == 12 as char
|| c >= 14 as char && c <= 127 as char
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{HeaderName, HeaderValue, Headers}; use pretty_assertions::assert_eq;
use super::{HeaderName, HeaderValue, Headers, To};
use crate::message::Mailboxes;
#[test] #[test]
fn valid_headername() { fn valid_headername() {
@@ -624,7 +474,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_string(), "John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -638,14 +488,14 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_string(), "Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_owned(),
)); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith \r\n", "To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith\r\n",
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand \r\n", " <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand\r\n",
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n" " <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
) )
); );
@@ -656,14 +506,14 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string() "Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
)); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"Subject: Hello! This is lettre, and this \r\n ", "Subject: Hello! This is lettre, and this\r\n",
"IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n", " IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
" guess that's it!\r\n" " guess that's it!\r\n"
) )
); );
@@ -675,14 +525,15 @@ mod tests {
headers.insert_raw( headers.insert_raw(
HeaderValue::new( HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string() "Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
)); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n", "Subject: Hello!\r\n",
" ouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know\r\n", " IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n",
" I don't know\r\n",
) )
); );
} }
@@ -692,16 +543,12 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string() "1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
)); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( "Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n",
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n",
" mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n",
" ghijklmnopqrstuvwxyz\r\n",
)
); );
} }
@@ -710,7 +557,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"Seán <sean@example.com>".to_string(), "Seán <sean@example.com>".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -724,7 +571,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"🌎 <world@example.com>".to_string(), "🌎 <world@example.com>".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -736,19 +583,46 @@ mod tests {
#[test] #[test]
fn format_special_with_folding() { fn format_special_with_folding() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( let to = To::from(Mailboxes::from_iter([
HeaderName::new_from_ascii_str("To"), "🌍 <world@example.com>".parse().unwrap(),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(), "🦆 Everywhere <ducks@example.com>".parse().unwrap(),
) ); "Иванов Иван Иванович <ivanov@example.com>".parse().unwrap(),
"Jānis Bērziņš <janis@example.com>".parse().unwrap(),
"Seán Ó Rudaí <sean@example.com>".parse().unwrap(),
]));
headers.set(to);
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n", "To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhiBFdmVyeXdo?=\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n", " =?utf-8?b?ZXJl?= <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LI=?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n", " =?utf-8?b?0LDQvSDQmNCy0LDQvdC+0LLQuNGH?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n", " =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n" " =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
)
);
}
#[test]
fn format_special_with_folding_raw() {
let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("To"),
"🌍 <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",
) )
); );
} }
@@ -759,12 +633,19 @@ mod tests {
headers.insert_raw( headers.insert_raw(
HeaderValue::new( HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(),) "🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
); );
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n" concat!(
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+lsw==?=\r\n"
)
); );
} }
@@ -773,7 +654,7 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! \r\n This is \" bad \0. 👋".to_string(), "Hello! \r\n This is \" bad \0. 👋".to_owned(),
)); ));
assert_eq!( assert_eq!(
@@ -788,35 +669,38 @@ mod tests {
headers.insert_raw( headers.insert_raw(
HeaderValue::new( HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string() "Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
) )
); );
headers.insert_raw( headers.insert_raw(
HeaderValue::new( HeaderValue::new(
HeaderName::new_from_ascii_str("To"), HeaderName::new_from_ascii_str("To"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(), "🌍 <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(),
) )
); );
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("From"), HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".to_string(), "Someone <somewhere@example.com>".to_owned(),
)); ));
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"), HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"quoted-printable".to_string(), "quoted-printable".to_owned(),
)); ));
// TODO: fix the fact that the encoder doesn't know that
// the space between the name and the address should be
// removed when wrapping.
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"Subject: Hello! This is lettre, and this \r\n", "Subject: Hello! This is lettre, and this\r\n",
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n", " IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
" guess that's it!\r\n", " guess that's it!\r\n",
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n", "To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n", " Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n", " =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n", " =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n", " =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n", "From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n", "Content-Transfer-Encoding: quoted-printable\r\n",
) )
@@ -828,14 +712,14 @@ mod tests {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_string(), "+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_owned(),
)); ));
assert_eq!( assert_eq!(
headers.to_string(), headers.to_string(),
concat!( concat!(
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n", "Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7?=\r\n",
" =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n" " =?utf-8?b?Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n",
) )
); );
} }

View File

@@ -4,7 +4,7 @@ use crate::{
}; };
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4) /// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct MimeVersion { pub struct MimeVersion {
major: u8, major: u8,
minor: u8, minor: u8,
@@ -16,15 +16,18 @@ pub struct MimeVersion {
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0); pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
impl MimeVersion { impl MimeVersion {
/// Build a new `MimeVersion` header
pub const fn new(major: u8, minor: u8) -> Self { pub const fn new(major: u8, minor: u8) -> Self {
MimeVersion { major, minor } MimeVersion { major, minor }
} }
/// Get the `major` value of this `MimeVersion` header.
#[inline] #[inline]
pub const fn major(self) -> u8 { pub const fn major(self) -> u8 {
self.major self.major
} }
/// Get the `minor` value of this `MimeVersion` header.
#[inline] #[inline]
pub const fn minor(self) -> u8 { pub const fn minor(self) -> u8 {
self.minor self.minor
@@ -64,6 +67,8 @@ impl Default for MimeVersion {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::{MimeVersion, MIME_VERSION_1_0}; use super::{MimeVersion, MIME_VERSION_1_0};
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -86,14 +91,14 @@ mod test {
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"), HeaderName::new_from_ascii_str("MIME-Version"),
"1.0".to_string(), "1.0".to_owned(),
)); ));
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0)); assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("MIME-Version"), HeaderName::new_from_ascii_str("MIME-Version"),
"0.1".to_string(), "0.1".to_owned(),
)); ));
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1))); assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));

View File

@@ -4,7 +4,7 @@ use crate::BoxError;
macro_rules! text_header { macro_rules! text_header {
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => { ($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
$(#[$attr])* $(#[$attr])*
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct $type_name(String); pub struct $type_name(String);
impl Header for $type_name { impl Header for $type_name {
@@ -85,6 +85,8 @@ text_header! {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::Subject; use super::Subject;
use crate::message::header::{HeaderName, HeaderValue, Headers}; use crate::message::header::{HeaderName, HeaderValue, Headers};
@@ -107,12 +109,23 @@ mod test {
); );
} }
#[test]
fn format_utf8_word() {
let mut headers = Headers::new();
headers.set(Subject("Administratör".into()));
assert_eq!(
headers.to_string(),
"Subject: =?utf-8?b?QWRtaW5pc3RyYXTDtnI=?=\r\n"
);
}
#[test] #[test]
fn parse_ascii() { fn parse_ascii() {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.insert_raw(HeaderValue::new( headers.insert_raw(HeaderValue::new(
HeaderName::new_from_ascii_str("Subject"), HeaderName::new_from_ascii_str("Subject"),
"Sample subject".to_string(), "Sample subject".to_owned(),
)); ));
assert_eq!( assert_eq!(

View File

@@ -1,3 +1,4 @@
mod parsers;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
mod serde; mod serde;
mod types; 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,248 @@
//! 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)))).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

@@ -13,7 +13,7 @@ impl Serialize for Mailbox {
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(&self.to_string()) serializer.collect_str(self)
} }
} }
@@ -111,7 +111,7 @@ impl Serialize for Mailboxes {
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(&self.to_string()) serializer.collect_str(self)
} }
} }
@@ -154,6 +154,7 @@ impl<'de> Deserialize<'de> for Mailboxes {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use serde_json::from_str; use serde_json::from_str;
use super::*; use super::*;
@@ -178,7 +179,7 @@ mod test {
} }
#[test] #[test]
fn parse_mailbox_object_address_stirng() { fn parse_mailbox_object_address_string() {
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap(); let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap()); assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
} }
@@ -197,7 +198,7 @@ mod test {
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap(); from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
assert_eq!( assert_eq!(
m, m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>" "yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse() .parse()
.unwrap() .unwrap()
); );
@@ -210,7 +211,7 @@ mod test {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
m, m,
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>" "yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
.parse() .parse()
.unwrap() .unwrap()
); );

View File

@@ -5,15 +5,17 @@ use std::{
str::FromStr, str::FromStr,
}; };
use chumsky::prelude::*;
use email_encoding::headers::EmailWriter; use email_encoding::headers::EmailWriter;
use super::parsers;
use crate::address::{Address, AddressError}; use crate::address::{Address, AddressError};
/// Represents an email address with an optional name for the sender/recipient. /// Represents an email address with an optional name for the sender/recipient.
/// ///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_). /// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
/// ///
/// # Examples /// # Examples
/// ///
@@ -70,7 +72,7 @@ impl Mailbox {
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult { pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
if let Some(name) = &self.name { if let Some(name) = &self.name {
email_encoding::headers::quoted_string::encode(name, w)?; email_encoding::headers::quoted_string::encode(name, w)?;
w.space(); w.optional_breakpoint();
w.write_char('<')?; w.write_char('<')?;
} }
@@ -108,40 +110,24 @@ impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
} }
} }
/*
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
type Error = AddressError;
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
let (name, address) = header;
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
}
}*/
impl FromStr for Mailbox { impl FromStr for Mailbox {
type Err = AddressError; type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> { fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
match (src.find('<'), src.find('>')) { let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => { // TODO: improve error management
let name = src.split_at(addr_open).0; AddressError::InvalidInput
let addr_open = addr_open + 1; })?;
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
let addr = addr.parse()?; let mailbox = Mailbox::new(name, Address::new(user, domain)?);
let name = name.trim();
let name = if name.is_empty() { Ok(mailbox)
None }
} else { }
Some(name.into())
}; impl From<Address> for Mailbox {
Ok(Mailbox::new(name, addr)) fn from(value: Address) -> Self {
} Self::new(None, value)
(Some(_), _) => Err(AddressError::Unbalanced),
_ => {
let addr = src.parse()?;
Ok(Mailbox::new(None, addr))
}
}
} }
} }
@@ -149,7 +135,7 @@ impl FromStr for Mailbox {
/// ///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._). /// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
/// ///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/). /// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailboxes(Vec<Mailbox>); pub struct Mailboxes(Vec<Mailbox>);
@@ -275,7 +261,7 @@ impl Mailboxes {
for mailbox in self.iter() { for mailbox in self.iter() {
if !mem::take(&mut first) { if !mem::take(&mut first) {
w.write_char(',')?; w.write_char(',')?;
w.space(); w.optional_breakpoint();
} }
mailbox.encode(w)?; mailbox.encode(w)?;
@@ -315,6 +301,18 @@ impl From<Mailboxes> for Vec<Mailbox> {
} }
} }
impl FromIterator<Mailbox> for Mailboxes {
fn from_iter<T: IntoIterator<Item = Mailbox>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
self.0.extend(iter);
}
}
impl IntoIterator for Mailboxes { impl IntoIterator for Mailboxes {
type Item = Mailbox; type Item = Mailbox;
type IntoIter = ::std::vec::IntoIter<Mailbox>; type IntoIter = ::std::vec::IntoIter<Mailbox>;
@@ -324,14 +322,6 @@ impl IntoIterator for Mailboxes {
} }
} }
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
for elem in iter {
self.0.push(elem);
}
}
}
impl Display for Mailboxes { impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut iter = self.iter(); let mut iter = self.iter();
@@ -352,34 +342,16 @@ impl Display for Mailboxes {
impl FromStr for Mailboxes { impl FromStr for Mailboxes {
type Err = AddressError; type Err = AddressError;
fn from_str(mut src: &str) -> Result<Self, Self::Err> { fn from_str(src: &str) -> Result<Self, Self::Err> {
let mut mailboxes = Vec::new(); let mut mailboxes = Vec::new();
if !src.is_empty() { let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
// n-1 elements // TODO: improve error management
let mut skip = 0; AddressError::InvalidInput
while let Some(i) = src[skip..].find(',') { })?;
let left = &src[..skip + i];
match left.trim().parse() { for (name, (user, domain)) in parsed_mailboxes {
Ok(mailbox) => { mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
mailboxes.push(mailbox);
src = &src[left.len() + ",".len()..];
skip = 0;
}
Err(AddressError::MissingParts) => {
skip = left.len() + ",".len();
}
Err(err) => {
return Err(err);
}
}
}
// last element
let mailbox = src.trim().parse()?;
mailboxes.push(mailbox);
} }
Ok(Mailboxes(mailboxes)) Ok(Mailboxes(mailboxes))
@@ -393,7 +365,7 @@ fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
} else { } else {
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5 // Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
f.write_char('"')?; f.write_char('"')?;
for &c in s.as_bytes() { for c in s.chars() {
write_quoted_string_char(f, c)?; write_quoted_string_char(f, c)?;
} }
f.write_char('"')?; f.write_char('"')?;
@@ -432,45 +404,50 @@ fn is_valid_atom_char(c: u8) -> bool {
b'}' | b'}' |
b'~' | b'~' |
// Not techically allowed but will be escaped into allowed characters. // Not technically allowed but will be escaped into allowed characters.
128..=255) 128..=255)
} }
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5 // https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
fn write_quoted_string_char(f: &mut Formatter<'_>, c: u8) -> FmtResult { fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
match c { match c {
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1 // Can not be encoded.
1..=8 | 11 | 12 | 14..=31 | 127 | '\n' | '\r' => Err(std::fmt::Error),
// Note, not qcontent but can be put before or after any qcontent. // Note, not qcontent but can be put before or after any qcontent.
b'\t' | '\t' | ' ' => f.write_char(c),
b' ' |
// The rest of the US-ASCII except \ and " c if match c as u32 {
33 | // NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
35..=91 | 1..=8 | 11 | 12 | 14..=31 | 127 |
93..=126 |
// Non-ascii characters will be escaped separately later. // The rest of the US-ASCII except \ and "
128..=255 33 |
35..=91 |
93..=126 |
=> f.write_char(c.into()), // Non-ascii characters will be escaped separately later.
128.. => true,
_ => false,
} =>
{
f.write_char(c)
}
// Can not be encoded. _ => {
b'\n' | b'\r' => Err(std::fmt::Error), // quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
f.write_char('\\')?;
c => { f.write_char(c)
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2 }
f.write_char('\\')?; }
f.write_char(c.into())
}
}
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::convert::TryInto; use std::convert::TryInto;
use pretty_assertions::assert_eq;
use super::Mailbox; use super::Mailbox;
#[test] #[test]
@@ -509,6 +486,34 @@ mod test {
); );
} }
#[test]
fn mailbox_format_address_with_comma_and_non_ascii() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Laşt, First".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Laşt, First" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_comma_and_quoted_non_ascii() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some(r#"Laşt, "First""#.into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Laşt, \"First\"" <kayo@example.com>"#
);
}
#[test] #[test]
fn mailbox_format_address_with_color() { fn mailbox_format_address_with_color() {
assert_eq!( assert_eq!(
@@ -583,7 +588,7 @@ mod test {
#[test] #[test]
fn parse_address_from_tuple() { fn parse_address_from_tuple() {
assert_eq!( assert_eq!(
("K.".to_string(), "kayo@example.com".to_string()).try_into(), ("K.".to_owned(), "kayo@example.com".to_owned()).try_into(),
Ok(Mailbox::new( Ok(Mailbox::new(
Some("K.".into()), Some("K.".into()),
"kayo@example.com".parse().unwrap() "kayo@example.com".parse().unwrap()

View File

@@ -100,14 +100,14 @@ impl SinglePart {
SinglePartBuilder::new() SinglePartBuilder::new()
} }
/// Directly create a `SinglePart` from an plain UTF-8 content /// Directly create a `SinglePart` from a plain UTF-8 content
pub fn plain<T: IntoBody>(body: T) -> Self { pub fn plain<T: IntoBody>(body: T) -> Self {
Self::builder() Self::builder()
.header(header::ContentType::TEXT_PLAIN) .header(header::ContentType::TEXT_PLAIN)
.body(body) .body(body)
} }
/// Directly create a `SinglePart` from an UTF-8 HTML content /// Directly create a `SinglePart` from a UTF-8 HTML content
pub fn html<T: IntoBody>(body: T) -> Self { pub fn html<T: IntoBody>(body: T) -> Self {
Self::builder() Self::builder()
.header(header::ContentType::TEXT_HTML) .header(header::ContentType::TEXT_HTML)
@@ -149,17 +149,17 @@ impl EmailFormat for SinglePart {
pub enum MultiPartKind { pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts /// Mixed kind to combine unrelated content parts
/// ///
/// For example this kind can be used to mix email message and attachments. /// For example, this kind can be used to mix an email message and attachments.
Mixed, Mixed,
/// Alternative kind to join several variants of same email contents. /// Alternative kind to join several variants of same email contents.
/// ///
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message. /// That kind is recommended to use for joining plain (text) and rich (HTML) messages into a single email message.
Alternative, Alternative,
/// Related kind to mix content and related resources. /// Related kind to mix content and related resources.
/// ///
/// For example, you can include images into HTML content using that. /// For example, you can include images in HTML content using that.
Related, Related,
/// Encrypted kind for encrypted messages /// Encrypted kind for encrypted messages
@@ -190,9 +190,9 @@ impl MultiPartKind {
}, },
boundary, boundary,
match self { match self {
Self::Encrypted { protocol } => format!("; protocol=\"{}\"", protocol), Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
Self::Signed { protocol, micalg } => Self::Signed { protocol, micalg } =>
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg), format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
_ => String::new(), _ => String::new(),
} }
) )
@@ -398,6 +398,8 @@ impl EmailFormat for MultiPart {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use pretty_assertions::assert_eq;
use super::*; use super::*;
use crate::message::header; use crate::message::header;
@@ -477,7 +479,7 @@ mod test {
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: multipart/mixed; \r\n", "Content-Type: multipart/mixed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
@@ -524,8 +526,8 @@ mod test {
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: multipart/encrypted; \r\n", "Content-Type: multipart/encrypted;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
" protocol=\"application/pgp-encrypted\"\r\n", " protocol=\"application/pgp-encrypted\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
@@ -580,8 +582,8 @@ mod test {
assert_eq!( assert_eq!(
String::from_utf8(part.formatted()).unwrap(), String::from_utf8(part.formatted()).unwrap(),
concat!( concat!(
"Content-Type: multipart/signed; \r\n", "Content-Type: multipart/signed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
" protocol=\"application/pgp-signature\";", " protocol=\"application/pgp-signature\";",
" micalg=\"pgp-sha256\"\r\n", " micalg=\"pgp-sha256\"\r\n",
"\r\n", "\r\n",
@@ -622,7 +624,7 @@ mod test {
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>"))); .body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/alternative; \r\n", concat!("Content-Type: multipart/alternative;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
@@ -660,11 +662,11 @@ mod test {
.body(String::from("int main() { return 0; }"))); .body(String::from("int main() { return 0; }")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(), assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed; \r\n", concat!("Content-Type: multipart/mixed;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: multipart/related; \r\n", "Content-Type: multipart/related;\r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n", "\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",

View File

@@ -14,7 +14,7 @@
//! The easiest way of creating a message, which uses a plain text body. //! The easiest way of creating a message, which uses a plain text body.
//! //!
//! ```rust //! ```rust
//! use lettre::message::Message; //! use lettre::message::{header::ContentType, Message};
//! //!
//! # use std::error::Error; //! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> { //! # fn main() -> Result<(), Box<dyn Error>> {
@@ -23,6 +23,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?) //! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?) //! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year") //! .subject("Happy new year")
//! .header(ContentType::TEXT_PLAIN)
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! # Ok(()) //! # Ok(())
//! # } //! # }
@@ -38,6 +39,7 @@
//! To: Hei <hei@domain.tld> //! To: Hei <hei@domain.tld>
//! Subject: Happy new year //! Subject: Happy new year
//! Date: Sat, 12 Dec 2020 16:33:19 GMT //! Date: Sat, 12 Dec 2020 16:33:19 GMT
//! Content-Type: text/plain; charset=utf-8
//! Content-Transfer-Encoding: 7bit //! Content-Transfer-Encoding: 7bit
//! //!
//! Be happy! //! Be happy!
@@ -232,6 +234,7 @@ trait EmailFormat {
pub struct MessageBuilder { pub struct MessageBuilder {
headers: Headers, headers: Headers,
envelope: Option<Envelope>, envelope: Option<Envelope>,
drop_bcc: bool,
} }
impl MessageBuilder { impl MessageBuilder {
@@ -240,24 +243,26 @@ impl MessageBuilder {
Self { Self {
headers: Headers::new(), headers: Headers::new(),
envelope: None, envelope: None,
drop_bcc: true,
} }
} }
/// Set custom header to message /// Set or add mailbox to `From` header
pub fn header<H: Header>(mut self, header: H) -> Self { ///
self.headers.set(header); /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
self ///
/// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From::from(Mailboxes::from(mbox)))
} }
/// Add mailbox to header /// Set `Sender` header. Should be used when providing several `From` mailboxes.
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self { ///
match self.headers.get::<H>() { /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
Some(mut header_) => { ///
header_.join_mailboxes(header); /// Shortcut for `self.header(header::Sender(mbox))`.
self.header(header_) pub fn sender(self, mbox: Mailbox) -> Self {
} self.header(header::Sender::from(mbox))
None => self.header(header),
}
} }
/// Add `Date` header to message /// Add `Date` header to message
@@ -275,41 +280,6 @@ impl MessageBuilder {
self.date(SystemTime::now()) self.date(SystemTime::now())
} }
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
let s: String = subject.into();
self.header(header::Subject::from(s))
}
/// Set `MIME-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
/// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.header(header::Sender(mbox))`.
pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender::from(mbox))
}
/// Set or add mailbox to `From` header
///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From::from(Mailboxes::from(mbox)))
}
/// Set or add mailbox to `ReplyTo` header /// Set or add mailbox to `ReplyTo` header
/// ///
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2). /// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
@@ -352,6 +322,14 @@ impl MessageBuilder {
self.header(header::References::from(id)) self.header(header::References::from(id))
} }
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
let s: String = subject.into();
self.header(header::Subject::from(s))
}
/// Set [Message-ID /// Set [Message-ID
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4) /// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
/// ///
@@ -367,9 +345,9 @@ impl MessageBuilder {
let hostname = hostname::get() let hostname = hostname::get()
.map_err(|_| ()) .map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ())) .and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string()); .unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
#[cfg(not(feature = "hostname"))] #[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string(); let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
self.header(header::MessageId::from( self.header(header::MessageId::from(
// https://tools.ietf.org/html/rfc5322#section-3.6.4 // https://tools.ietf.org/html/rfc5322#section-3.6.4
@@ -380,17 +358,48 @@ impl MessageBuilder {
} }
/// Set [User-Agent /// Set [User-Agent
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004) /// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00)
pub fn user_agent(self, id: String) -> Self { pub fn user_agent(self, id: String) -> Self {
self.header(header::UserAgent::from(id)) self.header(header::UserAgent::from(id))
} }
/// Set custom header to message
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
match self.headers.get::<H>() {
Some(mut header_) => {
header_.join_mailboxes(header);
self.header(header_)
}
None => self.header(header),
}
}
/// Force specific envelope (by default it is derived from headers) /// Force specific envelope (by default it is derived from headers)
pub fn envelope(mut self, envelope: Envelope) -> Self { pub fn envelope(mut self, envelope: Envelope) -> Self {
self.envelope = Some(envelope); self.envelope = Some(envelope);
self self
} }
/// Keep the `Bcc` header
///
/// 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 behavior.
pub fn keep_bcc(mut self) -> Self {
self.drop_bcc = false;
self
}
// TODO: High-level methods for attachments and embedded files // TODO: High-level methods for attachments and embedded files
/// Create message from body /// Create message from body
@@ -423,8 +432,10 @@ impl MessageBuilder {
None => Envelope::try_from(&res.headers)?, None => Envelope::try_from(&res.headers)?,
}; };
// Remove `Bcc` headers now the envelope is set if res.drop_bcc {
res.headers.remove::<header::Bcc>(); // Remove `Bcc` headers now the envelope is set
res.headers.remove::<header::Bcc>();
}
Ok(Message { Ok(Message {
headers: res.headers, headers: res.headers,
@@ -455,6 +466,15 @@ impl MessageBuilder {
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> { pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(MessageBody::Mime(Part::Single(part))) self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
} }
/// Set `MIME-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
/// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
} }
/// Email message which can be formatted /// Email message which can be formatted
@@ -483,6 +503,11 @@ impl Message {
&self.headers &self.headers
} }
/// Get a mutable reference to the headers
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
/// Get `Message` envelope /// Get `Message` envelope
pub fn envelope(&self) -> &Envelope { pub fn envelope(&self) -> &Envelope {
&self.envelope &self.envelope
@@ -521,7 +546,7 @@ impl Message {
/// .reply_to("Bob <bob@example.org>".parse().unwrap()) /// .reply_to("Bob <bob@example.org>".parse().unwrap())
/// .to("Carla <carla@example.net>".parse().unwrap()) /// .to("Carla <carla@example.net>".parse().unwrap())
/// .subject("Hello") /// .subject("Hello")
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_string()) /// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
/// .unwrap(); /// .unwrap();
/// let key = "-----BEGIN RSA PRIVATE KEY----- /// let key = "-----BEGIN RSA PRIVATE KEY-----
/// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8 /// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
@@ -550,10 +575,10 @@ impl Message {
/// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw /// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw
/// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25 /// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25
/// -----END RSA PRIVATE KEY-----"; /// -----END RSA PRIVATE KEY-----";
/// let signing_key = DkimSigningKey::new(key.to_string(), DkimSigningAlgorithm::Rsa).unwrap(); /// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
/// message.sign(&DkimConfig::default_config( /// message.sign(&DkimConfig::default_config(
/// "dkimtest".to_string(), /// "dkimtest".to_owned(),
/// "example.org".to_string(), /// "example.org".to_owned(),
/// signing_key, /// signing_key,
/// )); /// ));
/// println!( /// println!(
@@ -598,6 +623,8 @@ fn make_message_id() -> String {
mod test { mod test {
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use pretty_assertions::assert_eq;
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart}; use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
#[test] #[test]
@@ -608,7 +635,7 @@ mod test {
} }
#[test] #[test]
fn email_miminal_message() { fn email_minimal_message() {
assert!(Message::builder() assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("NoBody <nobody@domain.tld>".parse().unwrap()) .to("NoBody <nobody@domain.tld>".parse().unwrap())
@@ -626,7 +653,7 @@ mod test {
} }
#[test] #[test]
fn email_message() { fn email_message_no_bcc() {
// Tue, 15 Nov 1994 08:12:31 GMT // Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151); let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
@@ -661,6 +688,44 @@ mod test {
); );
} }
#[test]
fn email_message_keep_bcc() {
// Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
let email = Message::builder()
.date(date)
.bcc("hidden@example.com".parse().unwrap())
.keep_bcc()
.header(header::From(
vec![Mailbox::new(
Some("Каи".into()),
"kayo@example.com".parse().unwrap(),
)]
.into(),
))
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject::from(String::from("яңа ел белән!")))
.body(String::from("Happy new year!"))
.unwrap();
assert_eq!(
String::from_utf8(email.formatted()).unwrap(),
concat!(
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
"Bcc: hidden@example.com\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Happy new year!"
)
);
}
#[test] #[test]
fn email_with_png() { fn email_with_png() {
// Tue, 15 Nov 1994 08:12:31 GMT // Tue, 15 Nov 1994 08:12:31 GMT

View File

@@ -71,7 +71,7 @@ impl fmt::Display for Error {
}; };
if let Some(ref e) = self.inner.source { if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?; write!(f, ": {e}")?;
} }
Ok(()) Ok(())

View File

@@ -208,18 +208,18 @@ impl FileTransport {
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> { pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
use std::fs; use std::fs;
let eml_file = self.path.join(format!("{}.eml", email_id)); let eml_file = self.path.join(format!("{email_id}.eml"));
let eml = fs::read(eml_file).map_err(error::io)?; let eml = fs::read(eml_file).map_err(error::io)?;
let json_file = self.path.join(format!("{}.json", email_id)); let json_file = self.path.join(format!("{email_id}.json"));
let json = fs::read(&json_file).map_err(error::io)?; let json = fs::read(json_file).map_err(error::io)?;
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?; let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
Ok((envelope, eml)) Ok((envelope, eml))
} }
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf { fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
self.path.join(format!("{}.{}", email_id, extension)) self.path.join(format!("{email_id}.{extension}"))
} }
} }
@@ -255,10 +255,10 @@ where
/// Reads the envelope and the raw message content. /// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> { pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
let eml_file = self.inner.path.join(format!("{}.eml", email_id)); let eml_file = self.inner.path.join(format!("{email_id}.eml"));
let eml = E::fs_read(&eml_file).await.map_err(error::io)?; let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
let json_file = self.inner.path.join(format!("{}.json", email_id)); let json_file = self.inner.path.join(format!("{email_id}.json"));
let json = E::fs_read(&json_file).await.map_err(error::io)?; let json = E::fs_read(&json_file).await.map_err(error::io)?;
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?; let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
@@ -276,6 +276,8 @@ impl Transport for FileTransport {
let email_id = Uuid::new_v4(); let email_id = Uuid::new_v4();
let file = self.path(&email_id, "eml"); let file = self.path(&email_id, "eml");
#[cfg(feature = "tracing")]
tracing::debug!(?file, "writing email to");
fs::write(file, email).map_err(error::io)?; fs::write(file, email).map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]
@@ -306,6 +308,8 @@ where
let email_id = Uuid::new_v4(); let email_id = Uuid::new_v4();
let file = self.inner.path(&email_id, "eml"); let file = self.inner.path(&email_id, "eml");
#[cfg(feature = "tracing")]
tracing::debug!(?file, "writing email to");
E::fs_write(&file, email).await.map_err(error::io)?; E::fs_write(&file, email).await.map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")] #[cfg(feature = "file-transport-envelope")]

View File

@@ -65,7 +65,7 @@
//! .subject("Happy new year") //! .subject("Happy new year")
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); //! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
//! //!
//! // Open a remote connection to the SMTP relay server //! // Open a remote connection to the SMTP relay server
//! let mailer = SmtpTransport::relay("smtp.gmail.com")? //! let mailer = SmtpTransport::relay("smtp.gmail.com")?
@@ -75,7 +75,7 @@
//! // Send the email //! // Send the email
//! match mailer.send(&email) { //! match mailer.send(&email) {
//! Ok(_) => println!("Email sent successfully!"), //! Ok(_) => println!("Email sent successfully!"),
//! Err(e) => panic!("Could not send email: {:?}", e), //! Err(e) => panic!("Could not send email: {e:?}"),
//! } //! }
//! # Ok(()) //! # Ok(())
//! # } //! # }
@@ -127,6 +127,9 @@ pub trait Transport {
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))] #[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> { fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "tracing")]
tracing::trace!("starting to send an email");
let raw = message.formatted(); let raw = message.formatted();
self.send_raw(message.envelope(), &raw) self.send_raw(message.envelope(), &raw)
} }
@@ -149,6 +152,9 @@ pub trait AsyncTransport {
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))] #[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message // TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> { async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "tracing")]
tracing::trace!("starting to send an email");
let raw = message.formatted(); let raw = message.formatted();
let envelope = message.envelope(); let envelope = message.envelope();
self.send_raw(envelope, &raw).await self.send_raw(envelope, &raw).await

View File

@@ -68,7 +68,7 @@ impl fmt::Display for Error {
}; };
if let Some(ref e) = self.inner.source { if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?; write!(f, ": {e}")?;
} }
Ok(()) Ok(())

View File

@@ -232,6 +232,9 @@ impl Transport for SendmailTransport {
type Error = Error; type Error = Error;
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "tracing")]
tracing::debug!(command = ?self.command, "sending email with");
// Spawn the sendmail command // Spawn the sendmail command
let mut process = self.command(envelope).spawn().map_err(error::client)?; let mut process = self.command(envelope).spawn().map_err(error::client)?;
@@ -261,6 +264,9 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> { async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use async_std::io::prelude::WriteExt; use async_std::io::prelude::WriteExt;
#[cfg(feature = "tracing")]
tracing::debug!(command = ?self.inner.command, "sending email with");
let mut command = self.async_std_command(envelope); let mut command = self.async_std_command(envelope);
// Spawn the sendmail command // Spawn the sendmail command
@@ -293,6 +299,9 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> { async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio1_crate::io::AsyncWriteExt; use tokio1_crate::io::AsyncWriteExt;
#[cfg(feature = "tracing")]
tracing::debug!(command = ?self.inner.command, "sending email with");
let mut command = self.tokio1_command(envelope); let mut command = self.tokio1_command(envelope);
// Spawn the sendmail command // Spawn the sendmail command

View File

@@ -1,7 +1,8 @@
#[cfg(feature = "pool")]
use std::sync::Arc;
use std::{ use std::{
fmt::{self, Debug}, fmt::{self, Debug},
marker::PhantomData, marker::PhantomData,
sync::Arc,
time::Duration, time::Duration,
}; };
@@ -102,7 +103,7 @@ where
.tls(Tls::Wrapper(tls_parameters))) .tls(Tls::Wrapper(tls_parameters)))
} }
/// Simple an secure transport, using STARTTLS to obtain encrypted connections /// Simple and secure transport, using STARTTLS to obtain encrypted connections
/// ///
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers /// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections. /// that don't take SMTPS connections.
@@ -150,28 +151,114 @@ where
/// ///
/// * No authentication /// * No authentication
/// * No TLS /// * No TLS
/// * A 60 seconds timeout for smtp commands /// * A 60-seconds timeout for smtp commands
/// * Port 25 /// * Port 25
/// ///
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or /// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead, /// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible. /// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder { pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
let info = SmtpInfo { AsyncSmtpTransportBuilder::new(server)
server: server.into(), }
..Default::default()
}; /// Creates a `AsyncSmtpTransportBuilder` from a connection URL
AsyncSmtpTransportBuilder { ///
info, /// The protocol, credentials, host and port can be provided in a single URL.
#[cfg(feature = "pool")] /// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
pool_config: PoolConfig::default(), /// `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 /// Tests the SMTP connection
/// ///
/// `test_connection()` tests the connection by using the SMTP NOOP command. /// `test_connection()` tests the connection by using the SMTP NOOP command.
/// The connection is closed afterwards if a connection pool is not used. /// The connection is closed afterward if a connection pool is not used.
pub async fn test_connection(&self) -> Result<bool, Error> { pub async fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection().await?; let mut conn = self.inner.connection().await?;
@@ -198,6 +285,9 @@ where
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
#[cfg(feature = "pool")]
inner: Arc::clone(&self.inner),
#[cfg(not(feature = "pool"))]
inner: self.inner.clone(), inner: self.inner.clone(),
} }
} }
@@ -215,6 +305,20 @@ pub struct AsyncSmtpTransportBuilder {
/// Builder for the SMTP `AsyncSmtpTransport` /// Builder for the SMTP `AsyncSmtpTransport`
impl AsyncSmtpTransportBuilder { impl AsyncSmtpTransportBuilder {
// Create new builder with default parameters
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
let info = SmtpInfo {
server: server.into(),
..Default::default()
};
AsyncSmtpTransportBuilder {
info,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Set the name used during EHLO /// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self { pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name; self.info.hello_name = name;

View File

@@ -51,7 +51,7 @@ pub enum Mechanism {
/// [RFC 4616](https://tools.ietf.org/html/rfc4616) /// [RFC 4616](https://tools.ietf.org/html/rfc4616)
Plain, Plain,
/// LOGIN authentication mechanism /// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365) /// Obsolete but needed for some providers (like Office 365)
/// ///
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt). /// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
Login, Login,
@@ -71,7 +71,7 @@ impl Display for Mechanism {
} }
impl Mechanism { impl Mechanism {
/// Does the mechanism supports initial response /// Does the mechanism support initial response?
pub fn supports_initial_response(self) -> bool { pub fn supports_initial_response(self) -> bool {
match self { match self {
Mechanism::Plain | Mechanism::Xoauth2 => true, Mechanism::Plain | Mechanism::Xoauth2 => true,
@@ -98,12 +98,12 @@ impl Mechanism {
let decoded_challenge = challenge let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?; .ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) { if ["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.authentication_identity.to_string()); return Ok(credentials.authentication_identity.clone());
} }
if vec!["Password", "Password:"].contains(&decoded_challenge) { if ["Password", "Password:"].contains(&decoded_challenge) {
return Ok(credentials.secret.to_string()); return Ok(credentials.secret.clone());
} }
Err(error::client("Unrecognized challenge")) Err(error::client("Unrecognized challenge"))
@@ -127,7 +127,7 @@ mod test {
fn test_plain() { fn test_plain() {
let mechanism = Mechanism::Plain; let mechanism = Mechanism::Plain;
let credentials = Credentials::new("username".to_string(), "password".to_string()); let credentials = Credentials::new("username".to_owned(), "password".to_owned());
assert_eq!( assert_eq!(
mechanism.response(&credentials, None).unwrap(), mechanism.response(&credentials, None).unwrap(),
@@ -140,7 +140,7 @@ mod test {
fn test_login() { fn test_login() {
let mechanism = Mechanism::Login; let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string()); let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
assert_eq!( assert_eq!(
mechanism.response(&credentials, Some("Username")).unwrap(), mechanism.response(&credentials, Some("Username")).unwrap(),
@@ -158,8 +158,8 @@ mod test {
let mechanism = Mechanism::Xoauth2; let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new( let credentials = Credentials::new(
"username".to_string(), "username".to_owned(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(), "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
); );
assert_eq!( assert_eq!(
@@ -172,7 +172,7 @@ mod test {
#[test] #[test]
fn test_from_user_pass_for_credentials() { fn test_from_user_pass_for_credentials() {
assert_eq!( assert_eq!(
Credentials::new("alice".to_string(), "wonderland".to_string()), Credentials::new("alice".to_owned(), "wonderland".to_owned()),
Credentials::from(("alice", "wonderland")) Credentials::from(("alice", "wonderland"))
); );
} }

View File

@@ -1,14 +1,16 @@
use std::{fmt::Display, time::Duration}; use std::{fmt::Display, net::IpAddr, time::Duration};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[cfg(feature = "tokio1")]
use super::async_net::AsyncTokioStream;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use super::escape_crlf; use super::escape_crlf;
use super::{AsyncNetworkStream, ClientCodec, TlsParameters}; use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{ use crate::{
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
commands::*, commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
error, error,
error::Error, error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
@@ -41,21 +43,65 @@ pub struct AsyncSmtpConnection {
} }
impl AsyncSmtpConnection { impl AsyncSmtpConnection {
/// Get information about the server
pub fn server_info(&self) -> &ServerInfo { pub fn server_info(&self) -> &ServerInfo {
&self.server_info &self.server_info
} }
/// Connects to the configured server /// Connects with existing async stream
/// ///
/// Sends EHLO and parses server information /// Sends EHLO and parses server information
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub async fn connect_with_transport(
stream: Box<dyn AsyncTokioStream>,
hello_name: &ClientId,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::use_existing_tokio1(stream);
Self::connect_impl(stream, hello_name).await
}
/// 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_addres` 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>( pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
local_address: Option<IpAddr>,
) -> Result<AsyncSmtpConnection, Error> { ) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters).await?; let stream =
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
.await?;
Self::connect_impl(stream, hello_name).await Self::connect_impl(stream, hello_name).await
} }
@@ -114,7 +160,7 @@ impl AsyncSmtpConnection {
mail_options.push(MailParameter::SmtpUtfEight); mail_options.push(MailParameter::SmtpUtfEight);
} }
// Check for non-ascii content in message // Check for non-ascii content in the message
if !email.is_ascii() { if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) { if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(error::client( return Err(error::client(
@@ -154,6 +200,12 @@ impl AsyncSmtpConnection {
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls) !self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
} }
/// Upgrade the connection using `STARTTLS`.
///
/// As described in [rfc3207]. Note that this mechanism has been deprecated in [rfc8314].
///
/// [rfc3207]: https://www.rfc-editor.org/rfc/rfc3207
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
#[allow(unused_variables)] #[allow(unused_variables)]
pub async fn starttls( pub async fn starttls(
&mut self, &mut self,
@@ -190,6 +242,7 @@ impl AsyncSmtpConnection {
self.panic = true; self.panic = true;
let _ = self.command(Quit).await; let _ = self.command(Quit).await;
} }
let _ = self.stream.close().await;
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -207,7 +260,7 @@ impl AsyncSmtpConnection {
self.command(Noop).await.is_ok() self.command(Noop).await.is_ok()
} }
/// Sends an AUTH command with the given mechanism, and handles challenge if needed /// Sends an AUTH command with the given mechanism, and handles the challenge if needed
pub async fn auth( pub async fn auth(
&mut self, &mut self,
mechanisms: &[Mechanism], mechanisms: &[Mechanism],
@@ -298,7 +351,7 @@ impl AsyncSmtpConnection {
} else { } else {
Err(error::code( Err(error::code(
response.code(), response.code(),
response.first_line().map(|s| s.to_owned()), Some(response.message().collect()),
)) ))
} }
} }
@@ -316,7 +369,7 @@ impl AsyncSmtpConnection {
} }
/// The X509 certificate of the server (DER encoded) /// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate() self.stream.get_ref().peer_certificate()
} }

View File

@@ -1,6 +1,6 @@
use std::{ use std::{
io, mem, fmt, io, mem,
net::SocketAddr, net::{IpAddr, SocketAddr},
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
time::Duration, time::Duration,
@@ -16,10 +16,15 @@ use futures_io::{
}; };
#[cfg(feature = "async-std1-rustls-tls")] #[cfg(feature = "async-std1-rustls-tls")]
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream; use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio1-boring-tls")]
use tokio1_boring::SslStream as Tokio1SslStream;
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf}; use tokio1_crate::io::{AsyncRead, AsyncWrite, ReadBuf as Tokio1ReadBuf};
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
use tokio1_crate::net::{TcpStream as Tokio1TcpStream, ToSocketAddrs as Tokio1ToSocketAddrs}; use tokio1_crate::net::{
TcpSocket as Tokio1TcpSocket, TcpStream as Tokio1TcpStream,
ToSocketAddrs as Tokio1ToSocketAddrs,
};
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream; use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
@@ -28,33 +33,53 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(any( #[cfg(any(
feature = "tokio1-native-tls", feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls", feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls",
feature = "async-std1-native-tls", feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls" feature = "async-std1-rustls-tls"
))] ))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::TlsParameters;
#[cfg(feature = "tokio1")]
use crate::transport::smtp::client::net::resolved_address_filter;
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// A network stream /// A network stream
#[derive(Debug)]
pub struct AsyncNetworkStream { pub struct AsyncNetworkStream {
inner: InnerAsyncNetworkStream, inner: InnerAsyncNetworkStream,
} }
#[cfg(feature = "tokio1")]
pub trait AsyncTokioStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + fmt::Debug {
fn peer_addr(&self) -> io::Result<SocketAddr>;
}
#[cfg(feature = "tokio1")]
impl AsyncTokioStream for Tokio1TcpStream {
fn peer_addr(&self) -> io::Result<SocketAddr> {
self.peer_addr()
}
}
/// Represents the different types of underlying network streams /// Represents the different types of underlying network streams
// usually only one TLS backend at a time is going to be enabled, // usually only one TLS backend at a time is going to be enabled,
// so clippy::large_enum_variant doesn't make sense here // so clippy::large_enum_variant doesn't make sense here
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug)]
enum InnerAsyncNetworkStream { enum InnerAsyncNetworkStream {
/// Plain Tokio 1.x TCP stream /// Plain Tokio 1.x TCP stream
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
Tokio1Tcp(Tokio1TcpStream), Tokio1Tcp(Box<dyn AsyncTokioStream>),
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-native-tls")] #[cfg(feature = "tokio1-native-tls")]
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>), Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream /// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>), Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-boring-tls")]
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
/// Plain Tokio 1.x TCP stream /// Plain Tokio 1.x TCP stream
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
AsyncStd1Tcp(AsyncStd1TcpStream), AsyncStd1Tcp(AsyncStd1TcpStream),
@@ -88,6 +113,8 @@ impl AsyncNetworkStream {
} }
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(), InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(), InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -104,50 +131,71 @@ impl AsyncNetworkStream {
} }
} }
#[cfg(feature = "tokio1")]
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
}
#[cfg(feature = "tokio1")] #[cfg(feature = "tokio1")]
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>( pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
local_addr: Option<IpAddr>,
) -> Result<AsyncNetworkStream, Error> { ) -> Result<AsyncNetworkStream, Error> {
async fn try_connect_timeout<T: Tokio1ToSocketAddrs>( async fn try_connect<T: Tokio1ToSocketAddrs>(
server: T, server: T,
timeout: Duration, timeout: Option<Duration>,
local_addr: Option<IpAddr>,
) -> Result<Tokio1TcpStream, Error> { ) -> Result<Tokio1TcpStream, Error> {
let addrs = tokio1_crate::net::lookup_host(server) let addrs = tokio1_crate::net::lookup_host(server)
.await .await
.map_err(error::connection)?; .map_err(error::connection)?
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
let mut last_err = None; let mut last_err = None;
for addr in addrs { for addr in addrs {
let connect_future = Tokio1TcpStream::connect(&addr); let socket = match addr.ip() {
match tokio1_crate::time::timeout(timeout, connect_future).await { IpAddr::V4(_) => Tokio1TcpSocket::new_v4(),
Ok(Ok(stream)) => return Ok(stream), IpAddr::V6(_) => Tokio1TcpSocket::new_v6(),
Ok(Err(err)) => last_err = Some(err), }
Err(_) => { .map_err(error::connection)?;
last_err = Some(io::Error::new( if let Some(local_addr) = local_addr {
io::ErrorKind::TimedOut, socket
"connection timed out", .bind(SocketAddr::new(local_addr, 0))
)) .map_err(error::connection)?;
}
let connect_future = socket.connect(addr);
if let Some(timeout) = timeout {
match tokio1_crate::time::timeout(timeout, connect_future).await {
Ok(Ok(stream)) => return Ok(stream),
Ok(Err(err)) => last_err = Some(err),
Err(_) => {
last_err = Some(io::Error::new(
io::ErrorKind::TimedOut,
"connection timed out",
))
}
}
} else {
match connect_future.await {
Ok(stream) => return Ok(stream),
Err(err) => last_err = Some(err),
} }
} }
} }
Err(match last_err { Err(match last_err {
Some(last_err) => error::connection(last_err), Some(last_err) => error::connection(last_err),
None => error::connection("could not resolve to any address"), None => error::connection("could not resolve to any supported address"),
}) })
} }
let tcp_stream = match timeout { let tcp_stream = try_connect(server, timeout, local_addr).await?;
Some(t) => try_connect_timeout(server, t).await?, let mut stream =
None => Tokio1TcpStream::connect(server) AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
.await
.map_err(error::connection)?,
};
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?; stream.upgrade_tls(tls_parameters).await?;
} }
@@ -160,6 +208,9 @@ impl AsyncNetworkStream {
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<TlsParameters>, tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> { ) -> Result<AsyncNetworkStream, Error> {
// Unfortunately, there doesn't currently seem to be a way to set the local address.
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
// been connected, which is a blocking operation.
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>( async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
server: T, server: T,
timeout: Duration, timeout: Duration,
@@ -206,14 +257,22 @@ impl AsyncNetworkStream {
match &self.inner { match &self.inner {
#[cfg(all( #[cfg(all(
feature = "tokio1", feature = "tokio1",
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls")) not(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls"
))
))] ))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => { InnerAsyncNetworkStream::Tokio1Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature"); panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
} }
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls"
))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => { InnerAsyncNetworkStream::Tokio1Tcp(_) => {
// get owned TcpStream // get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None); let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
@@ -255,12 +314,16 @@ impl AsyncNetworkStream {
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))] #[cfg(any(
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "tokio1-boring-tls"
))]
async fn upgrade_tokio1_tls( async fn upgrade_tokio1_tls(
tcp_stream: Tokio1TcpStream, tcp_stream: Box<dyn AsyncTokioStream>,
tls_parameters: TlsParameters, tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> { ) -> Result<InnerAsyncNetworkStream, Error> {
let domain = tls_parameters.domain().to_string(); let domain = tls_parameters.domain().to_owned();
match tls_parameters.connector { match tls_parameters.connector {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
@@ -301,11 +364,31 @@ impl AsyncNetworkStream {
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream)) Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
}; };
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
#[cfg(not(feature = "tokio1-boring-tls"))]
panic!("built without the tokio1-boring-tls feature");
#[cfg(feature = "tokio1-boring-tls")]
return {
let mut config = connector.configure().map_err(error::connection)?;
config.set_verify_hostname(tls_parameters.accept_invalid_hostnames);
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1BoringTls(stream))
};
}
} }
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))] #[cfg(any(
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls",
feature = "async-std1-boring-tls"
))]
async fn upgrade_asyncstd1_tls( async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream, tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters, mut tls_parameters: TlsParameters,
@@ -354,6 +437,10 @@ impl AsyncNetworkStream {
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream)) Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
}; };
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
panic!("boring-tls isn't supported with async-std yet.");
}
} }
} }
@@ -365,6 +452,8 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true, InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true, InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false, InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -399,6 +488,13 @@ impl AsyncNetworkStream {
.unwrap() .unwrap()
.clone() .clone()
.0), .0),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
.ssl()
.peer_certificate()
.unwrap()
.to_der()
.map_err(error::tls)?),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => { InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
Err(error::client("Connection is not encrypted")) Err(error::client("Connection is not encrypted"))
@@ -454,6 +550,15 @@ impl FuturesAsyncRead for AsyncNetworkStream {
Poll::Pending => Poll::Pending, Poll::Pending => Poll::Pending,
} }
} }
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut 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())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf), InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -485,6 +590,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut 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),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf), InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -510,6 +617,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx), InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]
@@ -531,6 +640,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-rustls-tls")] #[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx), InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-boring-tls")]
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")] #[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx), InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-native-tls")] #[cfg(feature = "async-std1-native-tls")]

View File

@@ -1,7 +1,7 @@
use std::{ use std::{
fmt::Display, fmt::Display,
io::{self, BufRead, BufReader, Write}, io::{self, BufRead, BufReader, Write},
net::ToSocketAddrs, net::{IpAddr, ToSocketAddrs},
time::Duration, time::Duration,
}; };
@@ -12,7 +12,7 @@ use crate::{
address::Envelope, address::Envelope,
transport::smtp::{ transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
commands::*, commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
error, error,
error::Error, error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
@@ -44,6 +44,7 @@ pub struct SmtpConnection {
} }
impl SmtpConnection { impl SmtpConnection {
/// Get information about the server
pub fn server_info(&self) -> &ServerInfo { pub fn server_info(&self) -> &ServerInfo {
&self.server_info &self.server_info
} }
@@ -58,8 +59,9 @@ impl SmtpConnection {
timeout: Option<Duration>, timeout: Option<Duration>,
hello_name: &ClientId, hello_name: &ClientId,
tls_parameters: Option<&TlsParameters>, tls_parameters: Option<&TlsParameters>,
local_address: Option<IpAddr>,
) -> Result<SmtpConnection, Error> { ) -> Result<SmtpConnection, Error> {
let stream = NetworkStream::connect(server, timeout, tls_parameters)?; let stream = NetworkStream::connect(server, timeout, tls_parameters, local_address)?;
let stream = BufReader::new(stream); let stream = BufReader::new(stream);
let mut conn = SmtpConnection { let mut conn = SmtpConnection {
stream, stream,
@@ -98,7 +100,7 @@ impl SmtpConnection {
mail_options.push(MailParameter::SmtpUtfEight); mail_options.push(MailParameter::SmtpUtfEight);
} }
// Check for non-ascii content in message // Check for non-ascii content in the message
if !email.is_ascii() { if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) { if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(error::client( return Err(error::client(
@@ -141,7 +143,7 @@ impl SmtpConnection {
hello_name: &ClientId, hello_name: &ClientId,
) -> Result<(), Error> { ) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) { if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
{ {
try_smtp!(self.command(Starttls), self); try_smtp!(self.command(Starttls), self);
self.stream.get_mut().upgrade_tls(tls_parameters)?; self.stream.get_mut().upgrade_tls(tls_parameters)?;
@@ -151,7 +153,11 @@ impl SmtpConnection {
try_smtp!(self.ehlo(hello_name), self); try_smtp!(self.ehlo(hello_name), self);
Ok(()) Ok(())
} }
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] #[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
// This should never happen as `Tls` can only be created // This should never happen as `Tls` can only be created
// when a TLS library is enabled // when a TLS library is enabled
unreachable!("TLS support required but not supported"); unreachable!("TLS support required but not supported");
@@ -177,6 +183,7 @@ impl SmtpConnection {
self.panic = true; self.panic = true;
let _ = self.command(Quit); let _ = self.command(Quit);
} }
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
} }
/// Sets the underlying stream /// Sets the underlying stream
@@ -200,7 +207,7 @@ impl SmtpConnection {
self.command(Noop).is_ok() self.command(Noop).is_ok()
} }
/// Sends an AUTH command with the given mechanism, and handles challenge if needed /// Sends an AUTH command with the given mechanism, and handles the challenge if needed
pub fn auth( pub fn auth(
&mut self, &mut self,
mechanisms: &[Mechanism], mechanisms: &[Mechanism],
@@ -236,11 +243,12 @@ impl SmtpConnection {
/// Sends the message content /// Sends the message content
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> { pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new(); let mut codec = ClientCodec::new();
let mut out_buf = Vec::with_capacity(message.len());
codec.encode(message, &mut out_buf); codec.encode(message, &mut out_buf);
self.write(out_buf.as_slice())?; self.write(out_buf.as_slice())?;
self.write(b"\r\n.\r\n")?; self.write(b"\r\n.\r\n")?;
self.read_response() self.read_response()
} }
@@ -277,7 +285,7 @@ impl SmtpConnection {
} else { } else {
Err(error::code( Err(error::code(
response.code(), response.code(),
response.first_line().map(|s| s.to_owned()), Some(response.message().collect()),
)) ))
}; };
} }
@@ -295,7 +303,7 @@ impl SmtpConnection {
} }
/// The X509 certificate of the server (DER encoded) /// The X509 certificate of the server (DER encoded)
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
self.stream.get_ref().peer_certificate() self.stream.get_ref().peer_certificate()
} }

View File

@@ -11,8 +11,8 @@
//! client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT, //! client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT,
//! }; //! };
//! //!
//! let hello = ClientId::Domain("my_hostname".to_string()); //! let hello = ClientId::Domain("my_hostname".to_owned());
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None)?; //! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?;
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?; //! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?; //! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
//! client.command(Data)?; //! client.command(Data)?;
@@ -29,12 +29,16 @@ use std::fmt::Debug;
pub use self::async_connection::AsyncSmtpConnection; pub use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_net::AsyncNetworkStream; pub use self::async_net::AsyncNetworkStream;
#[cfg(feature = "tokio1")]
pub use self::async_net::AsyncTokioStream;
use self::net::NetworkStream; use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub(super) use self::tls::InnerTlsParameters; pub(super) use self::tls::InnerTlsParameters;
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub use self::tls::TlsVersion;
pub use self::{ pub use self::{
connection::SmtpConnection, connection::SmtpConnection,
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder}, tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
}; };
#[cfg(any(feature = "tokio1", feature = "async-std1"))] #[cfg(any(feature = "tokio1", feature = "async-std1"))]
@@ -46,60 +50,57 @@ mod net;
mod tls; mod tls;
/// The codec used for transparency /// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
struct ClientCodec { struct ClientCodec {
escape_count: u8, status: CodecStatus,
} }
impl ClientCodec { impl ClientCodec {
/// Creates a new client codec /// Creates a new client codec
pub fn new() -> Self { pub fn new() -> Self {
ClientCodec::default() Self {
status: CodecStatus::StartOfNewLine,
}
} }
/// Adds transparency /// Adds transparency
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) { fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
match frame.len() { for &b in frame {
0 => { buf.push(b);
match self.escape_count { match (b, self.status) {
0 => buf.extend_from_slice(b"\r\n.\r\n"), (b'\r', _) => {
1 => buf.extend_from_slice(b"\n.\r\n"), self.status = CodecStatus::StartingNewLine;
2 => buf.extend_from_slice(b".\r\n"),
_ => unreachable!(),
} }
self.escape_count = 0; (b'\n', CodecStatus::StartingNewLine) => {
} self.status = CodecStatus::StartOfNewLine;
_ => {
let mut start = 0;
for (idx, byte) in frame.iter().enumerate() {
match self.escape_count {
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
2 => {
self.escape_count = if *byte == b'.' {
3
} else if *byte == b'\r' {
1
} else {
0
}
}
_ => unreachable!(),
}
if self.escape_count == 3 {
self.escape_count = 0;
buf.extend_from_slice(&frame[start..idx]);
buf.extend_from_slice(b".");
start = idx;
}
} }
buf.extend_from_slice(&frame[start..]); (_, CodecStatus::StartingNewLine) => {
self.status = CodecStatus::MiddleOfLine;
}
(b'.', CodecStatus::StartOfNewLine) => {
self.status = CodecStatus::MiddleOfLine;
buf.push(b'.');
}
(_, CodecStatus::StartOfNewLine) => {
self.status = CodecStatus::MiddleOfLine;
}
_ => {}
} }
} }
} }
} }
#[derive(Debug, Copy, Clone)]
#[allow(clippy::enum_variant_names)]
enum CodecStatus {
/// We are past the first character of the current line
MiddleOfLine,
/// We just read a `\r` character
StartingNewLine,
/// We are at the start of a new line
StartOfNewLine,
}
/// Returns the string replacing all the CRLF with "\<CRLF\>" /// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays /// Used for debug displays
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
@@ -113,9 +114,10 @@ mod test {
#[test] #[test]
fn test_codec() { fn test_codec() {
let mut buf = Vec::new();
let mut codec = ClientCodec::new(); let mut codec = ClientCodec::new();
let mut buf: Vec<u8> = vec![];
codec.encode(b".\r\n", &mut buf);
codec.encode(b"test\r\n", &mut buf); codec.encode(b"test\r\n", &mut buf);
codec.encode(b"test\r\n\r\n", &mut buf); codec.encode(b"test\r\n\r\n", &mut buf);
codec.encode(b".\r\n", &mut buf); codec.encode(b".\r\n", &mut buf);
@@ -126,9 +128,13 @@ mod test {
codec.encode(b"test\n", &mut buf); codec.encode(b"test\n", &mut buf);
codec.encode(b".test\n", &mut buf); codec.encode(b".test\n", &mut buf);
codec.encode(b"test", &mut buf); codec.encode(b"test", &mut buf);
codec.encode(b"test", &mut buf);
codec.encode(b"test\r\n", &mut buf);
codec.encode(b".test\r\n", &mut buf);
codec.encode(b"test.\r\n", &mut buf);
assert_eq!( assert_eq!(
String::from_utf8(buf).unwrap(), String::from_utf8(buf).unwrap(),
"test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest" "..\r\ntest\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntesttesttest\r\n..test\r\ntest.\r\n"
); );
} }

View File

@@ -1,16 +1,21 @@
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
use std::{ use std::{
io::{self, Read, Write}, io::{self, Read, Write},
mem, mem,
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs}, net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
time::Duration, time::Duration,
}; };
#[cfg(feature = "boring-tls")]
use boring::ssl::SslStream;
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::TlsStream; use native_tls::TlsStream;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ClientConnection, ServerName, StreamOwned}; use rustls::{ClientConnection, ServerName, StreamOwned};
use socket2::{Domain, Protocol, Type};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::InnerTlsParameters; use super::InnerTlsParameters;
use super::TlsParameters; use super::TlsParameters;
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
@@ -33,6 +38,8 @@ enum InnerNetworkStream {
/// Encrypted TCP stream /// Encrypted TCP stream
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
RustlsTls(StreamOwned<ClientConnection, TcpStream>), RustlsTls(StreamOwned<ClientConnection, TcpStream>),
#[cfg(feature = "boring-tls")]
BoringTls(SslStream<TcpStream>),
/// Can't be built /// Can't be built
None, None,
} }
@@ -54,6 +61,8 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(), InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(), InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(SocketAddr::V4(SocketAddrV4::new( Ok(SocketAddr::V4(SocketAddrV4::new(
@@ -72,6 +81,8 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how), InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how), InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(()) Ok(())
@@ -83,19 +94,39 @@ impl NetworkStream {
server: T, server: T,
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<&TlsParameters>, tls_parameters: Option<&TlsParameters>,
local_addr: Option<IpAddr>,
) -> Result<NetworkStream, Error> { ) -> Result<NetworkStream, Error> {
fn try_connect_timeout<T: ToSocketAddrs>( fn try_connect<T: ToSocketAddrs>(
server: T, server: T,
timeout: Duration, timeout: Option<Duration>,
local_addr: Option<IpAddr>,
) -> Result<TcpStream, Error> { ) -> Result<TcpStream, Error> {
let addrs = server.to_socket_addrs().map_err(error::connection)?; let addrs = server
.to_socket_addrs()
.map_err(error::connection)?
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
let mut last_err = None; let mut last_err = None;
for addr in addrs { for addr in addrs {
match TcpStream::connect_timeout(&addr, timeout) { let socket = socket2::Socket::new(
Ok(stream) => return Ok(stream), Domain::for_address(addr),
Err(err) => last_err = Some(err), Type::STREAM,
Some(Protocol::TCP),
)
.map_err(error::connection)?;
bind_local_address(&socket, &addr, local_addr)?;
if let Some(timeout) = timeout {
match socket.connect_timeout(&addr.into(), timeout) {
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
} else {
match socket.connect(&addr.into()) {
Ok(_) => return Ok(socket.into()),
Err(err) => last_err = Some(err),
}
} }
} }
@@ -105,11 +136,7 @@ impl NetworkStream {
}) })
} }
let tcp_stream = match timeout { let tcp_stream = try_connect(server, timeout, local_addr)?;
Some(t) => try_connect_timeout(server, t)?,
None => TcpStream::connect(server).map_err(error::connection)?,
};
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream)); let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters { if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters)?; stream.upgrade_tls(tls_parameters)?;
@@ -119,13 +146,17 @@ impl NetworkStream {
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> { pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
match &self.inner { match &self.inner {
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] #[cfg(not(any(
feature = "native-tls",
feature = "rustls-tls",
feature = "boring-tls"
)))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(_) => {
let _ = tls_parameters; let _ = tls_parameters;
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature"); panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
InnerNetworkStream::Tcp(_) => { InnerNetworkStream::Tcp(_) => {
// get owned TcpStream // get owned TcpStream
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None); let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
@@ -141,7 +172,7 @@ impl NetworkStream {
} }
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
fn upgrade_tls_impl( fn upgrade_tls_impl(
tcp_stream: TcpStream, tcp_stream: TcpStream,
tls_parameters: &TlsParameters, tls_parameters: &TlsParameters,
@@ -158,11 +189,21 @@ impl NetworkStream {
InnerTlsParameters::RustlsTls(connector) => { InnerTlsParameters::RustlsTls(connector) => {
let domain = ServerName::try_from(tls_parameters.domain()) let domain = ServerName::try_from(tls_parameters.domain())
.map_err(|_| error::connection("domain isn't a valid DNS name"))?; .map_err(|_| error::connection("domain isn't a valid DNS name"))?;
let connection = let connection = ClientConnection::new(Arc::clone(connector), domain)
ClientConnection::new(connector.clone(), domain).map_err(error::connection)?; .map_err(error::connection)?;
let stream = StreamOwned::new(connection, tcp_stream); let stream = StreamOwned::new(connection, tcp_stream);
InnerNetworkStream::RustlsTls(stream) InnerNetworkStream::RustlsTls(stream)
} }
#[cfg(feature = "boring-tls")]
InnerTlsParameters::BoringTls(connector) => {
let stream = connector
.configure()
.map_err(error::connection)?
.verify_hostname(tls_parameters.accept_invalid_hostnames)
.connect(tls_parameters.domain(), tcp_stream)
.map_err(error::connection)?;
InnerNetworkStream::BoringTls(stream)
}
}) })
} }
@@ -173,6 +214,8 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(_) => true, InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(_) => true, InnerNetworkStream::RustlsTls(_) => true,
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(_) => true,
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
false false
@@ -180,7 +223,7 @@ impl NetworkStream {
} }
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> { pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
match &self.inner { match &self.inner {
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")), InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
@@ -200,6 +243,13 @@ impl NetworkStream {
.unwrap() .unwrap()
.clone() .clone()
.0), .0),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(stream) => Ok(stream
.ssl()
.peer_certificate()
.unwrap()
.to_der()
.map_err(error::tls)?),
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"), InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
} }
} }
@@ -215,6 +265,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(ref mut stream) => { InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration) stream.get_ref().set_read_timeout(duration)
} }
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(()) Ok(())
@@ -235,7 +289,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(ref mut stream) => { InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration) stream.get_ref().set_write_timeout(duration)
} }
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(()) Ok(())
@@ -252,6 +309,8 @@ impl Read for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf), InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf), InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.read(buf),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0) Ok(0)
@@ -268,6 +327,8 @@ impl Write for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf), InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf), InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.write(buf),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0) Ok(0)
@@ -282,6 +343,8 @@ impl Write for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.flush(), InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(), InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
#[cfg(feature = "boring-tls")]
InnerNetworkStream::BoringTls(ref mut s) => s.flush(),
InnerNetworkStream::None => { InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built"); debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(()) Ok(())
@@ -289,3 +352,47 @@ impl Write for NetworkStream {
} }
} }
} }
/// If the local address is set, binds the socket to this address.
/// If local address is not set, then destination address is required to determine the default
/// local address on some platforms.
/// See: https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560
fn bind_local_address(
socket: &socket2::Socket,
dst_addr: &SocketAddr,
local_addr: Option<IpAddr>,
) -> Result<(), Error> {
match local_addr {
Some(local_addr) => {
socket
.bind(&SocketAddr::new(local_addr, 0).into())
.map_err(error::connection)?;
}
_ => {
if cfg!(windows) {
// Windows requires a socket be bound before calling connect
let any: SocketAddr = match dst_addr {
SocketAddr::V4(_) => ([0, 0, 0, 0], 0).into(),
SocketAddr::V6(_) => ([0, 0, 0, 0, 0, 0, 0, 0], 0).into(),
};
socket.bind(&any.into()).map_err(error::connection)?;
}
}
}
Ok(())
}
/// When we have an iterator of resolved remote addresses, we must filter them to be the same
/// protocol as the local address binding. If no local address is set, then all will be matched.
pub(crate) fn resolved_address_filter(
resolved_addr: &SocketAddr,
local_addr: Option<IpAddr>,
) -> bool {
match local_addr {
Some(local_addr) => match resolved_addr.ip() {
IpAddr::V4(_) => local_addr.is_ipv4(),
IpAddr::V6(_) => local_addr.is_ipv6(),
},
None => true,
}
}

View File

@@ -2,22 +2,57 @@ use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use std::{sync::Arc, time::SystemTime}; use std::{sync::Arc, time::SystemTime};
#[cfg(feature = "boring-tls")]
use boring::{
ssl::{SslConnector, SslVersion},
x509::store::X509StoreBuilder,
};
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
use rustls::{ use rustls::{
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier}, client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName, ClientConfig, Error as TlsError, RootCertStore, ServerName,
}; };
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// Accepted protocols by default. /// TLS protocol versions.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults. #[derive(Debug, Copy, Clone)]
// This is also rustls' default behavior #[non_exhaustive]
#[cfg(feature = "native-tls")] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12; pub enum TlsVersion {
/// TLS 1.0
///
/// Should only be used when trying to support legacy
/// SMTP servers that haven't updated to
/// at least TLS 1.2 yet.
///
/// Supported by `native-tls` and `boring-tls`.
Tlsv10,
/// TLS 1.1
///
/// Should only be used when trying to support legacy
/// SMTP servers that haven't updated to
/// at least TLS 1.2 yet.
///
/// Supported by `native-tls` and `boring-tls`.
Tlsv11,
/// TLS 1.2
///
/// A good option for most SMTP servers.
///
/// Supported by all TLS backends.
Tlsv12,
/// TLS 1.3
///
/// The most secure option, although not supported by all SMTP servers.
///
/// Although it is technically supported by all TLS backends,
/// trying to set it for `native-tls` will give a runtime error.
Tlsv13,
}
/// How to apply TLS to a client connection /// How to apply TLS to a client connection
#[derive(Clone)] #[derive(Clone)]
@@ -26,16 +61,25 @@ pub enum Tls {
/// Insecure connection only (for testing purposes) /// Insecure connection only (for testing purposes)
None, None,
/// Start with insecure connection and use `STARTTLS` when available /// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
Opportunistic(TlsParameters), Opportunistic(TlsParameters),
/// Start with insecure connection and require `STARTTLS` /// Start with insecure connection and require `STARTTLS`
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
Required(TlsParameters), Required(TlsParameters),
/// Use TLS wrapped connection /// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
Wrapper(TlsParameters), Wrapper(TlsParameters),
} }
@@ -43,31 +87,60 @@ impl Debug for Tls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self { match &self {
Self::None => f.pad("None"), Self::None => f.pad("None"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"), Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Required(_) => f.pad("Required"), Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"), Self::Wrapper(_) => f.pad("Wrapper"),
} }
} }
} }
/// Source for the base set of root certificates to trust.
#[allow(missing_copy_implementations)]
#[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
/// 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 = "rustls-tls")]
WebpkiRoots,
/// Don't use any system certificates.
None,
}
/// Parameters to use for secure clients /// Parameters to use for secure clients
#[derive(Clone)] #[derive(Clone)]
pub struct TlsParameters { pub struct TlsParameters {
pub(crate) connector: InnerTlsParameters, pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server /// The domain name which is expected in the TLS certificate from the server
pub(super) domain: String, pub(super) domain: String,
#[cfg(feature = "boring-tls")]
pub(super) accept_invalid_hostnames: bool,
} }
/// Builder for `TlsParameters` /// Builder for `TlsParameters`
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TlsParametersBuilder { pub struct TlsParametersBuilder {
domain: String, domain: String,
cert_store: CertificateStore,
root_certs: Vec<Certificate>, root_certs: Vec<Certificate>,
accept_invalid_hostnames: bool, accept_invalid_hostnames: bool,
accept_invalid_certs: bool, accept_invalid_certs: bool,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
min_tls_version: TlsVersion,
} }
impl TlsParametersBuilder { impl TlsParametersBuilder {
@@ -75,12 +148,21 @@ impl TlsParametersBuilder {
pub fn new(domain: String) -> Self { pub fn new(domain: String) -> Self {
Self { Self {
domain, domain,
cert_store: CertificateStore::Default,
root_certs: Vec::new(), root_certs: Vec::new(),
accept_invalid_hostnames: false, accept_invalid_hostnames: false,
accept_invalid_certs: false, accept_invalid_certs: false,
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
min_tls_version: TlsVersion::Tlsv12,
} }
} }
/// Set the source for the base set of root certificates to trust.
pub fn certificate_store(mut self, cert_store: CertificateStore) -> Self {
self.cert_store = cert_store;
self
}
/// Add a custom root certificate /// Add a custom root certificate
/// ///
/// Can be used to safely connect to a server using a self signed certificate, for example. /// Can be used to safely connect to a server using a self signed certificate, for example.
@@ -102,13 +184,22 @@ impl TlsParametersBuilder {
/// This method introduces significant vulnerabilities to man-in-the-middle attacks. /// This method introduces significant vulnerabilities to man-in-the-middle attacks.
/// ///
/// Hostname verification can only be disabled with the `native-tls` TLS backend. /// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(feature = "native-tls")] #[cfg(any(feature = "native-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] #[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self { pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames; self.accept_invalid_hostnames = accept_invalid_hostnames;
self self
} }
/// Controls which minimum TLS version is allowed
///
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
self.min_tls_version = min_tls_version;
self
}
/// Controls whether invalid certificates are accepted /// Controls whether invalid certificates are accepted
/// ///
/// Defaults to `false`. /// Defaults to `false`.
@@ -130,16 +221,20 @@ impl TlsParametersBuilder {
self self
} }
/// Creates a new `TlsParameters` using native-tls or rustls /// Creates a new `TlsParameters` using native-tls, boring-tls or rustls
/// depending on which one is available /// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn build(self) -> Result<TlsParameters, Error> { pub fn build(self) -> Result<TlsParameters, Error> {
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
return self.build_rustls(); return self.build_rustls();
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
#[cfg(not(feature = "rustls-tls"))]
return self.build_native(); return self.build_native();
#[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
return self.build_boring();
} }
/// Creates a new `TlsParameters` using native-tls with the provided configuration /// Creates a new `TlsParameters` using native-tls with the provided configuration
@@ -148,17 +243,93 @@ impl TlsParametersBuilder {
pub fn build_native(self) -> Result<TlsParameters, Error> { pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder(); let mut tls_builder = TlsConnector::builder();
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
tls_builder.disable_built_in_roots(true);
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in native tls"
)))
}
}
for cert in self.root_certs { for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls); tls_builder.add_root_certificate(cert.native_tls);
} }
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames); tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs); tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL)); let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => Protocol::Tlsv10,
TlsVersion::Tlsv11 => Protocol::Tlsv11,
TlsVersion::Tlsv12 => Protocol::Tlsv12,
TlsVersion::Tlsv13 => {
return Err(error::tls(
"min tls version Tlsv13 not supported in native tls",
))
}
};
tls_builder.min_protocol_version(Some(min_tls_version));
let connector = tls_builder.build().map_err(error::tls)?; let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector), connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain, domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
})
}
/// Creates a new `TlsParameters` using boring-tls with the provided configuration
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn build_boring(self) -> Result<TlsParameters, Error> {
use boring::ssl::{SslMethod, SslVerifyMode};
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
if self.accept_invalid_certs {
tls_builder.set_verify(SslVerifyMode::NONE);
} else {
match self.cert_store {
CertificateStore::Default => {}
CertificateStore::None => {
// Replace the default store with an empty store.
tls_builder
.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
}
#[allow(unreachable_patterns)]
other => {
return Err(error::tls(format!(
"{other:?} is not supported in boring tls"
)))
}
}
let cert_store = tls_builder.cert_store_mut();
for cert in self.root_certs {
cert_store.add_cert(cert.boring_tls).map_err(error::tls)?;
}
}
let min_tls_version = match self.min_tls_version {
TlsVersion::Tlsv10 => SslVersion::TLS1,
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
TlsVersion::Tlsv12 => SslVersion::TLS1_2,
TlsVersion::Tlsv13 => SslVersion::TLS1_3,
};
tls_builder
.set_min_proto_version(Some(min_tls_version))
.map_err(error::tls)?;
let connector = tls_builder.build();
Ok(TlsParameters {
connector: InnerTlsParameters::BoringTls(connector),
domain: self.domain,
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
@@ -167,26 +338,83 @@ impl TlsParametersBuilder {
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))] #[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> { pub fn build_rustls(self) -> Result<TlsParameters, Error> {
let tls = ClientConfig::builder(); let tls = ClientConfig::builder();
let tls = tls.with_safe_defaults();
let just_version3 = &[&rustls::version::TLS13];
let supported_versions = match self.min_tls_version {
TlsVersion::Tlsv10 => {
return Err(error::tls("min tls version Tlsv10 not supported in rustls"))
}
TlsVersion::Tlsv11 => {
return Err(error::tls("min tls version Tlsv11 not supported in rustls"))
}
TlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
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 = if self.accept_invalid_certs { let tls = if self.accept_invalid_certs {
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {})) tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
} else { } else {
let mut root_cert_store = RootCertStore::empty(); let mut root_cert_store = RootCertStore::empty();
#[cfg(feature = "rustls-native-certs")]
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
let mut valid_count = 0;
let mut invalid_count = 0;
for cert in native_certs {
match store.add(&rustls::Certificate(cert.0)) {
Ok(_) => valid_count += 1,
Err(err) => {
#[cfg(feature = "tracing")]
tracing::debug!("certificate parsing failed: {:?}", err);
invalid_count += 1;
}
}
}
#[cfg(feature = "tracing")]
tracing::debug!(
"loaded platform certs with {valid_count} valid and {invalid_count} invalid certs"
);
Ok(())
}
#[cfg(feature = "rustls-tls")]
fn load_webpki_roots(store: &mut RootCertStore) {
// TODO: handle this in the rustls 0.22 upgrade
#[allow(deprecated)]
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
}
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 cert in self.root_certs {
for rustls_cert in cert.rustls { for rustls_cert in cert.rustls {
root_cert_store.add(&rustls_cert).map_err(error::tls)?; root_cert_store.add(&rustls_cert).map_err(error::tls)?;
} }
} }
root_cert_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
|ta| {
OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
},
));
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new( tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
root_cert_store, root_cert_store,
@@ -198,27 +426,36 @@ impl TlsParametersBuilder {
Ok(TlsParameters { Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)), connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
domain: self.domain, domain: self.domain,
#[cfg(feature = "boring-tls")]
accept_invalid_hostnames: self.accept_invalid_hostnames,
}) })
} }
} }
#[derive(Clone)] #[derive(Clone)]
#[allow(clippy::enum_variant_names)]
pub enum InnerTlsParameters { pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
NativeTls(TlsConnector), NativeTls(TlsConnector),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
RustlsTls(Arc<ClientConfig>), RustlsTls(Arc<ClientConfig>),
#[cfg(feature = "boring-tls")]
BoringTls(SslConnector),
} }
impl TlsParameters { impl TlsParameters {
/// Creates a new `TlsParameters` using native-tls or rustls /// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available /// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn new(domain: String) -> Result<Self, Error> { pub fn new(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build() TlsParametersBuilder::new(domain).build()
} }
/// Creates a new `TlsParameters` builder
pub fn builder(domain: String) -> TlsParametersBuilder { pub fn builder(domain: String) -> TlsParametersBuilder {
TlsParametersBuilder::new(domain) TlsParametersBuilder::new(domain)
} }
@@ -237,6 +474,13 @@ impl TlsParameters {
TlsParametersBuilder::new(domain).build_rustls() TlsParametersBuilder::new(domain).build_rustls()
} }
/// Creates a new `TlsParameters` using boring
#[cfg(feature = "boring-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
pub fn new_boring(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_boring()
}
pub fn domain(&self) -> &str { pub fn domain(&self) -> &str {
&self.domain &self.domain
} }
@@ -250,20 +494,27 @@ pub struct Certificate {
native_tls: native_tls::Certificate, native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>, rustls: Vec<rustls::Certificate>,
#[cfg(feature = "boring-tls")]
boring_tls: boring::x509::X509,
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
impl Certificate { impl Certificate {
/// Create a `Certificate` from a DER encoded certificate /// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> { pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?; let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_der(&der).map_err(error::tls)?;
Ok(Self { Ok(Self {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)], rustls: vec![rustls::Certificate(der)],
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
}) })
} }
@@ -272,6 +523,9 @@ impl Certificate {
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?; let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "boring-tls")]
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
let rustls_cert = { let rustls_cert = {
use std::io::Cursor; use std::io::Cursor;
@@ -289,6 +543,8 @@ impl Certificate {
native_tls: native_tls_cert, native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
rustls: rustls_cert, rustls: rustls_cert,
#[cfg(feature = "boring-tls")]
boring_tls: boring_tls_cert,
}) })
} }
} }

View File

@@ -13,7 +13,7 @@ use crate::{
}; };
/// EHLO command /// EHLO command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ehlo { pub struct Ehlo {
client_id: ClientId, client_id: ClientId,
@@ -33,7 +33,7 @@ impl Ehlo {
} }
/// STARTTLS command /// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Starttls; pub struct Starttls;
@@ -44,7 +44,7 @@ impl Display for Starttls {
} }
/// MAIL command /// MAIL command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Mail { pub struct Mail {
sender: Option<Address>, sender: Option<Address>,
@@ -59,7 +59,7 @@ impl Display for Mail {
self.sender.as_ref().map_or("", |s| s.as_ref()) self.sender.as_ref().map_or("", |s| s.as_ref())
)?; )?;
for parameter in &self.parameters { for parameter in &self.parameters {
write!(f, " {}", parameter)?; write!(f, " {parameter}")?;
} }
f.write_str("\r\n") f.write_str("\r\n")
} }
@@ -73,7 +73,7 @@ impl Mail {
} }
/// RCPT command /// RCPT command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rcpt { pub struct Rcpt {
recipient: Address, recipient: Address,
@@ -84,7 +84,7 @@ impl Display for Rcpt {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?; write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters { for parameter in &self.parameters {
write!(f, " {}", parameter)?; write!(f, " {parameter}")?;
} }
f.write_str("\r\n") f.write_str("\r\n")
} }
@@ -101,7 +101,7 @@ impl Rcpt {
} }
/// DATA command /// DATA command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Data; pub struct Data;
@@ -112,7 +112,7 @@ impl Display for Data {
} }
/// QUIT command /// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Quit; pub struct Quit;
@@ -123,7 +123,7 @@ impl Display for Quit {
} }
/// NOOP command /// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Noop; pub struct Noop;
@@ -134,7 +134,7 @@ impl Display for Noop {
} }
/// HELP command /// HELP command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Help { pub struct Help {
argument: Option<String>, argument: Option<String>,
@@ -144,7 +144,7 @@ impl Display for Help {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("HELP")?; f.write_str("HELP")?;
if let Some(argument) = &self.argument { if let Some(argument) = &self.argument {
write!(f, " {}", argument)?; write!(f, " {argument}")?;
} }
f.write_str("\r\n") f.write_str("\r\n")
} }
@@ -158,7 +158,7 @@ impl Help {
} }
/// VRFY command /// VRFY command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vrfy { pub struct Vrfy {
argument: String, argument: String,
@@ -178,7 +178,7 @@ impl Vrfy {
} }
/// EXPN command /// EXPN command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Expn { pub struct Expn {
argument: String, argument: String,
@@ -198,7 +198,7 @@ impl Expn {
} }
/// RSET command /// RSET command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rset; pub struct Rset;
@@ -209,7 +209,7 @@ impl Display for Rset {
} }
/// AUTH command /// AUTH command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Auth { pub struct Auth {
mechanism: Mechanism, mechanism: Mechanism,
@@ -220,7 +220,7 @@ pub struct Auth {
impl Display for Auth { impl Display for Auth {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let encoded_response = self.response.as_ref().map(base64::encode); let encoded_response = self.response.as_ref().map(crate::base64::encode);
if self.mechanism.supports_initial_response() { if self.mechanism.supports_initial_response() {
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?; write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
@@ -271,7 +271,7 @@ impl Auth {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("auth encoded challenge: {}", encoded_challenge); tracing::debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_base64 = base64::decode(&encoded_challenge).map_err(error::response)?; let decoded_base64 = crate::base64::decode(encoded_challenge).map_err(error::response)?;
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?; let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("auth decoded challenge: {}", decoded_challenge); tracing::debug!("auth decoded challenge: {}", decoded_challenge);
@@ -296,15 +296,15 @@ mod test {
#[test] #[test]
fn test_display() { fn test_display() {
let id = ClientId::Domain("localhost".to_string()); let id = ClientId::Domain("localhost".to_owned());
let email = Address::from_str("test@example.com").unwrap(); let email = Address::from_str("test@example.com").unwrap();
let mail_parameter = MailParameter::Other { let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(), keyword: "TEST".to_owned(),
value: Some("value".to_string()), value: Some("value".to_owned()),
}; };
let rcpt_parameter = RcptParameter::Other { let rcpt_parameter = RcptParameter::Other {
keyword: "TEST".to_string(), keyword: "TEST".to_owned(),
value: Some("value".to_string()), value: Some("value".to_owned()),
}; };
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n"); assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
assert_eq!( assert_eq!(
@@ -341,24 +341,18 @@ mod test {
format!("{}", Rcpt::new(email, vec![rcpt_parameter])), format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n" "RCPT TO:<test@example.com> TEST=value\r\n"
); );
assert_eq!(format!("{}", Quit), "QUIT\r\n"); assert_eq!(format!("{Quit}"), "QUIT\r\n");
assert_eq!(format!("{}", Data), "DATA\r\n"); assert_eq!(format!("{Data}"), "DATA\r\n");
assert_eq!(format!("{}", Noop), "NOOP\r\n"); assert_eq!(format!("{Noop}"), "NOOP\r\n");
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n"); assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
assert_eq!( assert_eq!(
format!("{}", Help::new(Some("test".to_string()))), format!("{}", Help::new(Some("test".to_owned()))),
"HELP test\r\n" "HELP test\r\n"
); );
assert_eq!( assert_eq!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
format!("{}", Vrfy::new("test".to_string())), assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\r\n");
"VRFY test\r\n" assert_eq!(format!("{Rset}"), "RSET\r\n");
); let credentials = Credentials::new("user".to_owned(), "password".to_owned());
assert_eq!(
format!("{}", Expn::new("test".to_string())),
"EXPN test\r\n"
);
assert_eq!(format!("{}", Rset), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string());
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",

View File

@@ -0,0 +1,120 @@
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 credentials = Credentials::new(connection_url.username().into(), password.into());
builder = builder.credentials(credentials);
}
Ok(builder)
}

View File

@@ -68,8 +68,11 @@ impl Error {
} }
/// Returns true if the error is from TLS /// Returns true if the error is from TLS
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn is_tls(&self) -> bool { pub fn is_tls(&self) -> bool {
matches!(self.inner.kind, Kind::Tls) matches!(self.inner.kind, Kind::Tls)
} }
@@ -102,8 +105,11 @@ pub(crate) enum Kind {
/// Underlying network i/o error /// Underlying network i/o error
Network, Network,
/// TLS error /// TLS error
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls, Tls,
} }
@@ -128,18 +134,18 @@ impl fmt::Display for Error {
Kind::Client => f.write_str("internal client error")?, Kind::Client => f.write_str("internal client error")?,
Kind::Network => f.write_str("network error")?, Kind::Network => f.write_str("network error")?,
Kind::Connection => f.write_str("Connection error")?, Kind::Connection => f.write_str("Connection error")?,
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Kind::Tls => f.write_str("tls error")?, Kind::Tls => f.write_str("tls error")?,
Kind::Transient(ref code) => { Kind::Transient(ref code) => {
write!(f, "transient error ({})", code)?; write!(f, "transient error ({code})")?;
} }
Kind::Permanent(ref code) => { Kind::Permanent(ref code) => {
write!(f, "permanent error ({})", code)?; write!(f, "permanent error ({code})")?;
} }
}; };
if let Some(ref e) = self.inner.source { if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?; write!(f, ": {e}")?;
} }
Ok(()) Ok(())
@@ -179,7 +185,7 @@ pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Connection, Some(e)) Error::new(Kind::Connection, Some(e))
} }
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error { pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Tls, Some(e)) Error::new(Kind::Tls, Some(e))
} }

View File

@@ -55,8 +55,8 @@ impl Display for ClientId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match *self {
Self::Domain(ref value) => f.write_str(value), Self::Domain(ref value) => f.write_str(value),
Self::Ipv4(ref value) => write!(f, "[{}]", value), Self::Ipv4(ref value) => write!(f, "[{value}]"),
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value), Self::Ipv6(ref value) => write!(f, "[IPv6:{value}]"),
} }
} }
} }
@@ -97,7 +97,7 @@ impl Display for Extension {
Extension::EightBitMime => f.write_str("8BITMIME"), Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"), Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
Extension::StartTls => f.write_str("STARTTLS"), Extension::StartTls => f.write_str("STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism), Extension::Authentication(ref mechanism) => write!(f, "AUTH {mechanism}"),
} }
} }
} }
@@ -119,7 +119,7 @@ pub struct ServerInfo {
impl Display for ServerInfo { impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let features = if self.features.is_empty() { let features = if self.features.is_empty() {
"no supported features".to_string() "no supported features".to_owned()
} else { } else {
format!("{:?}", self.features) format!("{:?}", self.features)
}; };
@@ -174,7 +174,7 @@ impl ServerInfo {
} }
Ok(ServerInfo { Ok(ServerInfo {
name: name.to_string(), name: name.to_owned(),
features, features,
}) })
} }
@@ -190,7 +190,7 @@ impl ServerInfo {
.contains(&Extension::Authentication(mechanism)) .contains(&Extension::Authentication(mechanism))
} }
/// Gets a compatible mechanism from list /// Gets a compatible mechanism from a list
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> { pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
for mechanism in mechanisms { for mechanism in mechanisms {
if self.supports_auth_mechanism(*mechanism) { if self.supports_auth_mechanism(*mechanism) {
@@ -228,8 +228,8 @@ pub enum MailParameter {
impl Display for MailParameter { impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self { match *self {
MailParameter::Body(ref value) => write!(f, "BODY={}", value), MailParameter::Body(ref value) => write!(f, "BODY={value}"),
MailParameter::Size(size) => write!(f, "SIZE={}", size), MailParameter::Size(size) => write!(f, "SIZE={size}"),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"), MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other { MailParameter::Other {
ref keyword, ref keyword,
@@ -281,7 +281,7 @@ impl Display for RcptParameter {
RcptParameter::Other { RcptParameter::Other {
ref keyword, ref keyword,
value: Some(ref value), value: Some(ref value),
} => write!(f, "{}={}", keyword, XText(value)), } => write!(f, "{keyword}={}", XText(value)),
RcptParameter::Other { RcptParameter::Other {
ref keyword, ref keyword,
value: None, value: None,
@@ -304,21 +304,21 @@ mod test {
#[test] #[test]
fn test_clientid_fmt() { fn test_clientid_fmt() {
assert_eq!( assert_eq!(
format!("{}", ClientId::Domain("test".to_string())), format!("{}", ClientId::Domain("test".to_owned())),
"test".to_string() "test".to_owned()
); );
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string()); assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_owned());
} }
#[test] #[test]
fn test_extension_fmt() { fn test_extension_fmt() {
assert_eq!( assert_eq!(
format!("{}", Extension::EightBitMime), format!("{}", Extension::EightBitMime),
"8BITMIME".to_string() "8BITMIME".to_owned()
); );
assert_eq!( assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)), format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string() "AUTH PLAIN".to_owned()
); );
} }
@@ -331,11 +331,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: eightbitmime, features: eightbitmime,
} }
), ),
"name with {EightBitMime}".to_string() "name with {EightBitMime}".to_owned()
); );
let empty = HashSet::new(); let empty = HashSet::new();
@@ -344,11 +344,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: empty, features: empty,
} }
), ),
"name with no supported features".to_string() "name with no supported features".to_owned()
); );
let mut plain = HashSet::new(); let mut plain = HashSet::new();
@@ -358,11 +358,11 @@ mod test {
format!( format!(
"{}", "{}",
ServerInfo { ServerInfo {
name: "name".to_string(), name: "name".to_owned(),
features: plain, features: plain,
} }
), ),
"name with {Authentication(Plain)}".to_string() "name with {Authentication(Plain)}".to_owned()
); );
} }
@@ -374,18 +374,14 @@ mod test {
Category::Unspecified4, Category::Unspecified4,
Detail::One, Detail::One,
), ),
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
); );
let mut features = HashSet::new(); let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime)); assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo { let server_info = ServerInfo {
name: "me".to_string(), name: "me".to_owned(),
features, features,
}; };
@@ -401,10 +397,10 @@ mod test {
Detail::One, Detail::One,
), ),
vec![ vec![
"me".to_string(), "me".to_owned(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(), "AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
); );
@@ -414,7 +410,7 @@ mod test {
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),)); assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo { let server_info2 = ServerInfo {
name: "me".to_string(), name: "me".to_owned(),
features: features2, features: features2,
}; };

View File

@@ -77,8 +77,8 @@
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")? //! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication //! // Add credentials for authentication
//! .credentials(Credentials::new( //! .credentials(Credentials::new(
//! "username".to_string(), //! "username".to_owned(),
//! "password".to_string(), //! "password".to_owned(),
//! )) //! ))
//! // Configure expected authentication mechanism //! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain]) //! .authentication(vec![Mechanism::Plain])
@@ -111,7 +111,7 @@
//! .body(String::from("Be happy!"))?; //! .body(String::from("Be happy!"))?;
//! //!
//! // Custom TLS configuration //! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_string()) //! let tls = TlsParameters::builder("smtp.example.com".to_owned())
//! .dangerous_accept_invalid_certs(true) //! .dangerous_accept_invalid_certs(true)
//! .build()?; //! .build()?;
//! //!
@@ -140,7 +140,7 @@ pub use self::{
error::Error, error::Error,
transport::{SmtpTransport, SmtpTransportBuilder}, transport::{SmtpTransport, SmtpTransportBuilder},
}; };
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use crate::transport::smtp::client::TlsParameters; use crate::transport::smtp::client::TlsParameters;
use crate::transport::smtp::{ use crate::transport::smtp::{
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS}, authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
@@ -154,6 +154,7 @@ mod async_transport;
pub mod authentication; pub mod authentication;
pub mod client; pub mod client;
pub mod commands; pub mod commands;
mod connection_url;
mod error; mod error;
pub mod extension; pub mod extension;
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
@@ -200,7 +201,7 @@ struct SmtpInfo {
impl Default for SmtpInfo { impl Default for SmtpInfo {
fn default() -> Self { fn default() -> Self {
Self { Self {
server: "localhost".to_string(), server: "localhost".to_owned(),
port: SMTP_PORT, port: SMTP_PORT,
hello_name: ClientId::default(), hello_name: ClientId::default(),
credentials: None, credentials: None,

View File

@@ -158,14 +158,14 @@ impl<E: Executor> Pool<E> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("reusing a pooled connection"); tracing::debug!("reusing a pooled connection");
return Ok(PooledConnection::wrap(conn, self.clone())); return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
} }
None => { None => {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("creating a new connection"); tracing::debug!("creating a new connection");
let conn = self.client.connection().await?; let conn = self.client.connection().await?;
return Ok(PooledConnection::wrap(conn, self.clone())); return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
} }
} }
} }
@@ -203,7 +203,7 @@ impl<E: Executor> Debug for Pool<E> {
&match self.connections.try_lock() { &match self.connections.try_lock() {
Some(connections) => format!("{} connections", connections.len()), Some(connections) => format!("{} connections", connections.len()),
None => "LOCKED".to_string(), None => "LOCKED".to_owned(),
}, },
) )
.field("client", &self.client) .field("client", &self.client)

View File

@@ -141,14 +141,14 @@ impl Pool {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("reusing a pooled connection"); tracing::debug!("reusing a pooled connection");
return Ok(PooledConnection::wrap(conn, self.clone())); return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
} }
None => { None => {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
tracing::debug!("creating a new connection"); tracing::debug!("creating a new connection");
let conn = self.client.connection()?; let conn = self.client.connection()?;
return Ok(PooledConnection::wrap(conn, self.clone())); return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
} }
} }
} }
@@ -186,8 +186,8 @@ impl Debug for Pool {
&match self.connections.try_lock() { &match self.connections.try_lock() {
Ok(connections) => format!("{} connections", connections.len()), Ok(connections) => format!("{} connections", connections.len()),
Err(TryLockError::WouldBlock) => "LOCKED".to_string(), Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
Err(TryLockError::Poisoned(_)) => "POISONED".to_string(), Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
}, },
) )
.field("client", &self.client) .field("client", &self.client)

View File

@@ -19,7 +19,7 @@ use nom::{
use crate::transport::smtp::{error, Error}; use crate::transport::smtp::{error, Error};
/// First digit indicates severity /// The first digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)] #[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity { pub enum Severity {
@@ -151,7 +151,7 @@ impl FromStr for Response {
fn from_str(s: &str) -> result::Result<Response, Error> { fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s) parse_response(s)
.map(|(_, r)| r) .map(|(_, r)| r)
.map_err(|e| error::response(e.to_string())) .map_err(|e| error::response(e.to_owned()))
} }
} }
@@ -329,10 +329,10 @@ mod test {
detail: Detail::Zero, detail: Detail::Zero,
}, },
message: vec![ message: vec![
"me".to_string(), "me".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
"AUTH PLAIN CRAM-MD5".to_string(), "AUTH PLAIN CRAM-MD5".to_owned(),
], ],
} }
); );
@@ -352,11 +352,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::Zero, detail: Detail::Zero,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.is_positive()); .is_positive());
assert!(!Response::new( assert!(!Response::new(
@@ -365,11 +361,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::Zero, detail: Detail::Zero,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.is_positive()); .is_positive());
} }
@@ -382,11 +374,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.has_code(451)); .has_code(451));
assert!(!Response::new( assert!(!Response::new(
@@ -395,11 +383,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.has_code(251)); .has_code(251));
} }
@@ -413,11 +397,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.first_word(), .first_word(),
Some("me") Some("me")
@@ -430,9 +410,9 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec![
"me mo".to_string(), "me mo".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
) )
.first_word(), .first_word(),
@@ -457,7 +437,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -469,7 +449,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -481,7 +461,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec!["".to_string()], vec!["".to_owned()],
) )
.first_word(), .first_word(),
None None
@@ -494,7 +474,7 @@ mod test {
let res = parse_response(raw_response); let res = parse_response(raw_response);
match res { match res {
Err(nom::Err::Incomplete(_)) => {} Err(nom::Err::Incomplete(_)) => {}
_ => panic!("Expected incomplete response, got {:?}", res), _ => panic!("Expected incomplete response, got {res:?}"),
} }
} }
@@ -507,11 +487,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
) )
.first_line(), .first_line(),
Some("me") Some("me")
@@ -524,9 +500,9 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![ vec![
"me mo".to_string(), "me mo".to_owned(),
"8BITMIME".to_string(), "8BITMIME".to_owned(),
"SIZE 42".to_string(), "SIZE 42".to_owned(),
], ],
) )
.first_line(), .first_line(),
@@ -551,7 +527,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_line(), .first_line(),
Some(" ") Some(" ")
@@ -563,7 +539,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_owned()],
) )
.first_line(), .first_line(),
Some(" ") Some(" ")
@@ -575,7 +551,7 @@ mod test {
category: Category::MailSystem, category: Category::MailSystem,
detail: Detail::One, detail: Detail::One,
}, },
vec!["".to_string()], vec!["".to_owned()],
) )
.first_line(), .first_line(),
Some("") Some("")

View File

@@ -7,7 +7,7 @@ use super::pool::sync_impl::Pool;
#[cfg(feature = "pool")] #[cfg(feature = "pool")]
use super::PoolConfig; use super::PoolConfig;
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo}; use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT}; use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::{address::Envelope, Transport}; use crate::{address::Envelope, Transport};
@@ -45,8 +45,11 @@ impl SmtpTransport {
/// ///
/// Creates an encrypted transport over submissions port, using the provided domain /// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates. /// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> { pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?; let tls_parameters = TlsParameters::new(relay.into())?;
@@ -55,7 +58,7 @@ impl SmtpTransport {
.tls(Tls::Wrapper(tls_parameters))) .tls(Tls::Wrapper(tls_parameters)))
} }
/// Simple an secure transport, using STARTTLS to obtain encrypted connections /// Simple and secure transport, using STARTTLS to obtain encrypted connections
/// ///
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers /// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections. /// that don't take SMTPS connections.
@@ -66,8 +69,11 @@ impl SmtpTransport {
/// ///
/// An error is returned if the connection can't be upgraded. No credentials /// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks. /// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> { pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?; let tls_parameters = TlsParameters::new(relay.into())?;
@@ -89,29 +95,106 @@ impl SmtpTransport {
/// ///
/// * No authentication /// * No authentication
/// * No TLS /// * No TLS
/// * A 60 seconds timeout for smtp commands /// * A 60-seconds timeout for smtp commands
/// * Port 25 /// * Port 25
/// ///
/// Consider using [`SmtpTransport::relay`](#method.relay) or /// Consider using [`SmtpTransport::relay`](#method.relay) or
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead, /// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible. /// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder { pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let new = SmtpInfo { SmtpTransportBuilder::new(server)
server: server.into(), }
..Default::default()
};
SmtpTransportBuilder { /// Creates a `SmtpTransportBuilder` from a connection URL
info: new, ///
#[cfg(feature = "pool")] /// The protocol, credentials, host and port can be provided in a single URL.
pool_config: PoolConfig::default(), /// 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 /// Tests the SMTP connection
/// ///
/// `test_connection()` tests the connection by using the SMTP NOOP command. /// `test_connection()` tests the connection by using the SMTP NOOP command.
/// The connection is closed afterwards if a connection pool is not used. /// The connection is closed afterward if a connection pool is not used.
pub fn test_connection(&self) -> Result<bool, Error> { pub fn test_connection(&self) -> Result<bool, Error> {
let mut conn = self.inner.connection()?; let mut conn = self.inner.connection()?;
@@ -135,6 +218,20 @@ pub struct SmtpTransportBuilder {
/// Builder for the SMTP `SmtpTransport` /// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder { impl SmtpTransportBuilder {
// Create new builder with default parameters
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
let new = SmtpInfo {
server: server.into(),
..Default::default()
};
Self {
info: new,
#[cfg(feature = "pool")]
pool_config: PoolConfig::default(),
}
}
/// Set the name used during EHLO /// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self { pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name; self.info.hello_name = name;
@@ -166,8 +263,11 @@ impl SmtpTransportBuilder {
} }
/// Set the TLS settings to use /// Set the TLS settings to use
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))] #[cfg_attr(
docsrs,
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
)]
pub fn tls(mut self, tls: Tls) -> Self { pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls; self.info.tls = tls;
self self
@@ -185,7 +285,7 @@ impl SmtpTransportBuilder {
/// Build the transport /// Build the transport
/// ///
/// If the `pool` feature is enabled an `Arc` wrapped pool is be created. /// If the `pool` feature is enabled, an `Arc` wrapped pool is created.
/// Defaults can be found at [`PoolConfig`] /// Defaults can be found at [`PoolConfig`]
pub fn build(self) -> SmtpTransport { pub fn build(self) -> SmtpTransport {
let client = SmtpClient { info: self.info }; let client = SmtpClient { info: self.info };
@@ -210,7 +310,7 @@ impl SmtpClient {
pub fn connection(&self) -> Result<SmtpConnection, Error> { pub fn connection(&self) -> Result<SmtpConnection, Error> {
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
let tls_parameters = match self.info.tls { let tls_parameters = match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters), Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None, _ => None,
}; };
@@ -221,9 +321,10 @@ impl SmtpClient {
self.info.timeout, self.info.timeout,
&self.info.hello_name, &self.info.hello_name,
tls_parameters, tls_parameters,
None,
)?; )?;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] #[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
match self.info.tls { match self.info.tls {
Tls::Opportunistic(ref tls_parameters) => { Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() { if conn.can_starttls() {
@@ -242,3 +343,62 @@ impl SmtpClient {
Ok(conn) Ok(conn)
} }
} }
#[cfg(test)]
mod tests {
use crate::{
transport::smtp::{authentication::Credentials, client::Tls},
SmtpTransport,
};
#[test]
fn transport_from_url() {
let builder = SmtpTransport::from_url("smtp://127.0.0.1:2525").unwrap();
assert_eq!(builder.info.port, 2525);
assert!(matches!(builder.info.tls, Tls::None));
assert_eq!(builder.info.server, "127.0.0.1");
let builder =
SmtpTransport::from_url("smtps://username:password@smtp.example.com:465").unwrap();
assert_eq!(builder.info.port, 465);
assert_eq!(
builder.info.credentials,
Some(Credentials::new(
"username".to_owned(),
"password".to_owned()
))
);
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
assert_eq!(builder.info.server, "smtp.example.com");
let builder =
SmtpTransport::from_url("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(_)));
}
}

View File

@@ -41,7 +41,7 @@ mod tests {
] ]
.iter() .iter()
{ {
assert_eq!(format!("{}", XText(input)), expect.to_string()); assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
} }
} }
} }

View File

@@ -53,6 +53,8 @@ use futures_util::lock::Mutex as FuturesMutex;
use crate::AsyncTransport; use crate::AsyncTransport;
use crate::{address::Envelope, Transport}; use crate::{address::Envelope, Transport};
/// An error returned by the stub transport
#[non_exhaustive]
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct Error; pub struct Error;

View File

@@ -4,7 +4,7 @@ Reply-To: Yuin <yuin@domain.tld>
To: Hei <hei@domain.tld> To: Hei <hei@domain.tld>
Subject: Happy new year Subject: Happy new year
MIME-Version: 1.0 MIME-Version: 1.0
Content-Type: multipart/related; Content-Type: multipart/related;
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA" boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1 --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1

View File

@@ -33,7 +33,7 @@ mod sync {
let result = sender.send(&email); let result = sender.send(&email);
let id = result.unwrap(); let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id)); let eml_file = temp_dir().join(format!("{id}.eml"));
let eml = read_to_string(&eml_file).unwrap(); let eml = read_to_string(&eml_file).unwrap();
assert_eq!( assert_eq!(
@@ -68,10 +68,10 @@ mod sync {
let result = sender.send(&email); let result = sender.send(&email);
let id = result.unwrap(); let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id)); let eml_file = temp_dir().join(format!("{id}.eml"));
let eml = read_to_string(&eml_file).unwrap(); let eml = read_to_string(&eml_file).unwrap();
let json_file = temp_dir().join(format!("{}.json", id)); let json_file = temp_dir().join(format!("{id}.json"));
let json = read_to_string(&json_file).unwrap(); let json = read_to_string(&json_file).unwrap();
assert_eq!( assert_eq!(
@@ -131,7 +131,7 @@ mod tokio_1 {
let result = sender.send(email).await; let result = sender.send(email).await;
let id = result.unwrap(); let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id)); let eml_file = temp_dir().join(format!("{id}.eml"));
let eml = read_to_string(&eml_file).unwrap(); let eml = read_to_string(&eml_file).unwrap();
assert_eq!( assert_eq!(
@@ -182,7 +182,7 @@ mod asyncstd_1 {
let result = sender.send(email).await; let result = sender.send(email).await;
let id = result.unwrap(); let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id)); let eml_file = temp_dir().join(format!("{id}.eml"));
let eml = read_to_string(&eml_file).unwrap(); let eml = read_to_string(&eml_file).unwrap();
assert_eq!( assert_eq!(

View File

@@ -15,7 +15,7 @@ mod sync {
.unwrap(); .unwrap();
let result = sender.send(&email); let result = sender.send(&email);
println!("{:?}", result); println!("{result:?}");
assert!(result.is_ok()); assert!(result.is_ok());
} }
} }
@@ -42,7 +42,7 @@ mod tokio_1 {
.unwrap(); .unwrap();
let result = sender.send(email).await; let result = sender.send(email).await;
println!("{:?}", result); println!("{result:?}");
assert!(result.is_ok()); assert!(result.is_ok());
} }
} }
@@ -68,7 +68,7 @@ mod asyncstd_1 {
.unwrap(); .unwrap();
let result = sender.send(email).await; let result = sender.send(email).await;
println!("{:?}", result); println!("{result:?}");
assert!(result.is_ok()); assert!(result.is_ok());
} }
} }

View File

@@ -18,7 +18,7 @@ mod sync {
sender_ok.send(&email).unwrap(); sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err(); sender_ko.send(&email).unwrap_err();
let expected_messages = vec![( let expected_messages = [(
email.envelope().clone(), email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(), String::from_utf8(email.formatted()).unwrap(),
)]; )];
@@ -47,7 +47,7 @@ mod tokio_1 {
sender_ok.send(email.clone()).await.unwrap(); sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email.clone()).await.unwrap_err(); sender_ko.send(email.clone()).await.unwrap_err();
let expected_messages = vec![( let expected_messages = [(
email.envelope().clone(), email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(), String::from_utf8(email.formatted()).unwrap(),
)]; )];
@@ -75,7 +75,7 @@ mod asyncstd_1 {
sender_ok.send(email.clone()).await.unwrap(); sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email.clone()).await.unwrap_err(); sender_ko.send(email.clone()).await.unwrap_err();
let expected_messages = vec![( let expected_messages = [(
email.envelope().clone(), email.envelope().clone(),
String::from_utf8(email.formatted()).unwrap(), String::from_utf8(email.formatted()).unwrap(),
)]; )];