Compare commits
221 Commits
v0.10.0-be
...
v0.11.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d3ebfab1a | ||
|
|
6fb69086fb | ||
|
|
dfdf3a61d2 | ||
|
|
e30ac2dbff | ||
|
|
22dca340a7 | ||
|
|
c7d1f35676 | ||
|
|
eebea56f16 | ||
|
|
851d6ae164 | ||
|
|
6f38e6b9a9 | ||
|
|
c40af78809 | ||
|
|
6d2e0d5046 | ||
|
|
c64cb0ff2e | ||
|
|
10d7b197ed | ||
|
|
fb54855d5f | ||
|
|
157c4fb5ae | ||
|
|
1196e332ee | ||
|
|
75770f7bc6 | ||
|
|
76d0929c94 | ||
|
|
c3d00051b2 | ||
|
|
12580d82f4 | ||
|
|
f7849078b8 | ||
|
|
f2c94cdf4d | ||
|
|
74f64b81ab | ||
|
|
39c71dbfd2 | ||
|
|
c1bf5dfda1 | ||
|
|
1c1fef8055 | ||
|
|
1540f16015 | ||
|
|
330daa1173 | ||
|
|
47f2fe0750 | ||
|
|
8b6cee30ee | ||
|
|
62c16e90ef | ||
|
|
e0494a5f9d | ||
|
|
8c3bffa728 | ||
|
|
47eda90433 | ||
|
|
46ea8c48ac | ||
|
|
5f7063fdc3 | ||
|
|
61c1f6bc6f | ||
|
|
283e21f8d6 | ||
|
|
20c3701eb0 | ||
|
|
74117d5cc6 | ||
|
|
bb49e0a46b | ||
|
|
42365478c2 | ||
|
|
94769242d1 | ||
|
|
7e6ffe8aea | ||
|
|
16c35ef583 | ||
|
|
bbab86b484 | ||
|
|
b5652f18b7 | ||
|
|
c2f2b907a9 | ||
|
|
a1cc770613 | ||
|
|
57886c367d | ||
|
|
f3a469431e | ||
|
|
9b48ef355b | ||
|
|
7fee8dc5a8 | ||
|
|
7e9fff9bd0 | ||
|
|
92f5460132 | ||
|
|
cd0c032f71 | ||
|
|
f41c9c19ab | ||
|
|
cb6a7178d9 | ||
|
|
2bfc759aa3 | ||
|
|
89673d0eb2 | ||
|
|
8b588cf275 | ||
|
|
5f37b66352 | ||
|
|
69e5974024 | ||
|
|
4fb67a7da1 | ||
|
|
9041f210f4 | ||
|
|
77b7d40fb8 | ||
|
|
2b6d457f85 | ||
|
|
952c1b39df | ||
|
|
7ecb87f9fd | ||
|
|
fd700b1717 | ||
|
|
f8f19d6af5 | ||
|
|
cc25223914 | ||
|
|
750573d38b | ||
|
|
0734a96343 | ||
|
|
3c2f996856 | ||
|
|
9cae29dd07 | ||
|
|
e1a146c8f8 | ||
|
|
840a19784a | ||
|
|
5a61ba36b5 | ||
|
|
dbf0e53c31 | ||
|
|
c914a07379 | ||
|
|
2c4fa39523 | ||
|
|
28f0af16be | ||
|
|
f0614be555 | ||
|
|
a3fcdf263d | ||
|
|
d4da2e1f14 | ||
|
|
5655958288 | ||
|
|
11b4acf0cd | ||
|
|
b3b5df285a | ||
|
|
3c051d52e7 | ||
|
|
d6128a146e | ||
|
|
fab6680150 | ||
|
|
0c9fc6cb71 | ||
|
|
2228cbdf93 | ||
|
|
17c95b0fa8 | ||
|
|
62725af00a | ||
|
|
758bf1a4a7 | ||
|
|
054c79f914 | ||
|
|
985fa7edc4 | ||
|
|
9004d4ccc5 | ||
|
|
10171f8c75 | ||
|
|
99e805952d | ||
|
|
2d21dde5a1 | ||
|
|
6fec936c0c | ||
|
|
22dfa5aa96 | ||
|
|
44e4cfd622 | ||
|
|
7ea3d38a00 | ||
|
|
73b89f5a9f | ||
|
|
1ec1b705c9 | ||
|
|
e4006518fe | ||
|
|
b33dd562fc | ||
|
|
65958df14f | ||
|
|
50628af5fd | ||
|
|
cf858cc682 | ||
|
|
f9a4b5ba89 | ||
|
|
1391a834ce | ||
|
|
e6b4529896 | ||
|
|
ca5cb3f8f7 | ||
|
|
1e2279457e | ||
|
|
961364cc29 | ||
|
|
b0db759e5f | ||
|
|
5daf5d397a | ||
|
|
3f1647fa48 | ||
|
|
fd106d9b0c | ||
|
|
c1d37d54b4 | ||
|
|
efa0d58778 | ||
|
|
9567b23f4d | ||
|
|
f77376fa19 | ||
|
|
6e35b9b30d | ||
|
|
c24213c850 | ||
|
|
8b40e438fd | ||
|
|
e1462b2d1b | ||
|
|
96b42515cd | ||
|
|
1ea4987023 | ||
|
|
9273d24e54 | ||
|
|
7a0dd5bd92 | ||
|
|
9a8aa46dba | ||
|
|
0377ea29b7 | ||
|
|
89e5b9083e | ||
|
|
8c370e28c9 | ||
|
|
3eed80ef30 | ||
|
|
dbb135c533 | ||
|
|
4c5f02b4f6 | ||
|
|
f02542841c | ||
|
|
29c34adc25 | ||
|
|
5e3ebbb189 | ||
|
|
60399a93cc | ||
|
|
a48bc8a1b2 | ||
|
|
94cc0149d1 | ||
|
|
a89383cdb6 | ||
|
|
592593f4b8 | ||
|
|
97d3c760c0 | ||
|
|
8f28b0c341 | ||
|
|
dc9c5df210 | ||
|
|
c9b3fa0baa | ||
|
|
addf8754dd | ||
|
|
af157c5f26 | ||
|
|
3e8988ae55 | ||
|
|
941a00bcaa | ||
|
|
14079bff8c | ||
|
|
696c06e8d7 | ||
|
|
d4f7618898 | ||
|
|
e0a0a2e624 | ||
|
|
9ab6bb56d3 | ||
|
|
e1d3778329 | ||
|
|
623d69c553 | ||
|
|
55c2618201 | ||
|
|
9f550bce86 | ||
|
|
e875d9ff64 | ||
|
|
aadcc0f83c | ||
|
|
b534a18017 | ||
|
|
0684bccd47 | ||
|
|
4471759221 | ||
|
|
ed454819ee | ||
|
|
47cad567b0 | ||
|
|
b0e2fc9bca | ||
|
|
1d8249165c | ||
|
|
98fc0cb2f3 | ||
|
|
0439bab874 | ||
|
|
504fc51b26 | ||
|
|
d54343cf00 | ||
|
|
904789ac3d | ||
|
|
94cae6df0d | ||
|
|
f17dccc46d | ||
|
|
7e7f05eb45 | ||
|
|
99df9e8d7c | ||
|
|
1b5109b6ac | ||
|
|
a4be3c4cd8 | ||
|
|
4586f2ad8a | ||
|
|
31de9e508b | ||
|
|
69334fe5eb | ||
|
|
2ad2444183 | ||
|
|
8afa442e93 | ||
|
|
486e0f9d50 | ||
|
|
acc4ff4898 | ||
|
|
1728d57c34 | ||
|
|
53bfb65423 | ||
|
|
61b08814c9 | ||
|
|
0e74042b4e | ||
|
|
29affe9398 | ||
|
|
b10f6ff8de | ||
|
|
2002a9d75a | ||
|
|
1193e1134d | ||
|
|
7c6ade7afe | ||
|
|
3bc729ca64 | ||
|
|
f041c00df7 | ||
|
|
fe8dc4967d | ||
|
|
137566a4e4 | ||
|
|
216c612931 | ||
|
|
a429a24913 | ||
|
|
648bf2b2f6 | ||
|
|
509a623a27 | ||
|
|
a681c6b49d | ||
|
|
22efe341fe | ||
|
|
97fba6a47e | ||
|
|
f7066ac858 | ||
|
|
9379f2e328 | ||
|
|
05133a7102 | ||
|
|
d7d05bf48a | ||
|
|
34ac265d60 | ||
|
|
bbf56de83d |
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
61
.github/workflows/test.yml
vendored
61
.github/workflows/test.yml
vendored
@@ -7,11 +7,13 @@ on:
|
||||
- master
|
||||
|
||||
env:
|
||||
RUSTFLAGS: "--cfg lettre_ignore_tls_mismatch"
|
||||
RUSTDOCFLAGS: "--cfg lettre_ignore_tls_mismatch"
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: rustfmt / stable
|
||||
name: rustfmt / nightly-2023-06-22
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -20,7 +22,7 @@ jobs:
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup default nightly-2023-06-22
|
||||
rustup component add rustfmt
|
||||
|
||||
- name: cargo fmt
|
||||
@@ -50,18 +52,12 @@ jobs:
|
||||
- name: Checkout
|
||||
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
|
||||
run: rustup update --no-self-update stable
|
||||
|
||||
- name: Setup cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install cargo hack
|
||||
run: cargo install cargo-hack --debug
|
||||
|
||||
@@ -79,27 +75,21 @@ jobs:
|
||||
rust: stable
|
||||
- name: beta
|
||||
rust: beta
|
||||
- name: 1.45.2
|
||||
rust: 1.45.2
|
||||
- name: '1.70'
|
||||
rust: '1.70'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
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
|
||||
run: |
|
||||
rustup default ${{ matrix.rust }}
|
||||
rustup update --no-self-update ${{ matrix.rust }}
|
||||
|
||||
- name: Setup cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install postfix
|
||||
run: |
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update
|
||||
@@ -108,14 +98,37 @@ jobs:
|
||||
- name: Run SMTP server
|
||||
run: smtp-sink 2525 1000&
|
||||
|
||||
- name: Install coredns
|
||||
run: |
|
||||
wget -q https://github.com/coredns/coredns/releases/download/v1.8.6/coredns_1.8.6_linux_amd64.tgz
|
||||
tar xzf coredns_1.8.6_linux_amd64.tgz
|
||||
|
||||
- name: Start coredns
|
||||
run: |
|
||||
sudo ./coredns -conf testdata/coredns.conf &
|
||||
sudo systemctl stop systemd-resolved
|
||||
echo "nameserver 127.0.0.54" | sudo tee /etc/resolv.conf
|
||||
|
||||
- name: Install dkimverify
|
||||
run: sudo apt -y install python3-dkim
|
||||
|
||||
- name: Work around early dependencies MSRV bump
|
||||
run: |
|
||||
cargo update -p anstyle --precise 1.0.2
|
||||
cargo update -p clap --precise 4.3.24
|
||||
cargo update -p clap_lex --precise 0.5.0
|
||||
|
||||
- name: Test with no default features
|
||||
run: cargo test --no-default-features
|
||||
|
||||
- name: Test with default features
|
||||
run: cargo test
|
||||
|
||||
- name: Test with all features
|
||||
run: cargo test --all-features
|
||||
- name: Test with all features (-native-tls)
|
||||
run: cargo test --no-default-features --features async-std1,async-std1-rustls-tls,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls-tls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tracing
|
||||
|
||||
- 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:
|
||||
# name: Coverage
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,4 +4,3 @@
|
||||
lettre.sublime-*
|
||||
lettre.iml
|
||||
target/
|
||||
/Cargo.lock
|
||||
|
||||
254
CHANGELOG.md
254
CHANGELOG.md
@@ -1,11 +1,253 @@
|
||||
<a name="v0.11.7"></a>
|
||||
### v0.11.7 (2024-04-23)
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bump `hostname` to v0.4 ([#956])
|
||||
* Fix `tracing` message consistency ([#960])
|
||||
* Bump minimum required `rustls` to v0.23.5 ([#958])
|
||||
* Dropped use of `ref` syntax in the entire project ([#959])
|
||||
|
||||
[#956]: https://github.com/lettre/lettre/pull/956
|
||||
[#958]: https://github.com/lettre/lettre/pull/958
|
||||
[#959]: https://github.com/lettre/lettre/pull/959
|
||||
[#960]: https://github.com/lettre/lettre/pull/960
|
||||
|
||||
<a name="v0.11.6"></a>
|
||||
### v0.11.6 (2024-03-28)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Updated copyright year in license ([#954])
|
||||
|
||||
[#952]: https://github.com/lettre/lettre/pull/952
|
||||
[#954]: https://github.com/lettre/lettre/pull/954
|
||||
|
||||
<a name="v0.11.5"></a>
|
||||
### v0.11.5 (2024-03-25)
|
||||
|
||||
#### Features
|
||||
|
||||
* Support SMTP SASL draft login challenge ([#911])
|
||||
* Add conversion from SMTP response code to integer ([#941])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Upgrade `rustls` to v0.23 ([#950])
|
||||
* Bump `base64` to v0.22 ([#945])
|
||||
* Fix typos in documentation ([#943], [#944])
|
||||
* Add `Cargo.lock` ([#942])
|
||||
|
||||
[#911]: https://github.com/lettre/lettre/pull/911
|
||||
[#941]: https://github.com/lettre/lettre/pull/941
|
||||
[#942]: https://github.com/lettre/lettre/pull/942
|
||||
[#943]: https://github.com/lettre/lettre/pull/943
|
||||
[#944]: https://github.com/lettre/lettre/pull/944
|
||||
[#945]: https://github.com/lettre/lettre/pull/945
|
||||
[#950]: https://github.com/lettre/lettre/pull/950
|
||||
|
||||
<a name="v0.11.4"></a>
|
||||
### v0.11.4 (2024-01-28)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Percent decode credentials in SMTP connect URL ([#932], [#934])
|
||||
* Fix mimebody DKIM body-hash computation ([#923])
|
||||
|
||||
[#923]: https://github.com/lettre/lettre/pull/923
|
||||
[#932]: https://github.com/lettre/lettre/pull/932
|
||||
[#934]: https://github.com/lettre/lettre/pull/934
|
||||
|
||||
<a name="v0.11.3"></a>
|
||||
### v0.11.3 (2024-01-02)
|
||||
|
||||
#### Features
|
||||
|
||||
* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924])
|
||||
* Derive `Debug` for `SmtpTransport` ([#925])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Upgrade `rustls` to v0.22 ([#921])
|
||||
* Drop once_cell dependency in favor of OnceLock from std ([#928])
|
||||
|
||||
[#921]: https://github.com/lettre/lettre/pull/921
|
||||
[#924]: https://github.com/lettre/lettre/pull/924
|
||||
[#925]: https://github.com/lettre/lettre/pull/925
|
||||
[#928]: https://github.com/lettre/lettre/pull/928
|
||||
|
||||
<a name="v0.11.2"></a>
|
||||
### v0.11.2 (2023-11-23)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.70 ([#916])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bump `idna` to v0.5 ([#918])
|
||||
* Bump `boring` and `tokio-boring` to v4 ([#915])
|
||||
|
||||
[#915]: https://github.com/lettre/lettre/pull/915
|
||||
[#916]: https://github.com/lettre/lettre/pull/916
|
||||
[#918]: https://github.com/lettre/lettre/pull/918
|
||||
|
||||
<a name="v0.11.1"></a>
|
||||
### v0.11.1 (2023-10-24)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Fix `webpki-roots` certificate store setup ([#909])
|
||||
|
||||
[#909]: https://github.com/lettre/lettre/pull/909
|
||||
|
||||
<a name="v0.11.0"></a>
|
||||
### v0.11.0 (2023-10-15)
|
||||
|
||||
While this release technically contains breaking changes, we expect most projects
|
||||
to be able to upgrade by only bumping the version in `Cargo.toml`.
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.65 ([#869] and [#881])
|
||||
* `AddressError` is now marked as `#[non_exhaustive]` ([#839])
|
||||
|
||||
#### Features
|
||||
|
||||
* Improve mailbox parsing ([#839])
|
||||
* Add construction of SMTP transport from URL ([#901])
|
||||
* Add `From<Address>` implementation for `Mailbox` ([#879])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bump `socket2` to v0.5 ([#868])
|
||||
* Bump `idna` to v0.4, `fastrand` to v2, `quoted_printable` to v0.5, `rsa` to v0.9 ([#882])
|
||||
* Bump `webpki-roots` to v0.25 ([#884] and [#890])
|
||||
* Bump `ed25519-dalek` to v2 fixing RUSTSEC-2022-0093 ([#896])
|
||||
* Bump `boring`ssl crates to v3 ([#897])
|
||||
|
||||
[#839]: https://github.com/lettre/lettre/pull/839
|
||||
[#868]: https://github.com/lettre/lettre/pull/868
|
||||
[#869]: https://github.com/lettre/lettre/pull/869
|
||||
[#879]: https://github.com/lettre/lettre/pull/879
|
||||
[#881]: https://github.com/lettre/lettre/pull/881
|
||||
[#882]: https://github.com/lettre/lettre/pull/882
|
||||
[#884]: https://github.com/lettre/lettre/pull/884
|
||||
[#890]: https://github.com/lettre/lettre/pull/890
|
||||
[#896]: https://github.com/lettre/lettre/pull/896
|
||||
[#897]: https://github.com/lettre/lettre/pull/897
|
||||
[#901]: https://github.com/lettre/lettre/pull/901
|
||||
|
||||
<a name="v0.10.4"></a>
|
||||
### v0.10.4 (2023-04-02)
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bumped rustls to 0.21 and all related dependencies ([#867])
|
||||
|
||||
[#867]: https://github.com/lettre/lettre/pull/867
|
||||
|
||||
<a name="v0.10.3"></a>
|
||||
### v0.10.3 (2023-02-20)
|
||||
|
||||
#### 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>
|
||||
### v0.10.0 (unreleased)
|
||||
### v0.10.0 (2022-06-29)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
|
||||
|
||||
* MSRV is now 1.45.2
|
||||
* MSRV is now 1.56.0
|
||||
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message`
|
||||
and make sure to enable the `builder` feature (it's enabled by default).
|
||||
* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
|
||||
@@ -14,7 +256,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
||||
|
||||
#### Features
|
||||
|
||||
* Add `tokio` 0.2 and 1.0 support
|
||||
* Add `tokio` 1 support
|
||||
* Add `rustls` support
|
||||
* Add `async-std` support. NOTE: native-tls isn't supported when using async-std for the smtp transport.
|
||||
* Allow enabling multiple SMTP authentication mechanisms
|
||||
@@ -29,9 +271,11 @@ 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
|
||||
* `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)
|
||||
* The `sendmail` and `file` transports aren't enabled by default anymore.
|
||||
* The `new` method of `ClientId` is deprecated
|
||||
* Rename `serde-impls` feature to `serde`
|
||||
|
||||
* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of
|
||||
`/usr/bin/sendmail`.
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
@@ -52,7 +296,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
||||
* Update `hostname` to 0.3
|
||||
* Update to `nom` 6
|
||||
* Replace `log` with `tracing`
|
||||
* Move CI to Github Actions
|
||||
* Move CI to GitHub Actions
|
||||
* Use criterion for benchmarks
|
||||
|
||||
<a name="v0.9.2"></a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Contributing to Lettre
|
||||
|
||||
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
||||
The following guidelines are inspired by the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
||||
|
||||
### Code formatting
|
||||
|
||||
|
||||
2674
Cargo.lock
generated
Normal file
2674
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
125
Cargo.toml
125
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lettre"
|
||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||
version = "0.10.0-beta.1"
|
||||
version = "0.11.7"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "https://lettre.rs"
|
||||
@@ -10,7 +10,8 @@ license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
|
||||
categories = ["email", "network-programming"]
|
||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
||||
[badges]
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
@@ -18,33 +19,37 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
idna = "0.2"
|
||||
chumsky = "0.9"
|
||||
idna = "0.5"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
||||
|
||||
# builder
|
||||
hyperx = { version = "1", optional = true, features = ["headers"] }
|
||||
httpdate = { version = "1", optional = true }
|
||||
mime = { version = "0.3.4", optional = true }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
rand = { version = "0.8", optional = true }
|
||||
quoted_printable = { version = "0.4", optional = true }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
once_cell = "1"
|
||||
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
|
||||
fastrand = { version = "2.0", optional = true }
|
||||
quoted_printable = { version = "0.5", optional = true }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
email-encoding = { version = "0.3", optional = true }
|
||||
|
||||
# file transport
|
||||
uuid = { version = "1", features = ["v4"], optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
# smtp
|
||||
nom = { version = "6", default-features = false, features = ["alloc"], optional = true }
|
||||
r2d2 = { version = "0.8", optional = true } # feature
|
||||
hostname = { version = "0.3", optional = true } # feature
|
||||
# smtp-transport
|
||||
nom = { version = "7", optional = true }
|
||||
hostname = { version = "0.4", optional = true } # feature
|
||||
socket2 = { version = "0.5.1", optional = true }
|
||||
url = { version = "2.4", optional = true }
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
|
||||
## tls
|
||||
native-tls = { version = "0.2", optional = true } # feature
|
||||
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true }
|
||||
webpki = { version = "0.21", optional = true }
|
||||
webpki-roots = { version = "0.21", optional = true }
|
||||
native-tls = { version = "0.2.5", optional = true } # feature
|
||||
rustls = { version = "0.23.5", default-features = false, features = ["ring", "logging", "std", "tls12"], optional = true }
|
||||
rustls-pemfile = { version = "2", optional = true }
|
||||
rustls-native-certs = { version = "0.7", optional = true }
|
||||
webpki-roots = { version = "0.26", optional = true }
|
||||
boring = { version = "4", optional = true }
|
||||
|
||||
# async
|
||||
futures-io = { version = "0.3.7", optional = true }
|
||||
@@ -52,59 +57,79 @@ futures-util = { version = "0.3.7", default-features = false, features = ["io"],
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
|
||||
## 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-rustls = { version = "0.2", optional = true }
|
||||
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
|
||||
|
||||
## tokio
|
||||
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["fs", "process", "tcp", "dns", "io-util"], optional = true }
|
||||
tokio02_native_tls_crate = { package = "tokio-native-tls", version = "0.1", optional = true }
|
||||
tokio02_rustls = { package = "tokio-rustls", version = "0.15", optional = true }
|
||||
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "process", "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_rustls = { package = "tokio-rustls", version = "0.22", optional = true }
|
||||
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true }
|
||||
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
|
||||
|
||||
## dkim
|
||||
sha2 = { version = "0.10", optional = true, features = ["oid"] }
|
||||
rsa = { version = "0.9", optional = true }
|
||||
ed25519-dalek = { version = "2", optional = true }
|
||||
|
||||
# email formats
|
||||
email_address = { version = "0.2.1", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3"
|
||||
tracing-subscriber = "0.2.10"
|
||||
pretty_assertions = "1"
|
||||
criterion = "0.5"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
|
||||
tracing-subscriber = "0.3"
|
||||
glob = "0.3"
|
||||
walkdir = "2"
|
||||
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
|
||||
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
async-std = { version = "1.8", features = ["attributes"] }
|
||||
serde_json = "1"
|
||||
maud = "0.22.1"
|
||||
maud = "0.26"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "transport_smtp"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "mailbox_parsing"
|
||||
|
||||
[features]
|
||||
default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"]
|
||||
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
|
||||
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
||||
builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
|
||||
mime03 = ["dep:mime"]
|
||||
|
||||
# transports
|
||||
file-transport = []
|
||||
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
||||
sendmail-transport = []
|
||||
smtp-transport = ["base64", "nom"]
|
||||
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
|
||||
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
||||
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||
|
||||
rustls-tls = ["webpki", "webpki-roots", "rustls"]
|
||||
pool = ["dep:futures-util"]
|
||||
|
||||
rustls-tls = ["dep:webpki-roots", "dep:rustls", "dep:rustls-pemfile"]
|
||||
|
||||
boring-tls = ["dep:boring"]
|
||||
|
||||
# async
|
||||
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
|
||||
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"]
|
||||
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"]
|
||||
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"]
|
||||
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
||||
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||
#async-std1-native-tls = ["async-std1", "native-tls", "dep:async-native-tls"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "dep:futures-rustls"]
|
||||
tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
|
||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "dep:tokio1_rustls"]
|
||||
tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
|
||||
|
||||
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
|
||||
|
||||
[[example]]
|
||||
name = "autoconfigure"
|
||||
required-features = ["smtp-transport", "native-tls"]
|
||||
|
||||
[[example]]
|
||||
name = "basic_html"
|
||||
@@ -130,14 +155,6 @@ required-features = ["smtp-transport", "native-tls", "builder"]
|
||||
name = "smtp_selfsigned"
|
||||
required-features = ["smtp-transport", "native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio02_smtp_tls"
|
||||
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio02_smtp_starttls"
|
||||
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio1_smtp_tls"
|
||||
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -1,5 +1,5 @@
|
||||
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me>
|
||||
Copyright (c) 2019-2020 Paolo Barbolini <paolo@paolo565.org>
|
||||
Copyright (c) 2014-2024 Alexis Mousset <contact@amousset.me>
|
||||
Copyright (c) 2019-2024 Paolo Barbolini <paolo@paolo565.org>
|
||||
Copyright (c) 2018 K. <kayo@illumium.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
|
||||
53
README.md
53
README.md
@@ -28,22 +28,14 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.10.0-beta.1">
|
||||
<img src="https://deps.rs/crate/lettre/0.10.0-beta.1/status.svg"
|
||||
<a href="https://deps.rs/crate/lettre/0.11.7">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.7/status.svg"
|
||||
alt="dependency status" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
**NOTE**: this readme refers to the 0.10 version of lettre, which is
|
||||
still being worked on. The master branch and the alpha releases will see
|
||||
API breaking changes and some features may be missing or incomplete until
|
||||
the stable 0.10.0 release is out.
|
||||
Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x) branch for stable releases.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
Lettre provides the following features:
|
||||
@@ -58,18 +50,24 @@ Lettre does not provide (for now):
|
||||
|
||||
* 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
|
||||
|
||||
This library requires Rust 1.45 or newer.
|
||||
This library requires Rust 1.70 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.10.0-beta.1"
|
||||
lettre = "0.11"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
@@ -78,10 +76,11 @@ let email = Message::builder()
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.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
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
@@ -92,16 +91,36 @@ let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
|
||||
such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
|
||||
|
||||
Alternatively only unit tests can be run by doing `cargo test --lib`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
These are general steps to be followed when troubleshooting SMTP related issues.
|
||||
|
||||
- Ensure basic connectivity, ensure requisite ports are open and daemons are listening.
|
||||
- Confirm that your service provider allows traffic on the ports being used for mail transfer.
|
||||
- Check SMTP relay authentication and configuration.
|
||||
- Validate your DNS records. (DMARC, SPF, DKIM, MX)
|
||||
- Verify your SSL/TLS certificates are setup properly.
|
||||
- Investigate if filtering, formatting, or filesize limits are causing messages to be lost, delayed, or blocked by relays or remote hosts.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Anyone who interacts with Lettre in any space, including but not limited to
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
|
||||
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
|
||||
should not be reported via the public Github Issue tracker.
|
||||
should not be reported via the public GitHub Issue tracker.
|
||||
|
||||
## Security advisories
|
||||
|
||||
|
||||
27
benches/mailbox_parsing.rs
Normal file
27
benches/mailbox_parsing.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::message::{Mailbox, Mailboxes};
|
||||
|
||||
fn bench_parse_single(mailbox: &str) {
|
||||
assert!(mailbox.parse::<Mailbox>().is_ok());
|
||||
}
|
||||
|
||||
fn bench_parse_multiple(mailboxes: &str) {
|
||||
assert!(mailboxes.parse::<Mailboxes>().is_ok());
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("parse single mailbox", |b| {
|
||||
b.iter(|| bench_parse_single(black_box("\"Benchmark test\" <test@mail.local>")))
|
||||
});
|
||||
|
||||
c.bench_function("parse multiple mailboxes", |b| {
|
||||
b.iter(|| {
|
||||
bench_parse_multiple(black_box(
|
||||
"\"Benchmark test\" <test@mail.local>, Test <test@mail.local>, <test@mail.local>, test@mail.local",
|
||||
))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -14,7 +14,7 @@ This folder contains examples showing how to use lettre in your own projects.
|
||||
- [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password.
|
||||
- [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password.
|
||||
- The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/).
|
||||
These files are prefixed with `tokio02_`, `tokio1_` or `asyncstd1_`.
|
||||
These files are prefixed with `tokio1_` or `asyncstd1_`.
|
||||
|
||||
[basic_html.rs]: ./basic_html.rs
|
||||
[maud_html.rs]: ./maud_html.rs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
|
||||
AsyncStd1Transport, Message,
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncStd1Executor, AsyncTransport, Message,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
@@ -12,10 +12,11 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.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
|
||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
@@ -27,6 +28,6 @@ async fn main() {
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
|
||||
AsyncStd1Transport, Message,
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncStd1Executor, AsyncTransport, Message,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
@@ -12,10 +12,11 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.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
|
||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
@@ -27,6 +28,6 @@ async fn main() {
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
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
93
examples/autoconfigure.rs
Normal 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 upgrading 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,16 +29,12 @@ fn main() {
|
||||
MultiPart::alternative() // This is composed of two parts.
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/html; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(String::from(html)),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -38,16 +38,12 @@ fn main() {
|
||||
MultiPart::alternative() // This is composed of two parts.
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/html; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(html.into_string()),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
@@ -8,6 +8,7 @@ fn main() {
|
||||
.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();
|
||||
|
||||
@@ -17,6 +18,6 @@ fn main() {
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fs;
|
||||
|
||||
use lettre::{
|
||||
message::header::ContentType,
|
||||
transport::smtp::{
|
||||
authentication::Credentials,
|
||||
client::{Certificate, Tls, TlsParameters},
|
||||
@@ -16,18 +17,19 @@ fn main() {
|
||||
.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();
|
||||
|
||||
// Use a custom certificate stored on disk to securely verify the server's certificate
|
||||
let pem_cert = fs::read("certificate.pem").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)
|
||||
.build()
|
||||
.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
|
||||
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
|
||||
@@ -39,6 +41,6 @@ fn main() {
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
tracing_subscriber::fmt::init();
|
||||
@@ -8,10 +11,11 @@ fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_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
|
||||
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
|
||||
@@ -22,6 +26,6 @@ fn main() {
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
tracing_subscriber::fmt::init();
|
||||
@@ -8,10 +11,11 @@ fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
@@ -22,6 +26,6 @@ fn main() {
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Executor,
|
||||
Tokio02Transport,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
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 async year")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer: AsyncSmtpTransport<Tokio02Executor> =
|
||||
AsyncSmtpTransport::<Tokio02Executor>::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Executor,
|
||||
Tokio02Transport,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
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 async year")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer: AsyncSmtpTransport<Tokio02Executor> =
|
||||
AsyncSmtpTransport::<Tokio02Executor>::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Executor,
|
||||
Tokio1Transport,
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -17,10 +16,11 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.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
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
@@ -32,6 +32,6 @@ async fn main() {
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Executor,
|
||||
Tokio1Transport,
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -17,10 +16,11 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.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
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
@@ -32,6 +32,6 @@ async fn main() {
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
3
rustfmt.toml
Normal file
3
rustfmt.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
format_code_in_doc_comments = true
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
@@ -1,6 +1,3 @@
|
||||
#[cfg(feature = "builder")]
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::Address;
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::message::header::{self, Headers};
|
||||
@@ -27,15 +24,12 @@ impl Envelope {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// # use lettre::Address;
|
||||
/// # use lettre::address::Envelope;
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = Address::from_str("from@email.com")?;
|
||||
/// let recipients = vec![Address::from_str("to@email.com")?];
|
||||
/// let sender = "sender@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients);
|
||||
/// # Ok(())
|
||||
@@ -59,15 +53,12 @@ impl Envelope {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// # use lettre::Address;
|
||||
/// # use lettre::address::Envelope;
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = Address::from_str("from@email.com")?;
|
||||
/// let recipients = vec![Address::from_str("to@email.com")?];
|
||||
/// let sender = "from@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
|
||||
/// assert_eq!(envelope.to(), recipients.as_slice());
|
||||
@@ -82,15 +73,12 @@ impl Envelope {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// # use lettre::Address;
|
||||
/// # use lettre::address::Envelope;
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = Address::from_str("from@email.com")?;
|
||||
/// let recipients = vec![Address::from_str("to@email.com")?];
|
||||
/// let sender = "from@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
|
||||
/// assert!(envelope.from().is_some());
|
||||
@@ -104,6 +92,7 @@ impl Envelope {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
/// Check if any of the addresses in the envelope contains non-ascii chars
|
||||
pub(crate) fn has_non_ascii_addresses(&self) -> bool {
|
||||
self.reverse_path
|
||||
@@ -120,15 +109,16 @@ impl TryFrom<&Headers> for Envelope {
|
||||
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
|
||||
let from = match headers.get::<header::Sender>() {
|
||||
// If there is a Sender, use it
|
||||
Some(header::Sender(a)) => Some(a.email.clone()),
|
||||
Some(sender) => Some(Mailbox::from(sender).email),
|
||||
// ... else try From
|
||||
None => match headers.get::<header::From>() {
|
||||
Some(header::From(a)) => {
|
||||
let from: Vec<Mailbox> = a.clone().into();
|
||||
let mut from: Vec<Mailbox> = a.into();
|
||||
if from.len() > 1 {
|
||||
return Err(Error::TooManyFrom);
|
||||
}
|
||||
Some(from[0].email.clone())
|
||||
let from = from.pop().expect("From header has 1 Mailbox");
|
||||
Some(from.email)
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
@@ -136,18 +126,16 @@ impl TryFrom<&Headers> for Envelope {
|
||||
|
||||
fn add_addresses_from_mailboxes(
|
||||
addresses: &mut Vec<Address>,
|
||||
mailboxes: Option<&Mailboxes>,
|
||||
mailboxes: Option<Mailboxes>,
|
||||
) {
|
||||
if let Some(mailboxes) = mailboxes {
|
||||
for mailbox in mailboxes.iter() {
|
||||
addresses.push(mailbox.email.clone());
|
||||
}
|
||||
addresses.extend(mailboxes.into_iter().map(|mb| mb.email));
|
||||
}
|
||||
}
|
||||
let mut to = vec![];
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| &h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| &h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| &h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| h.0));
|
||||
|
||||
Self::new(from, to)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
//! Representation of an email address
|
||||
|
||||
use idna::domain_to_ascii;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
@@ -12,21 +8,27 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use email_address::EmailAddress;
|
||||
use idna::domain_to_ascii;
|
||||
|
||||
/// Represents an email address with a user and a domain name.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// You can create an `Address` from a user and a domain:
|
||||
///
|
||||
/// ```
|
||||
/// # use lettre::Address;
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -34,11 +36,13 @@ use std::{
|
||||
/// You can also create an `Address` from a string literal by parsing it:
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// # use lettre::Address;
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::from_str("example@email.com")?;
|
||||
/// let address = "user@email.com".parse::<Address>()?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -50,42 +54,6 @@ pub struct Address {
|
||||
at_start: usize,
|
||||
}
|
||||
|
||||
impl<U, D> TryFrom<(U, D)> for Address
|
||||
where
|
||||
U: AsRef<str>,
|
||||
D: AsRef<str>,
|
||||
{
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
|
||||
let user = user.as_ref();
|
||||
Address::check_user(user)?;
|
||||
|
||||
let domain = domain.as_ref();
|
||||
Address::check_domain(domain)?;
|
||||
|
||||
let serialized = format!("{}@{}", user, domain);
|
||||
Ok(Address {
|
||||
serialized,
|
||||
at_start: user.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
/// Creates a new email address from a user and domain.
|
||||
///
|
||||
@@ -96,8 +64,8 @@ impl Address {
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let expected: Address = "example@email.com".parse()?;
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// let expected = "user@email.com".parse::<Address>()?;
|
||||
/// assert_eq!(expected, address);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -115,8 +83,8 @@ impl Address {
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// assert_eq!("example", address.user());
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -133,8 +101,8 @@ impl Address {
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// assert_eq!("email.com", address.domain());
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -143,7 +111,7 @@ impl Address {
|
||||
}
|
||||
|
||||
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
|
||||
if USER_RE.is_match(user) {
|
||||
if EmailAddress::is_valid_local_part(user) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AddressError::InvalidUser)
|
||||
@@ -159,21 +127,25 @@ impl Address {
|
||||
}
|
||||
|
||||
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
|
||||
if DOMAIN_RE.is_match(domain) {
|
||||
// Domain
|
||||
if EmailAddress::is_valid_domain(domain) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(caps) = LITERAL_RE.captures(domain) {
|
||||
if let Some(cap) = caps.get(1) {
|
||||
if cap.as_str().parse::<IpAddr>().is_ok() {
|
||||
// IP
|
||||
let ip = domain
|
||||
.strip_prefix('[')
|
||||
.and_then(|ip| ip.strip_suffix(']'))
|
||||
.unwrap_or(domain);
|
||||
|
||||
if ip.parse::<IpAddr>().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(AddressError::InvalidDomain)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
/// Check if the address contains non-ascii chars
|
||||
pub(super) fn is_ascii(&self) -> bool {
|
||||
self.serialized.is_ascii()
|
||||
@@ -190,19 +162,48 @@ impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, AddressError> {
|
||||
let mut parts = val.rsplitn(2, '@');
|
||||
let domain = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
let user = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
|
||||
Address::check_user(user)?;
|
||||
Address::check_domain(domain)?;
|
||||
let at_start = check_address(val)?;
|
||||
Ok(Address {
|
||||
serialized: val.into(),
|
||||
at_start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, D> TryFrom<(U, D)> for Address
|
||||
where
|
||||
U: AsRef<str>,
|
||||
D: AsRef<str>,
|
||||
{
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
|
||||
let user = user.as_ref();
|
||||
Address::check_user(user)?;
|
||||
|
||||
let domain = domain.as_ref();
|
||||
Address::check_domain(domain)?;
|
||||
|
||||
let serialized = format!("{user}@{domain}");
|
||||
Ok(Address {
|
||||
serialized,
|
||||
at_start: user.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Address {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(serialized: String) -> Result<Self, AddressError> {
|
||||
let at_start = check_address(&serialized)?;
|
||||
Ok(Address {
|
||||
serialized,
|
||||
at_start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Address {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.serialized
|
||||
@@ -215,14 +216,30 @@ impl AsRef<OsStr> for Address {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
fn check_address(val: &str) -> Result<usize, AddressError> {
|
||||
let mut parts = val.rsplitn(2, '@');
|
||||
let domain = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
let user = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
|
||||
Address::check_user(user)?;
|
||||
Address::check_domain(domain)?;
|
||||
Ok(user.len())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
/// Errors in email addresses parsing
|
||||
pub enum AddressError {
|
||||
/// Missing domain or user
|
||||
MissingParts,
|
||||
/// Unbalanced angle bracket
|
||||
Unbalanced,
|
||||
/// Invalid email user
|
||||
InvalidUser,
|
||||
/// Invalid email domain
|
||||
InvalidDomain,
|
||||
InvalidUtf8b,
|
||||
/// Invalid input found
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
impl Error for AddressError {}
|
||||
@@ -234,7 +251,7 @@ impl Display for AddressError {
|
||||
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
|
||||
AddressError::InvalidUser => f.write_str("Invalid email user"),
|
||||
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
|
||||
AddressError::InvalidUtf8b => f.write_str("Invalid UTF8b data"),
|
||||
AddressError::InvalidInput => f.write_str("Invalid input"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,7 +261,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_address() {
|
||||
fn ascii_address() {
|
||||
let addr_str = "something@example.com";
|
||||
let addr = Address::from_str(addr_str).unwrap();
|
||||
let addr2 = Address::new("something", "example.com").unwrap();
|
||||
@@ -254,4 +271,36 @@ mod tests {
|
||||
assert_eq!(addr2.user(), "something");
|
||||
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
12
src/base64.rs
Normal 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)
|
||||
}
|
||||
231
src/executor.rs
231
src/executor.rs
@@ -1,38 +1,71 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use std::fmt::Debug;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "file-transport")]
|
||||
use std::io::Result as IoResult;
|
||||
#[cfg(feature = "file-transport")]
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
use futures_util::future::BoxFuture;
|
||||
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::client::AsyncSmtpConnection;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::client::Tls;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::extension::ClientId;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
/// Async executor abstraction trait
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[async_trait]
|
||||
pub trait Executor: Send + Sync + private::Sealed {
|
||||
pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle: SpawnHandle;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep: Future<Output = ()> + Send + 'static;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error>;
|
||||
@@ -46,98 +79,82 @@ pub trait Executor: Send + Sync + private::Sealed {
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
|
||||
}
|
||||
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
|
||||
pub struct Tokio02Executor;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl Executor for Tokio02Executor {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||
tokio02_crate::fs::read(path).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||
tokio02_crate::fs::write(path, contents).await
|
||||
}
|
||||
pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
|
||||
async fn shutdown(self);
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `tokio` `1.x`
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
#[derive(Debug)]
|
||||
pub struct Tokio1Executor;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Executor for Tokio1Executor {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle = tokio1_crate::task::JoinHandle<()>;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep = tokio1_crate::time::Sleep;
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
tokio1_crate::spawn(fut)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep {
|
||||
tokio1_crate::time::sleep(duration)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_tokio1(hostname, port, hello_name, tls_parameters).await?;
|
||||
let mut conn = AsyncSmtpConnection::connect_tokio1(
|
||||
(hostname, port),
|
||||
timeout,
|
||||
hello_name,
|
||||
tls_parameters,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
@@ -146,55 +163,94 @@ impl Executor for Tokio1Executor {
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||
tokio1_crate::fs::read(path).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||
tokio1_crate::fs::write(path, contents).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
#[async_trait]
|
||||
impl SpawnHandle for tokio1_crate::task::JoinHandle<()> {
|
||||
async fn shutdown(self) {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `async-std` `1.x`
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
#[derive(Debug)]
|
||||
pub struct AsyncStd1Executor;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Executor for AsyncStd1Executor {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle = async_std::task::JoinHandle<()>;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep = BoxFuture<'static, ()>;
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
async_std::task::spawn(fut)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep {
|
||||
let fut = async_std::task::sleep(duration);
|
||||
Box::pin(fut)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_asyncstd1(hostname, port, hello_name, tls_parameters)
|
||||
let mut conn = AsyncSmtpConnection::connect_asyncstd1(
|
||||
(hostname, port),
|
||||
timeout,
|
||||
hello_name,
|
||||
tls_parameters,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
@@ -203,30 +259,37 @@ impl Executor for AsyncStd1Executor {
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||
async_std::fs::read(path).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||
async_std::fs::write(path, contents).await
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
#[async_trait]
|
||||
impl SpawnHandle for async_std::task::JoinHandle<()> {
|
||||
async fn shutdown(self) {
|
||||
self.cancel().await;
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl Sealed for Tokio02Executor {}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Sealed for Tokio1Executor {}
|
||||
impl Sealed for super::Tokio1Executor {}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Sealed for AsyncStd1Executor {}
|
||||
impl Sealed for super::AsyncStd1Executor {}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
impl Sealed for async_std::task::JoinHandle<()> {}
|
||||
}
|
||||
|
||||
273
src/lib.rs
273
src/lib.rs
@@ -4,33 +4,112 @@
|
||||
//! * Pluggable email transports
|
||||
//! * Unicode support
|
||||
//! * Secure defaults
|
||||
//! * Async support
|
||||
//!
|
||||
//! Lettre requires Rust 1.45 or newer.
|
||||
//! Lettre requires Rust 1.70 or newer.
|
||||
//!
|
||||
//! ## Optional features
|
||||
//! ## Features
|
||||
//!
|
||||
//! This section lists each lettre feature and briefly explains it.
|
||||
//! More info about each module can be found in the corresponding module page.
|
||||
//!
|
||||
//! Features with `📫` near them are enabled by default.
|
||||
//!
|
||||
//! ### Typed message builder
|
||||
//!
|
||||
//! _Strongly typed [`message`] builder_
|
||||
//!
|
||||
//! * **builder** 📫: Enable the [`Message`] builder
|
||||
//! * **hostname** 📫: Try to use the actual system hostname in the `Message-ID` header
|
||||
//!
|
||||
//! ### SMTP transport
|
||||
//!
|
||||
//! _Send emails using [`SMTP`]_
|
||||
//!
|
||||
//! * **smtp-transport** 📫: Enable the SMTP transport
|
||||
//! * **pool** 📫: Connection pool for SMTP transport
|
||||
//! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
|
||||
//!
|
||||
//! #### SMTP over TLS via the native-tls crate
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `native-tls` crate_
|
||||
//!
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
|
||||
//!
|
||||
//! * **native-tls** 📫: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
|
||||
//!
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! #### 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
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
|
||||
//!
|
||||
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
|
||||
//!
|
||||
//! * **rustls-tls**: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
|
||||
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
|
||||
//!
|
||||
//! ### Sendmail transport
|
||||
//!
|
||||
//! _Send emails using the [`sendmail`] command_
|
||||
//!
|
||||
//! * **sendmail-transport**: Enable the `sendmail` transport
|
||||
//!
|
||||
//! ### File transport
|
||||
//!
|
||||
//! _Save emails as an `.eml` [`file`]_
|
||||
//!
|
||||
//! * **file-transport**: Enable the file transport (saves emails into an `.eml` file)
|
||||
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file (additionally saves envelopes into a `.json` file)
|
||||
//!
|
||||
//! ### Async execution runtimes
|
||||
//!
|
||||
//! _Use [tokio] or [async-std] as an async execution runtime for sending emails_
|
||||
//!
|
||||
//! The correct runtime version must be chosen in order for lettre to work correctly.
|
||||
//! For example, when sending emails from a Tokio 1.x context, the Tokio 1.x executor
|
||||
//! ([`Tokio1Executor`]) must be used. Using a different version (for example Tokio 0.2.x),
|
||||
//! or async-std, would result in a runtime panic.
|
||||
//!
|
||||
//! * **tokio1**: Allow to asynchronously send emails using [Tokio 1.x]
|
||||
//! * **async-std1**: Allow to asynchronously send emails using [async-std 1.x]
|
||||
//!
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! ### Misc features
|
||||
//!
|
||||
//! _Additional features_
|
||||
//!
|
||||
//! * **builder**: Message builder
|
||||
//! * **file-transport**: Transport that write messages into a file
|
||||
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file
|
||||
//! * **smtp-transport**: Transport over SMTP
|
||||
//! * **sendmail-transport**: Transport over SMTP
|
||||
//! * **rustls-tls**: TLS support with the `rustls` crate
|
||||
//! * **native-tls**: TLS support with the `native-tls` crate
|
||||
//! * **tokio02**: Allow to asyncronously send emails using tokio 0.2.x
|
||||
//! * **tokio02-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.2
|
||||
//! * **tokio02-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.2
|
||||
//! * **tokio1**: Allow to asyncronously send emails using tokio 1.x
|
||||
//! * **tokio1-rustls-tls**: Async TLS support with the `rustls` crate using tokio 1.x
|
||||
//! * **tokio1-native-tls**: Async TLS support with the `native-tls` crate using tokio 1.x
|
||||
//! * **async-std1**: Allow to asynchronously send emails using async-std 1.x
|
||||
//! * NOTE: native-tls isn't supported with async-std at the moment
|
||||
//! * **async-std1-rustls-tls**: Async TLS support with the `rustls` crate using async-std 1.x
|
||||
//! * **r2d2**: Connection pool for SMTP transport
|
||||
//! * **tracing**: Logging using the `tracing` crate
|
||||
//! * **serde**: Serialization/Deserialization of entities
|
||||
//! * **hostname**: Ability to try to use actual hostname in SMTP transaction
|
||||
//! * **tracing**: Logging using the `tracing` crate
|
||||
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
|
||||
//! * **dkim**: Add support for signing email with DKIM
|
||||
//!
|
||||
//! [`SMTP`]: crate::transport::smtp
|
||||
//! [`sendmail`]: crate::transport::sendmail
|
||||
//! [`file`]: crate::transport::file
|
||||
//! [`ContentType`]: crate::message::header::ContentType
|
||||
//! [tokio]: https://docs.rs/tokio/1
|
||||
//! [async-std]: https://docs.rs/async-std/1
|
||||
//! [ring]: https://github.com/briansmith/ring#ring
|
||||
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
|
||||
//! [Tokio 1.x]: https://docs.rs/tokio/1
|
||||
//! [async-std 1.x]: https://docs.rs/async-std/1
|
||||
//! [mime 0.3]: https://docs.rs/mime/0.3
|
||||
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-beta.1")]
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.7")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -40,89 +119,153 @@
|
||||
trivial_numeric_casts,
|
||||
unstable_features,
|
||||
unused_import_braces,
|
||||
rust_2018_idioms
|
||||
rust_2018_idioms,
|
||||
clippy::string_add,
|
||||
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(not(lettre_ignore_tls_mismatch))]
|
||||
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(
|
||||
feature = "tokio1",
|
||||
feature = "native-tls",
|
||||
not(feature = "tokio1-native-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
|
||||
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
feature = "rustls-tls",
|
||||
not(feature = "tokio1-rustls-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `rustls-tls` features, but the `tokio1-rustls-tls` feature hasn't been turned on.
|
||||
If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
|
||||
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(
|
||||
feature = "async-std1",
|
||||
feature = "native-tls",
|
||||
not(feature = "async-std1-native-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the `async-std1-native-tls` feature hasn't been turned on.
|
||||
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
|
||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
||||
*/
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
feature = "native-tls",
|
||||
not(feature = "async-std1-native-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
|
||||
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
|
||||
If you were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
feature = "rustls-tls",
|
||||
not(feature = "async-std1-rustls-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
|
||||
If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
}
|
||||
|
||||
pub mod address;
|
||||
#[cfg(any(feature = "smtp-transport", feature = "dkim"))]
|
||||
mod base64;
|
||||
pub mod error;
|
||||
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod executor;
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
pub mod message;
|
||||
pub mod transport;
|
||||
|
||||
#[cfg(feature = "builder")]
|
||||
#[macro_use]
|
||||
extern crate hyperx;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub use self::executor::AsyncStd1Executor;
|
||||
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::executor::Executor;
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub use self::executor::Tokio02Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::executor::Tokio1Executor;
|
||||
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[doc(inline)]
|
||||
pub use self::transport::AsyncTransport;
|
||||
pub use crate::address::Address;
|
||||
#[cfg(feature = "builder")]
|
||||
#[doc(inline)]
|
||||
pub use crate::message::Message;
|
||||
#[cfg(all(
|
||||
feature = "file-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::file::AsyncFileTransport;
|
||||
#[cfg(feature = "file-transport")]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::file::FileTransport;
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::sendmail::AsyncSendmailTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::sendmail::SendmailTransport;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
pub use crate::transport::smtp::AsyncSmtpTransport;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use crate::transport::smtp::SmtpTransport;
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::Transport;
|
||||
use crate::{address::Envelope, error::Error};
|
||||
|
||||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
pub use crate::transport::smtp::AsyncStd1Connector;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use crate::transport::smtp::SmtpTransport;
|
||||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio02"))]
|
||||
pub use crate::transport::smtp::Tokio02Connector;
|
||||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
pub use crate::transport::smtp::Tokio1Connector;
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub use crate::transport::AsyncStd1Transport;
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub use crate::transport::Tokio02Transport;
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use crate::transport::Tokio1Transport;
|
||||
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "builder")]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::message::{header, Mailbox, Mailboxes};
|
||||
use hyperx::header::Headers;
|
||||
use std::convert::TryFrom;
|
||||
use crate::message::{header, header::Headers, Mailbox, Mailboxes};
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers() {
|
||||
@@ -150,9 +293,9 @@ mod test {
|
||||
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::Sender(sender));
|
||||
headers.set(header::To(to));
|
||||
headers.set(header::From::from(from));
|
||||
headers.set(header::Sender::from(sender));
|
||||
headers.set(header::To::from(to));
|
||||
|
||||
assert_eq!(
|
||||
Envelope::try_from(&headers).unwrap(),
|
||||
@@ -170,8 +313,8 @@ mod test {
|
||||
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::Sender(sender));
|
||||
headers.set(header::From::from(from));
|
||||
headers.set(header::Sender::from(sender));
|
||||
|
||||
assert!(Envelope::try_from(&headers).is_err(),);
|
||||
}
|
||||
|
||||
145
src/message/attachment.rs
Normal file
145
src/message/attachment.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use crate::message::{
|
||||
header::{self, ContentType},
|
||||
IntoBody, SinglePart,
|
||||
};
|
||||
|
||||
/// `SinglePart` builder for attachments
|
||||
///
|
||||
/// Allows building attachment parts easily.
|
||||
#[derive(Clone)]
|
||||
pub struct Attachment {
|
||||
disposition: Disposition,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Disposition {
|
||||
/// File name
|
||||
Attached(String),
|
||||
/// Content id
|
||||
Inline(String),
|
||||
}
|
||||
|
||||
impl Attachment {
|
||||
/// Create a new attachment
|
||||
///
|
||||
/// This attachment will be displayed as a normal attachment,
|
||||
/// with the chosen `filename` appearing as the file name.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::error::Error;
|
||||
/// use std::fs;
|
||||
///
|
||||
/// use lettre::message::{header::ContentType, Attachment};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let filename = String::from("invoice.pdf");
|
||||
/// # if false {
|
||||
/// let filebody = fs::read("invoice.pdf")?;
|
||||
/// # }
|
||||
/// # let filebody = fs::read("docs/lettre.png")?;
|
||||
/// let content_type = ContentType::parse("application/pdf").unwrap();
|
||||
/// let attachment = Attachment::new(filename).body(filebody, content_type);
|
||||
///
|
||||
/// // The document `attachment` will show up as a normal attachment.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(filename: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Attached(filename),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new inline attachment
|
||||
///
|
||||
/// This attachment should be displayed inline into the message
|
||||
/// body:
|
||||
///
|
||||
/// ```html
|
||||
/// <img src="cid:123">
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::error::Error;
|
||||
/// use std::fs;
|
||||
///
|
||||
/// use lettre::message::{header::ContentType, Attachment};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let content_id = String::from("123");
|
||||
/// # if false {
|
||||
/// let filebody = fs::read("image.jpg")?;
|
||||
/// # }
|
||||
/// # let filebody = fs::read("docs/lettre.png")?;
|
||||
/// let content_type = ContentType::parse("image/jpeg").unwrap();
|
||||
/// let attachment = Attachment::new_inline(content_id).body(filebody, content_type);
|
||||
///
|
||||
/// // The image `attachment` will display inline into the email.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new_inline(content_id: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Inline(content_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the attachment into a [`SinglePart`] which can then be used to build the rest of the email
|
||||
///
|
||||
/// Look at the [Complex MIME body example](crate::message#complex-mime-body)
|
||||
/// to see how [`SinglePart`] can be put into the email.
|
||||
pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
|
||||
let mut builder = SinglePart::builder();
|
||||
builder = match self.disposition {
|
||||
Disposition::Attached(filename) => {
|
||||
builder.header(header::ContentDisposition::attachment(&filename))
|
||||
}
|
||||
Disposition::Inline(content_id) => builder
|
||||
.header(header::ContentId::from(format!("<{content_id}>")))
|
||||
.header(header::ContentDisposition::inline()),
|
||||
};
|
||||
builder = builder.header(content_type);
|
||||
builder.body(content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::header::ContentType;
|
||||
|
||||
#[test]
|
||||
fn attachment() {
|
||||
let part = super::Attachment::new(String::from("test.txt")).body(
|
||||
String::from("Hello world!"),
|
||||
ContentType::parse("text/plain").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8_lossy(&part.formatted()),
|
||||
concat!(
|
||||
"Content-Disposition: attachment; filename=\"test.txt\"\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n\r\n",
|
||||
"Hello world!\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_inline() {
|
||||
let part = super::Attachment::new_inline(String::from("id")).body(
|
||||
String::from("Hello world!"),
|
||||
ContentType::parse("text/plain").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8_lossy(&part.formatted()),
|
||||
concat!(
|
||||
"Content-ID: <id>\r\n",
|
||||
"Content-Disposition: inline\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n\r\n",
|
||||
"Hello world!\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
ops::Deref,
|
||||
};
|
||||
use std::{mem, ops::Deref};
|
||||
|
||||
use crate::message::header::ContentTransferEncoding;
|
||||
|
||||
@@ -18,7 +15,9 @@ pub struct Body {
|
||||
/// makes for a more efficient `Content-Transfer-Encoding` to be chosen.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MaybeString {
|
||||
/// Binary data
|
||||
Binary(Vec<u8>),
|
||||
/// UTF-8 string
|
||||
String(String),
|
||||
}
|
||||
|
||||
@@ -30,13 +29,16 @@ impl Body {
|
||||
/// Automatically chooses the most efficient encoding between
|
||||
/// `7bit`, `quoted-printable` and `base64`.
|
||||
///
|
||||
/// If `String` is passed, line endings are converted to `CRLF`.
|
||||
///
|
||||
/// If `buf` is valid utf-8 a `String` should be supplied, as `String`s
|
||||
/// can be encoded as `7bit` or `quoted-printable`, while `Vec<u8>` always
|
||||
/// get encoded as `base64`.
|
||||
pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
|
||||
let buf: MaybeString = buf.into();
|
||||
let mut buf: MaybeString = buf.into();
|
||||
|
||||
let encoding = buf.encoding();
|
||||
let encoding = buf.encoding(false);
|
||||
buf.encode_crlf();
|
||||
Self::new_impl(buf.into(), encoding)
|
||||
}
|
||||
|
||||
@@ -44,6 +46,8 @@ impl Body {
|
||||
///
|
||||
/// [`Body::new`] is generally the better option.
|
||||
///
|
||||
/// If `String` is passed, line endings are converted to `CRLF`.
|
||||
///
|
||||
/// Returns an [`Err`] giving back the supplied `buf`, in case the chosen
|
||||
/// encoding would have resulted into `buf` being encoded
|
||||
/// into an invalid body.
|
||||
@@ -51,12 +55,28 @@ impl Body {
|
||||
buf: B,
|
||||
encoding: ContentTransferEncoding,
|
||||
) -> Result<Self, Vec<u8>> {
|
||||
let 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());
|
||||
}
|
||||
|
||||
buf.encode_crlf();
|
||||
Ok(Self::new_impl(buf.into(), encoding))
|
||||
}
|
||||
|
||||
@@ -82,36 +102,13 @@ impl Body {
|
||||
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
|
||||
}
|
||||
ContentTransferEncoding::Base64 => {
|
||||
let base64_len = buf.len() * 4 / 3 + 4;
|
||||
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
|
||||
let len = email_encoding::body::base64::encoded_len(buf.len());
|
||||
|
||||
let mut out = Vec::with_capacity(base64_endings_len);
|
||||
{
|
||||
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH);
|
||||
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
|
||||
let mut out = String::with_capacity(len);
|
||||
email_encoding::body::base64::encode(&buf, &mut out)
|
||||
.expect("encode body as base64");
|
||||
|
||||
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails");
|
||||
|
||||
// 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)
|
||||
Self::dangerous_pre_encoded(out.into_bytes(), ContentTransferEncoding::Base64)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,33 +141,28 @@ impl Body {
|
||||
impl 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
|
||||
/// characters, with no lines longer than 1000 characters, then 7bit
|
||||
/// encoding will be used, else quoted-printable will be chosen.
|
||||
///
|
||||
/// If the `MaybeString` was instead created from a `Vec<u8>`, base64 encoding is always
|
||||
/// chosen.
|
||||
///
|
||||
/// `8bit` and `binary` encodings are never returned, as they may not be
|
||||
/// supported by all SMTP servers.
|
||||
pub fn encoding(&self) -> ContentTransferEncoding {
|
||||
match &self {
|
||||
Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit,
|
||||
// TODO: consider when base64 would be a better option because of output size
|
||||
Self::String(_) => ContentTransferEncoding::QuotedPrintable,
|
||||
Self::Binary(_) => ContentTransferEncoding::Base64,
|
||||
/// The `binary` encoding is never returned
|
||||
fn encoding(&self, supports_utf8: bool) -> ContentTransferEncoding {
|
||||
use email_encoding::body::Encoding;
|
||||
|
||||
let output = match self {
|
||||
Self::String(s) => Encoding::choose(s.as_str(), supports_utf8),
|
||||
Self::Binary(b) => Encoding::choose(b.as_slice(), supports_utf8),
|
||||
};
|
||||
|
||||
match output {
|
||||
Encoding::SevenBit => ContentTransferEncoding::SevenBit,
|
||||
Encoding::EightBit => ContentTransferEncoding::EightBit,
|
||||
Encoding::QuotedPrintable => ContentTransferEncoding::QuotedPrintable,
|
||||
Encoding::Base64 => ContentTransferEncoding::Base64,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// Encode line endings to CRLF if the variant is `String`
|
||||
fn encode_crlf(&mut self) {
|
||||
match self {
|
||||
Self::String(string) => in_place_crlf_line_endings(string),
|
||||
Self::Binary(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,6 +181,7 @@ impl MaybeString {
|
||||
/// **NOTE:** if using the specified `encoding` would result into a malformed
|
||||
/// body, this will panic!
|
||||
pub trait IntoBody {
|
||||
/// Encode as valid body
|
||||
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body;
|
||||
}
|
||||
|
||||
@@ -255,76 +248,46 @@ impl Deref for MaybeString {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether it contains only US-ASCII characters,
|
||||
/// and no lines are longer than 1000 characters including the `\n` character.
|
||||
/// In place conversion to CRLF line endings
|
||||
fn in_place_crlf_line_endings(string: &mut String) {
|
||||
let indices = find_all_lf_char_indices(string);
|
||||
|
||||
for i in indices {
|
||||
// this relies on `indices` being in reverse order
|
||||
string.insert(i, '\r');
|
||||
}
|
||||
}
|
||||
|
||||
/// Find indices to all places where `\r` should be inserted
|
||||
/// in order to make `s` have CRLF line endings
|
||||
///
|
||||
/// Most efficient content encoding available
|
||||
fn is_7bit_encoded(buf: &[u8]) -> bool {
|
||||
buf.is_ascii() && !contains_too_long_lines(buf)
|
||||
}
|
||||
/// The list is reversed, which is more efficient.
|
||||
fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
|
||||
let mut indices = Vec::new();
|
||||
|
||||
/// 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;
|
||||
let mut found_lf = false;
|
||||
for (i, c) in s.char_indices().rev() {
|
||||
if mem::take(&mut found_lf) && c != '\r' {
|
||||
// the previous character was `\n`, but this isn't a `\r`
|
||||
indices.push(i + c.len_utf8());
|
||||
}
|
||||
|
||||
Ok(write_len)
|
||||
found_lf = c == '\n';
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
if found_lf {
|
||||
// the first character is `\n`
|
||||
indices.push(0);
|
||||
}
|
||||
|
||||
indices
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Body, ContentTransferEncoding};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
|
||||
|
||||
#[test]
|
||||
fn seven_bit_detect() {
|
||||
@@ -456,13 +419,10 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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.as_ref(),
|
||||
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
|
||||
);
|
||||
assert_eq!(encoded.as_ref(), b"Questo messaggio =C3=A8 corto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -494,14 +454,17 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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);
|
||||
println!("{}", std::str::from_utf8(encoded.as_ref()).unwrap());
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
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",
|
||||
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5"
|
||||
"Se lo standard =F0=9F=93=AC fosse stato pi=C3=B9 semplice avremmo finito mo=\r\n",
|
||||
"lto prima."
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
@@ -509,27 +472,31 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(encoding, ContentTransferEncoding::Base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_bytes() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
ContentTransferEncoding::Base64,
|
||||
)
|
||||
.unwrap();
|
||||
let encoded =
|
||||
Body::new_with_encoding(vec![0; 80], ContentTransferEncoding::Base64).unwrap();
|
||||
|
||||
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]
|
||||
fn base64_encode_bytes_wrapping() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
|
||||
ContentTransferEncoding::Base64,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -578,4 +545,31 @@ mod test {
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf() {
|
||||
let mut string = String::from("Send me a ✉️\nwith\nlettre!\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(string, "Send me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harsh_crlf() {
|
||||
let mut string = String::from("\n\nSend me a ✉️\r\n\nwith\n\nlettre!\n\r\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(
|
||||
string,
|
||||
"\r\n\r\nSend me a ✉️\r\n\r\nwith\r\n\r\nlettre!\r\n\r\n😀"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf_noop() {
|
||||
let mut string = String::from("\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(string, "\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
}
|
||||
}
|
||||
|
||||
616
src/message/dkim.rs
Normal file
616
src/message/dkim.rs
Normal file
@@ -0,0 +1,616 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use ed25519_dalek::Signer;
|
||||
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs1v15::Pkcs1v15Sign, RsaPrivateKey};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::message::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Headers, Message,
|
||||
};
|
||||
|
||||
/// Describe Dkim Canonicalization to apply to either body or headers
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum DkimCanonicalizationType {
|
||||
Simple,
|
||||
Relaxed,
|
||||
}
|
||||
|
||||
impl Display for DkimCanonicalizationType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
DkimCanonicalizationType::Simple => "simple",
|
||||
DkimCanonicalizationType::Relaxed => "relaxed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe Canonicalization to be applied before signing
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct DkimCanonicalization {
|
||||
pub header: DkimCanonicalizationType,
|
||||
pub body: DkimCanonicalizationType,
|
||||
}
|
||||
|
||||
impl Default for DkimCanonicalization {
|
||||
fn default() -> Self {
|
||||
DkimCanonicalization {
|
||||
header: DkimCanonicalizationType::Simple,
|
||||
body: DkimCanonicalizationType::Relaxed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format canonicalization to be shown in Dkim header
|
||||
impl Display for DkimCanonicalization {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", self.header, self.body)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe the algorithm used for signing the message
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum DkimSigningAlgorithm {
|
||||
Rsa,
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
impl Display for DkimSigningAlgorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
DkimSigningAlgorithm::Rsa => "rsa",
|
||||
DkimSigningAlgorithm::Ed25519 => "ed25519",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe DkimSigning key error
|
||||
#[derive(Debug)]
|
||||
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InnerDkimSigningKeyError {
|
||||
Base64(base64::DecodeError),
|
||||
Rsa(rsa::pkcs1::Error),
|
||||
Ed25519(ed25519_dalek::ed25519::Error),
|
||||
}
|
||||
|
||||
impl Display for DkimSigningKeyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match &self.0 {
|
||||
InnerDkimSigningKeyError::Base64(_err) => "base64 decode error",
|
||||
InnerDkimSigningKeyError::Rsa(_err) => "rsa decode error",
|
||||
InnerDkimSigningKeyError::Ed25519(_err) => "ed25519 decode error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for DkimSigningKeyError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
Some(match &self.0 {
|
||||
InnerDkimSigningKeyError::Base64(err) => err,
|
||||
InnerDkimSigningKeyError::Rsa(err) => err,
|
||||
InnerDkimSigningKeyError::Ed25519(err) => err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe a signing key to be carried by DkimConfig struct
|
||||
#[derive(Debug)]
|
||||
pub struct DkimSigningKey(InnerDkimSigningKey);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InnerDkimSigningKey {
|
||||
Rsa(RsaPrivateKey),
|
||||
Ed25519(ed25519_dalek::SigningKey),
|
||||
}
|
||||
|
||||
impl DkimSigningKey {
|
||||
pub fn new(
|
||||
private_key: &str,
|
||||
algorithm: DkimSigningAlgorithm,
|
||||
) -> Result<DkimSigningKey, DkimSigningKeyError> {
|
||||
Ok(Self(match algorithm {
|
||||
DkimSigningAlgorithm::Rsa => InnerDkimSigningKey::Rsa(
|
||||
RsaPrivateKey::from_pkcs1_pem(private_key)
|
||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
|
||||
),
|
||||
DkimSigningAlgorithm::Ed25519 => {
|
||||
InnerDkimSigningKey::Ed25519(ed25519_dalek::SigningKey::from_bytes(
|
||||
&crate::base64::decode(private_key)
|
||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)))?
|
||||
.try_into()
|
||||
.map_err(|_| {
|
||||
DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(
|
||||
ed25519_dalek::ed25519::Error::new(),
|
||||
))
|
||||
})?,
|
||||
))
|
||||
}
|
||||
}))
|
||||
}
|
||||
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
|
||||
match self.0 {
|
||||
InnerDkimSigningKey::Rsa(_) => DkimSigningAlgorithm::Rsa,
|
||||
InnerDkimSigningKey::Ed25519(_) => DkimSigningAlgorithm::Ed25519,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct to describe Dkim configuration applied when signing a message
|
||||
#[derive(Debug)]
|
||||
pub struct DkimConfig {
|
||||
/// The name of the key published in DNS
|
||||
selector: String,
|
||||
/// The domain for which we sign the message
|
||||
domain: String,
|
||||
/// The private key in PKCS1 string format
|
||||
private_key: DkimSigningKey,
|
||||
/// A list of header names to be included in the signature. Signing of more than one
|
||||
/// header with the same name is not supported
|
||||
headers: Vec<HeaderName>,
|
||||
/// The signing algorithm to be used when signing
|
||||
canonicalization: DkimCanonicalization,
|
||||
}
|
||||
|
||||
impl DkimConfig {
|
||||
/// Create a default signature configuration with a set of headers and "simple/relaxed"
|
||||
/// canonicalization
|
||||
pub fn default_config(
|
||||
selector: String,
|
||||
domain: String,
|
||||
private_key: DkimSigningKey,
|
||||
) -> DkimConfig {
|
||||
DkimConfig {
|
||||
selector,
|
||||
domain,
|
||||
private_key,
|
||||
headers: vec![
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
],
|
||||
canonicalization: DkimCanonicalization {
|
||||
header: DkimCanonicalizationType::Simple,
|
||||
body: DkimCanonicalizationType::Relaxed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a DkimConfig
|
||||
pub fn new(
|
||||
selector: String,
|
||||
domain: String,
|
||||
private_key: DkimSigningKey,
|
||||
headers: Vec<HeaderName>,
|
||||
canonicalization: DkimCanonicalization,
|
||||
) -> DkimConfig {
|
||||
DkimConfig {
|
||||
selector,
|
||||
domain,
|
||||
private_key,
|
||||
headers,
|
||||
canonicalization,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Headers struct with a Dkim-Signature Header created from given parameters
|
||||
fn dkim_header_format(
|
||||
config: &DkimConfig,
|
||||
timestamp: u64,
|
||||
headers_list: &str,
|
||||
body_hash: &str,
|
||||
signature: &str,
|
||||
) -> Headers {
|
||||
let mut headers = Headers::new();
|
||||
let header_name =
|
||||
dkim_canonicalize_header_tag("DKIM-Signature", config.canonicalization.header);
|
||||
let header_name = HeaderName::new_from_ascii(header_name.into()).unwrap();
|
||||
headers.insert_raw(HeaderValue::new(header_name, format!("v=1; a={signing_algorithm}-sha256; d={domain}; s={selector}; c={canon}; q=dns/txt; t={timestamp}; h={headers_list}; bh={body_hash}; b={signature}",domain=config.domain, selector=config.selector,canon=config.canonicalization,timestamp=timestamp,headers_list=headers_list,body_hash=body_hash,signature=signature,signing_algorithm=config.private_key.get_signing_algorithm())));
|
||||
headers
|
||||
}
|
||||
|
||||
/// Canonicalize the body of an email
|
||||
fn dkim_canonicalize_body(
|
||||
mut body: &[u8],
|
||||
canonicalization: DkimCanonicalizationType,
|
||||
) -> Cow<'_, [u8]> {
|
||||
match canonicalization {
|
||||
DkimCanonicalizationType::Simple => {
|
||||
// Remove empty lines at end
|
||||
while body.ends_with(b"\r\n\r\n") {
|
||||
body = &body[..body.len() - 2];
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
|
||||
let mut r = String::with_capacity(headers.len());
|
||||
|
||||
fn skip_whitespace(h: &str) -> &str {
|
||||
match h.as_bytes().first() {
|
||||
Some(b' ' | b'\t') => skip_whitespace(&h[1..]),
|
||||
_ => h,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
fn dkim_canonicalize_header_tag(
|
||||
name: &str,
|
||||
canonicalization: DkimCanonicalizationType,
|
||||
) -> Cow<'_, str> {
|
||||
match canonicalization {
|
||||
DkimCanonicalizationType::Simple => Cow::Borrowed(name),
|
||||
DkimCanonicalizationType::Relaxed => Cow::Owned(name.to_lowercase()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonicalize signed headers passed as headers_list among mail_headers using canonicalization
|
||||
fn dkim_canonicalize_headers<'a>(
|
||||
headers_list: impl IntoIterator<Item = &'a str>,
|
||||
mail_headers: &Headers,
|
||||
canonicalization: DkimCanonicalizationType,
|
||||
) -> 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 {
|
||||
DkimCanonicalizationType::Simple => serialized,
|
||||
DkimCanonicalizationType::Relaxed => dkim_canonicalize_headers_relaxed(&serialized),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
|
||||
/// dkim_config
|
||||
|
||||
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
|
||||
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)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
let headers = message.headers();
|
||||
let body_hash = Sha256::digest(dkim_canonicalize_body(
|
||||
&message.body_raw(),
|
||||
dkim_config.canonicalization.body,
|
||||
));
|
||||
let bh = crate::base64::encode(body_hash);
|
||||
let mut signed_headers_list =
|
||||
dkim_config
|
||||
.headers
|
||||
.iter()
|
||||
.fold(String::new(), |mut list, header| {
|
||||
if !list.is_empty() {
|
||||
list.push(':');
|
||||
}
|
||||
|
||||
list.push_str(header);
|
||||
list
|
||||
});
|
||||
if let DkimCanonicalizationType::Relaxed = dkim_config.canonicalization.header {
|
||||
signed_headers_list.make_ascii_lowercase();
|
||||
}
|
||||
let dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
|
||||
let signed_headers = dkim_canonicalize_headers(
|
||||
dkim_config.headers.iter().map(|h| h.as_ref()),
|
||||
headers,
|
||||
dkim_config.canonicalization.header,
|
||||
);
|
||||
let canonicalized_dkim_header = dkim_canonicalize_headers(
|
||||
["DKIM-Signature"],
|
||||
&dkim_header,
|
||||
dkim_config.canonicalization.header,
|
||||
);
|
||||
let mut hashed_headers = Sha256::new();
|
||||
hashed_headers.update(signed_headers.as_bytes());
|
||||
hashed_headers.update(canonicalized_dkim_header.trim_end().as_bytes());
|
||||
let hashed_headers = hashed_headers.finalize();
|
||||
let signature = match &dkim_config.private_key.0 {
|
||||
InnerDkimSigningKey::Rsa(private_key) => crate::base64::encode(
|
||||
private_key
|
||||
.sign(Pkcs1v15Sign::new::<Sha256>(), &hashed_headers)
|
||||
.unwrap(),
|
||||
),
|
||||
InnerDkimSigningKey::Ed25519(private_key) => {
|
||||
crate::base64::encode(private_key.sign(&hashed_headers).to_bytes())
|
||||
}
|
||||
};
|
||||
let dkim_header = dkim_header_format(
|
||||
dkim_config,
|
||||
timestamp,
|
||||
&signed_headers_list,
|
||||
&bh,
|
||||
&signature,
|
||||
);
|
||||
message.headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("DKIM-Signature"),
|
||||
dkim_header.get_raw("DKIM-Signature").unwrap().to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Header, Message,
|
||||
},
|
||||
dkim_canonicalize_body, dkim_canonicalize_headers, dkim_sign_fixed_time,
|
||||
DkimCanonicalization, DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm,
|
||||
DkimSigningKey,
|
||||
};
|
||||
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)]
|
||||
struct TestHeader(String);
|
||||
|
||||
impl Header for TestHeader {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Test")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
|
||||
Ok(Self(s.into()))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
HeaderValue::new(Self::name(), self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn test_message() -> Message {
|
||||
Message::builder()
|
||||
.from("Test O'Leary <test+ezrz@example.net>".parse().unwrap())
|
||||
.to("Test2 <test2@example.org>".parse().unwrap())
|
||||
.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 ë")
|
||||
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_owned()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_simple_canonicalize() {
|
||||
let message = test_message();
|
||||
dbg!(message.headers.to_string());
|
||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_relaxed_canonicalize() {
|
||||
let message = test_message();
|
||||
dbg!(message.headers.to_string());
|
||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
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 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::Simple,
|
||||
body: DkimCanonicalizationType::Simple,
|
||||
},
|
||||
),
|
||||
std::time::UNIX_EPOCH,
|
||||
);
|
||||
let signed = message.formatted();
|
||||
let signed = std::str::from_utf8(&signed).unwrap();
|
||||
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=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To;\r\n",
|
||||
" bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=;\r\n",
|
||||
" b=NhoIMMAALoSgu5lKAR0+MUQunOWnU7wpF9ORUFtpxq9sGZDo9AX43AMhFemyM5W204jpFwMU6pm7AMR1nOYBdSYye4yUALtvT2nqbJBwSh7JeYu+z22t1RFKp7qQR1il8aSrkbZuNMFHYuSEwW76QtKwcNqP4bQOzS9CzgQp0ABu8qwYPBr/EypykPTfqjtyN+ywrfdqjjGOzTpRGolH0hc3CrAETNjjHbNBgKgucXmXTN7hMRdzqWjeFPxizXwouwNAavFClPG0l33gXVArFWn+CkgA84G/s4zuJiF7QPZR87Pu4pw/vIlSXxH4a42W3tT19v9iBTH7X7ldYegtmQ==\r\n",
|
||||
"\r\n",
|
||||
"test\r\n",
|
||||
"\r\n",
|
||||
"test \ttest\r\n",
|
||||
"\r\n",
|
||||
"\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,45 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
|
||||
str::{from_utf8, FromStr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
header! {
|
||||
/// `Content-Id` header, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
|
||||
(ContentId, "Content-ID") => [String]
|
||||
}
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Transfer-Encoding` of the body
|
||||
///
|
||||
/// The `Message` builder takes care of choosing the most
|
||||
/// efficient encoding based on the chosen body, so in most
|
||||
/// use-caches this header shouldn't be set manually.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Default)]
|
||||
pub enum ContentTransferEncoding {
|
||||
/// ASCII
|
||||
SevenBit,
|
||||
/// Quoted-Printable encoding
|
||||
QuotedPrintable,
|
||||
/// base64 encoding
|
||||
#[default]
|
||||
Base64,
|
||||
// 8BITMIME
|
||||
/// Requires `8BITMIME`
|
||||
EightBit,
|
||||
/// Binary data
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::Base64
|
||||
impl Header for ContentTransferEncoding {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(s.parse()?)
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let val = self.to_string();
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,35 +69,12 @@ impl FromStr for ContentTransferEncoding {
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentTransferEncoding {
|
||||
fn header_name() -> &'static str {
|
||||
"Content-Transfer-Encoding"
|
||||
}
|
||||
|
||||
// FIXME HeaderError->HeaderError, same for result
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header))
|
||||
.and_then(|s| {
|
||||
s.parse::<ContentTransferEncoding>()
|
||||
.map_err(|_| HeaderError::Header)
|
||||
})
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&format!("{}", self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::ContentTransferEncoding;
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_transfer_encoding() {
|
||||
@@ -95,35 +82,35 @@ mod test {
|
||||
|
||||
headers.set(ContentTransferEncoding::SevenBit);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: 7bit\r\n"
|
||||
);
|
||||
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: 7bit\r\n");
|
||||
|
||||
headers.set(ContentTransferEncoding::Base64);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: base64\r\n"
|
||||
);
|
||||
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: base64\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("Content-Transfer-Encoding", "7bit");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"7bit".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::SevenBit)
|
||||
Some(ContentTransferEncoding::SevenBit)
|
||||
);
|
||||
|
||||
headers.set_raw("Content-Transfer-Encoding", "base64");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"base64".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::Base64)
|
||||
Some(ContentTransferEncoding::Base64)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
127
src/message/header/content_disposition.rs
Normal file
127
src/message/header/content_disposition.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Disposition` of an attachment
|
||||
///
|
||||
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ContentDisposition(HeaderValue);
|
||||
|
||||
impl ContentDisposition {
|
||||
/// An attachment which should be displayed inline into the message
|
||||
pub fn inline() -> Self {
|
||||
Self(HeaderValue::dangerous_new_pre_encoded(
|
||||
Self::name(),
|
||||
"inline".to_owned(),
|
||||
"inline".to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
/// An attachment which should be displayed inline into the message, but that also
|
||||
/// species the filename in case it is downloaded
|
||||
pub fn inline_with_name(file_name: &str) -> Self {
|
||||
Self::with_name("inline", file_name)
|
||||
}
|
||||
|
||||
/// An attachment which is separate from the body of the message, and can be downloaded separately
|
||||
pub fn attachment(file_name: &str) -> Self {
|
||||
Self::with_name("attachment", file_name)
|
||||
}
|
||||
|
||||
fn with_name(kind: &str, file_name: &str) -> Self {
|
||||
let raw_value = format!("{kind}; filename=\"{file_name}\"");
|
||||
|
||||
let mut encoded_value = String::new();
|
||||
let line_len = "Content-Disposition: ".len();
|
||||
{
|
||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||
w.write_str(kind).expect("writing `kind` returned an error");
|
||||
w.write_char(';').expect("writing `;` returned an error");
|
||||
w.space();
|
||||
|
||||
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
|
||||
.expect("some Write implementation returned an error");
|
||||
}
|
||||
|
||||
Self(HeaderValue::dangerous_new_pre_encoded(
|
||||
Self::name(),
|
||||
raw_value,
|
||||
encoded_value,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentDisposition {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Disposition")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
match (s.split_once(';'), s) {
|
||||
(_, "inline") => Ok(Self::inline()),
|
||||
(Some((kind @ ("inline" | "attachment"), file_name)), _) => file_name
|
||||
.split_once(" filename=\"")
|
||||
.and_then(|(_, file_name)| file_name.strip_suffix('"'))
|
||||
.map(|file_name| Self::with_name(kind, file_name))
|
||||
.ok_or_else(|| "Unsupported ContentDisposition value".into()),
|
||||
_ => Err("Unsupported ContentDisposition value".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::ContentDisposition;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_disposition() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentDisposition::inline());
|
||||
|
||||
assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");
|
||||
|
||||
headers.set(ContentDisposition::attachment("something.txt"));
|
||||
|
||||
assert_eq!(
|
||||
format!("{headers}"),
|
||||
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_disposition() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||
"inline".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentDisposition>(),
|
||||
Some(ContentDisposition::inline())
|
||||
);
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||
"attachment; filename=\"something.txt\"".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentDisposition>(),
|
||||
Some(ContentDisposition::attachment("something.txt"))
|
||||
);
|
||||
}
|
||||
}
|
||||
193
src/message/header/content_type.rs
Normal file
193
src/message/header/content_type.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Type` of the body
|
||||
///
|
||||
/// This struct can represent any valid [MIME type], which can be parsed via
|
||||
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
|
||||
///
|
||||
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
|
||||
///
|
||||
/// [MIME type]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContentType(Mime);
|
||||
|
||||
impl ContentType {
|
||||
/// A `ContentType` of type `text/plain; charset=utf-8`
|
||||
///
|
||||
/// Indicates that the body is in utf-8 encoded plain text.
|
||||
pub const TEXT_PLAIN: ContentType = Self::from_mime(mime::TEXT_PLAIN_UTF_8);
|
||||
|
||||
/// A `ContentType` of type `text/html; charset=utf-8`
|
||||
///
|
||||
/// Indicates that the body is in utf-8 encoded html.
|
||||
pub const TEXT_HTML: ContentType = Self::from_mime(mime::TEXT_HTML_UTF_8);
|
||||
|
||||
/// Parse `s` into `ContentType`
|
||||
pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
|
||||
Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
|
||||
}
|
||||
|
||||
pub(crate) const fn from_mime(mime: Mime) -> Self {
|
||||
Self(mime)
|
||||
}
|
||||
|
||||
pub(crate) fn as_ref(&self) -> &Mime {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentType {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Type")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(Self(s.parse()?))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
HeaderValue::new(Self::name(), self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ContentType {
|
||||
type Err = ContentTypeErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mime03")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "mime03")))]
|
||||
impl From<Mime> for ContentType {
|
||||
fn from(mime: Mime) -> Self {
|
||||
Self::from_mime(mime)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error occurred while trying to [`ContentType::parse`].
|
||||
#[derive(Debug)]
|
||||
pub struct ContentTypeErr(mime::FromStrError);
|
||||
|
||||
impl StdError for ContentTypeErr {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTypeErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
// -- Serialization and Deserialization --
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde {
|
||||
use std::fmt;
|
||||
|
||||
use serde::{
|
||||
de::{self, Deserialize, Deserializer, Visitor},
|
||||
ser::{Serialize, Serializer},
|
||||
};
|
||||
|
||||
use super::ContentType;
|
||||
|
||||
impl Serialize for ContentType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_newtype_struct("ContentType", &format!("{}", &self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ContentType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ContentTypeVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ContentTypeVisitor {
|
||||
type Value = ContentType;
|
||||
|
||||
// The error message which states what the Visitor expects to
|
||||
// receive
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a ContentType string like `text/plain`")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, mime: &str) -> Result<ContentType, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match ContentType::parse(mime) {
|
||||
Ok(content_type) => Ok(content_type),
|
||||
Err(_) => Err(E::custom(format!(
|
||||
"Couldn't parse the following MIME-Type: {mime}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(ContentTypeVisitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::ContentType;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_type() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentType::TEXT_PLAIN);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
);
|
||||
|
||||
headers.set(ContentType::TEXT_HTML);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Content-Type: text/html; charset=utf-8\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_type() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Type"),
|
||||
"text/plain; charset=utf-8".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Type"),
|
||||
"text/html; charset=utf-8".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
|
||||
}
|
||||
}
|
||||
135
src/message/header/date.rs
Normal file
135
src/message/header/date.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use httpdate::HttpDate;
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
/// Message `Date` header
|
||||
///
|
||||
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Date(HttpDate);
|
||||
|
||||
impl Date {
|
||||
/// Build a `Date` from [`SystemTime`]
|
||||
pub fn new(st: SystemTime) -> Self {
|
||||
Self(st.into())
|
||||
}
|
||||
|
||||
/// Get the current date
|
||||
///
|
||||
/// Shortcut for `Date::new(SystemTime::now())`
|
||||
pub fn now() -> Self {
|
||||
Self::new(SystemTime::now())
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for Date {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Date")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mut s = String::from(s);
|
||||
if s.ends_with("+0000") {
|
||||
// The httpdate crate expects the `Date` to end in ` GMT`, but email
|
||||
// uses `+0000` to indicate UTC, so we crudely fix this issue here.
|
||||
|
||||
s.truncate(s.len() - "+0000".len());
|
||||
s.push_str("GMT");
|
||||
}
|
||||
|
||||
Ok(Self(s.parse::<HttpDate>()?))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let mut val = self.0.to_string();
|
||||
if val.ends_with(" GMT") {
|
||||
// The httpdate crate always appends ` GMT` to the end of the string,
|
||||
// but this is considered an obsolete date format for email
|
||||
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
|
||||
// so we replace `GMT` with `+0000`
|
||||
val.truncate(val.len() - "GMT".len());
|
||||
val.push_str("+0000");
|
||||
}
|
||||
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemTime> for Date {
|
||||
fn from(st: SystemTime) -> Self {
|
||||
Self::new(st)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Date> for SystemTime {
|
||||
fn from(this: Date) -> SystemTime {
|
||||
this.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::Date;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_date() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
headers.set(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_owned()
|
||||
);
|
||||
|
||||
// Tue, 15 Nov 1994 08:12:32 GMT
|
||||
headers.set(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Date: Tue, 15 Nov 1994 08:12:32 +0000\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_date() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
"Tue, 15 Nov 1994 08:12:31 +0000".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Date>(),
|
||||
Some(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
|
||||
))
|
||||
);
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
"Tue, 15 Nov 1994 08:12:32 +0000".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Date>(),
|
||||
Some(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
use crate::message::{
|
||||
mailbox::{Mailbox, Mailboxes},
|
||||
utf8_b,
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::{
|
||||
message::mailbox::{Mailbox, Mailboxes},
|
||||
BoxError,
|
||||
};
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, slice::Iter, str::from_utf8};
|
||||
|
||||
/// Header which can contains multiple mailboxes
|
||||
pub trait MailboxesHeader {
|
||||
@@ -16,27 +14,42 @@ pub trait MailboxesHeader {
|
||||
macro_rules! mailbox_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailbox);
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct $type_name(Mailbox);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self> where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized {
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_mailboxes)
|
||||
.and_then(|mbs| {
|
||||
mbs.into_single().ok_or(HeaderError::Header)
|
||||
}).map($type_name)
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mailbox: Mailbox = s.parse()?;
|
||||
Ok(Self(mailbox))
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&self.0.recode_name(utf8_b::encode))
|
||||
fn display(&self) -> HeaderValue {
|
||||
let mut encoded_value = String::new();
|
||||
let line_len = $header_name.len() + ": ".len();
|
||||
{
|
||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
||||
}
|
||||
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Mailbox> for $type_name {
|
||||
#[inline]
|
||||
fn from(mailbox: Mailbox) -> Self {
|
||||
Self(mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<$type_name> for Mailbox {
|
||||
#[inline]
|
||||
fn from(this: $type_name) -> Mailbox {
|
||||
this.0
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -45,8 +58,8 @@ macro_rules! mailbox_header {
|
||||
macro_rules! mailboxes_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailboxes);
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct $type_name(pub(crate) Mailboxes);
|
||||
|
||||
impl MailboxesHeader for $type_name {
|
||||
fn join_mailboxes(&mut self, other: Self) {
|
||||
@@ -55,23 +68,38 @@ macro_rules! mailboxes_header {
|
||||
}
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mailbox: Mailboxes = s.parse()?;
|
||||
Ok(Self(mailbox))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let mut encoded_value = String::new();
|
||||
let line_len = $header_name.len() + ": ".len();
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_mailboxes)
|
||||
.map($type_name)
|
||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
format_mailboxes(self.0.iter(), f)
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Mailboxes> for $type_name {
|
||||
#[inline]
|
||||
fn from(mailboxes: Mailboxes) -> Self {
|
||||
Self(mailboxes)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<$type_name> for Mailboxes {
|
||||
#[inline]
|
||||
fn from(this: $type_name) -> Mailboxes {
|
||||
this.0
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -146,26 +174,12 @@ mailboxes_header! {
|
||||
(Bcc, "Bcc")
|
||||
}
|
||||
|
||||
fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
|
||||
if let Ok(src) = from_utf8(raw) {
|
||||
if let Ok(mbs) = src.parse() {
|
||||
return Ok(mbs);
|
||||
}
|
||||
}
|
||||
Err(HeaderError::Header)
|
||||
}
|
||||
|
||||
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&Mailboxes::from(
|
||||
mbs.map(|mb| mb.recode_name(utf8_b::encode))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{From, Mailbox, Mailboxes};
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_single_without_name() {
|
||||
@@ -174,17 +188,17 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n");
|
||||
assert_eq!(headers.to_string(), "From: kayo@example.com\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_single_with_name() {
|
||||
let from = Mailboxes::new().with("K. <kayo@example.com>".parse().unwrap());
|
||||
let from = Mailboxes::new().with("Kayo <kayo@example.com>".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n");
|
||||
assert_eq!(headers.to_string(), "From: Kayo <kayo@example.com>\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -197,7 +211,7 @@ mod test {
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
headers.to_string(),
|
||||
"From: kayo@example.com, pony@domain.tld\r\n"
|
||||
);
|
||||
}
|
||||
@@ -205,7 +219,7 @@ mod test {
|
||||
#[test]
|
||||
fn format_multi_with_name() {
|
||||
let from = vec![
|
||||
"K. <kayo@example.com>".parse().unwrap(),
|
||||
"Kayo <kayo@example.com>".parse().unwrap(),
|
||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||
];
|
||||
|
||||
@@ -213,8 +227,8 @@ mod test {
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
|
||||
headers.to_string(),
|
||||
"From: Kayo <kayo@example.com>, \"Pony P.\" <pony@domain.tld>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,7 +240,7 @@ mod test {
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
headers.to_string(),
|
||||
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
@@ -236,9 +250,12 @@ mod test {
|
||||
let from = vec!["kayo@example.com".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "kayo@example.com");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"kayo@example.com".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from)));
|
||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -246,9 +263,12 @@ mod test {
|
||||
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "K. <kayo@example.com>");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"K. <kayo@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from)));
|
||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -259,9 +279,12 @@ mod test {
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "kayo@example.com, pony@domain.tld");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"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())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -272,18 +295,65 @@ mod test {
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"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())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_with_utf8_name() {
|
||||
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()];
|
||||
fn parse_multi_with_name_containing_comma() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"\"Test, test\" <1@example.com>".parse().unwrap(),
|
||||
"\"Test2, test2\" <2@example.com>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>");
|
||||
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())));
|
||||
}
|
||||
|
||||
#[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())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_with_name_containing_comma_last_broken() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"\"Test, test\" <1@example.com>, \"Test2, test2\"".to_owned(),
|
||||
));
|
||||
|
||||
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>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,720 @@
|
||||
//! Headers widely used in email messages
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
error::Error,
|
||||
fmt::{self, Display, Formatter, Write},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
pub use self::{
|
||||
content::*,
|
||||
content_disposition::ContentDisposition,
|
||||
content_type::{ContentType, ContentTypeErr},
|
||||
date::Date,
|
||||
mailbox::*,
|
||||
special::*,
|
||||
textual::*,
|
||||
};
|
||||
use crate::BoxError;
|
||||
|
||||
mod content;
|
||||
mod content_disposition;
|
||||
mod content_type;
|
||||
mod date;
|
||||
mod mailbox;
|
||||
mod special;
|
||||
mod textual;
|
||||
|
||||
pub use self::{content::*, mailbox::*, special::*, textual::*};
|
||||
/// Represents an email header
|
||||
///
|
||||
/// Email header as defined in [RFC5322](https://datatracker.ietf.org/doc/html/rfc5322) and extensions.
|
||||
pub trait Header: Clone {
|
||||
fn name() -> HeaderName;
|
||||
|
||||
pub use hyperx::header::{
|
||||
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
|
||||
DispositionType, Header, Headers, HttpDate as EmailDate,
|
||||
};
|
||||
fn parse(s: &str) -> Result<Self, BoxError>;
|
||||
|
||||
fn display(&self) -> HeaderValue;
|
||||
}
|
||||
|
||||
/// A set of email headers
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Headers {
|
||||
headers: Vec<HeaderValue>,
|
||||
}
|
||||
|
||||
impl Headers {
|
||||
/// Create an empty `Headers`
|
||||
///
|
||||
/// This function does not allocate.
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
headers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty `Headers` with a pre-allocated capacity
|
||||
///
|
||||
/// Pre-allocates a capacity of at least `capacity`.
|
||||
#[inline]
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
headers: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a copy of a `Header` present in `Headers`
|
||||
///
|
||||
/// Returns `None` if `Header` isn't present in `Headers`.
|
||||
pub fn get<H: Header>(&self) -> Option<H> {
|
||||
self.get_raw(&H::name())
|
||||
.and_then(|raw_value| H::parse(raw_value).ok())
|
||||
}
|
||||
|
||||
/// Sets `Header` into `Headers`, overriding `Header` if it
|
||||
/// was already present in `Headers`
|
||||
pub fn set<H: Header>(&mut self, header: H) {
|
||||
self.insert_raw(header.display());
|
||||
}
|
||||
|
||||
/// Remove `Header` from `Headers`, returning it
|
||||
///
|
||||
/// Returns `None` if `Header` isn't in `Headers`.
|
||||
pub fn remove<H: Header>(&mut self) -> Option<H> {
|
||||
self.remove_raw(&H::name())
|
||||
.and_then(|value| H::parse(&value.raw_value).ok())
|
||||
}
|
||||
|
||||
/// Clears `Headers`, removing all headers from it
|
||||
///
|
||||
/// Any pre-allocated capacity is left untouched.
|
||||
#[inline]
|
||||
pub fn clear(&mut self) {
|
||||
self.headers.clear();
|
||||
}
|
||||
|
||||
/// Returns a reference to the raw value of header `name`
|
||||
///
|
||||
/// Returns `None` if `name` isn't present in `Headers`.
|
||||
pub fn get_raw(&self, name: &str) -> Option<&str> {
|
||||
self.find_header(name).map(|value| value.raw_value.as_str())
|
||||
}
|
||||
|
||||
/// Inserts a raw header into `Headers`, overriding `value` if it
|
||||
/// was already present in `Headers`.
|
||||
pub fn insert_raw(&mut self, value: HeaderValue) {
|
||||
match self.find_header_mut(&value.name) {
|
||||
Some(current_value) => {
|
||||
*current_value = value;
|
||||
}
|
||||
None => {
|
||||
self.headers.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a raw header from `Headers`, returning it
|
||||
///
|
||||
/// Returns `None` if `name` isn't present in `Headers`.
|
||||
pub fn remove_raw(&mut self, name: &str) -> Option<HeaderValue> {
|
||||
self.find_header_index(name).map(|i| self.headers.remove(i))
|
||||
}
|
||||
|
||||
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
|
||||
self.headers
|
||||
.iter()
|
||||
.find(|value| name.eq_ignore_ascii_case(&value.name))
|
||||
}
|
||||
|
||||
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
|
||||
self.headers
|
||||
.iter_mut()
|
||||
.find(|value| name.eq_ignore_ascii_case(&value.name))
|
||||
}
|
||||
|
||||
fn find_header_index(&self, name: &str) -> Option<usize> {
|
||||
self.headers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_i, value)| name.eq_ignore_ascii_case(&value.name))
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Headers {
|
||||
/// Formats `Headers`, ready to put them into an email
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for value in &self.headers {
|
||||
f.write_str(&value.name)?;
|
||||
f.write_str(": ")?;
|
||||
f.write_str(&value.encoded_value)?;
|
||||
f.write_str("\r\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A possible error when converting a `HeaderName` from another type.
|
||||
// comes from `http` crate
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct InvalidHeaderName {
|
||||
_priv: (),
|
||||
}
|
||||
|
||||
impl fmt::Debug for InvalidHeaderName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InvalidHeaderName")
|
||||
// skip _priv noise
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidHeaderName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("invalid header name")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for InvalidHeaderName {}
|
||||
|
||||
/// A valid header name
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeaderName(Cow<'static, str>);
|
||||
|
||||
impl HeaderName {
|
||||
/// Creates a new header name
|
||||
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
|
||||
if !ascii.is_empty()
|
||||
&& ascii.len() <= 76
|
||||
&& ascii.is_ascii()
|
||||
&& !ascii.contains(|c| c == ':' || c == ' ')
|
||||
{
|
||||
Ok(Self(Cow::Owned(ascii)))
|
||||
} else {
|
||||
Err(InvalidHeaderName { _priv: () })
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new header name, panics on invalid name
|
||||
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
|
||||
macro_rules! static_assert {
|
||||
($condition:expr) => {
|
||||
let _ = [()][(!($condition)) as usize];
|
||||
};
|
||||
}
|
||||
|
||||
static_assert!(!ascii.is_empty());
|
||||
static_assert!(ascii.len() <= 76);
|
||||
|
||||
let bytes = ascii.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
static_assert!(bytes[i].is_ascii());
|
||||
static_assert!(bytes[i] != b' ');
|
||||
static_assert!(bytes[i] != b':');
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Self(Cow::Borrowed(ascii))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HeaderName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for HeaderName {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for HeaderName {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
let s: &str = self.as_ref();
|
||||
s.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for HeaderName {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HeaderName> for HeaderName {
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
let s1: &str = self.as_ref();
|
||||
let s2: &str = other.as_ref();
|
||||
s1 == s2
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for HeaderName {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
let s: &str = self.as_ref();
|
||||
s == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HeaderName> for &str {
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
let s: &str = other.as_ref();
|
||||
*self == s
|
||||
}
|
||||
}
|
||||
|
||||
/// A safe for use header value
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HeaderValue {
|
||||
name: HeaderName,
|
||||
raw_value: String,
|
||||
encoded_value: String,
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut encoded_value = String::with_capacity(raw_value.len());
|
||||
HeaderValueEncoder::encode(&name, &raw_value, &mut encoded_value).unwrap();
|
||||
|
||||
Self {
|
||||
name,
|
||||
raw_value,
|
||||
encoded_value,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
name: HeaderName,
|
||||
raw_value: String,
|
||||
encoded_value: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
raw_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
|
||||
}
|
||||
}
|
||||
|
||||
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
|
||||
struct HeaderValueEncoder<'a> {
|
||||
writer: EmailWriter<'a>,
|
||||
encode_buf: String,
|
||||
}
|
||||
|
||||
impl<'a> HeaderValueEncoder<'a> {
|
||||
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
|
||||
let encoder = Self::new(name, f);
|
||||
encoder.format(value.split_inclusive(' '))
|
||||
}
|
||||
|
||||
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
|
||||
let line_len = name.len() + ": ".len();
|
||||
let writer = EmailWriter::new(writer, line_len, 0, false);
|
||||
|
||||
Self {
|
||||
writer,
|
||||
encode_buf: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format(mut self, words_iter: impl Iterator<Item = &'a str>) -> fmt::Result {
|
||||
for next_word in words_iter {
|
||||
let allowed = allowed_str(next_word);
|
||||
|
||||
if allowed {
|
||||
// This word only contains allowed characters
|
||||
|
||||
// the next word is allowed, but we may have accumulated some words to encode
|
||||
self.flush_encode_buf()?;
|
||||
|
||||
self.writer.folding().write_str(next_word)?;
|
||||
} else {
|
||||
// This word contains unallowed characters
|
||||
self.encode_buf.push_str(next_word);
|
||||
}
|
||||
}
|
||||
|
||||
self.flush_encode_buf()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush_encode_buf(&mut self) -> fmt::Result {
|
||||
if self.encode_buf.is_empty() {
|
||||
// nothing to encode
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let prefix = self.encode_buf.trim_end_matches(' ');
|
||||
email_encoding::headers::rfc2047::encode(prefix, &mut self.writer)?;
|
||||
|
||||
// TODO: add a better API for doing this in email-encoding
|
||||
let spaces = self.encode_buf.len() - prefix.len();
|
||||
for _ in 0..spaces {
|
||||
self.writer.space();
|
||||
}
|
||||
|
||||
self.encode_buf.clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn allowed_str(s: &str) -> bool {
|
||||
s.bytes().all(allowed_char)
|
||||
}
|
||||
|
||||
const fn allowed_char(c: u8) -> bool {
|
||||
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{HeaderName, HeaderValue, Headers, To};
|
||||
use crate::message::Mailboxes;
|
||||
|
||||
#[test]
|
||||
fn valid_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ascii_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("🌎")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spaces_in_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From ")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colons_in_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From:")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn const_valid_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_non_ascii_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("🌎");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_spaces_in_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_colons_in_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From:");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_empty_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("");
|
||||
}
|
||||
|
||||
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
|
||||
|
||||
#[test]
|
||||
fn format_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: John Doe <example@example.com>, Jean Dupont <jean@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
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_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"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",
|
||||
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_long_line() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello! This is lettre, and this\r\n",
|
||||
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||
" guess that's it!\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_very_long_line() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello!\r\n",
|
||||
" IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n",
|
||||
" I don't know\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_giant_word() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"Seán <sean@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special_emoji() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"🌎 <world@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: =?utf-8?b?8J+Mjg==?= <world@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special_with_folding() {
|
||||
let mut headers = Headers::new();
|
||||
let to = To::from(Mailboxes::from_iter([
|
||||
"🌍 <world@example.com>".parse().unwrap(),
|
||||
"🦆 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!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhiBFdmVyeXdo?=\r\n",
|
||||
" =?utf-8?b?ZXJl?= <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LI=?=\r\n",
|
||||
" =?utf-8?b?0LDQvSDQmNCy0LDQvdC+0LLQuNGH?= <ivanov@example.com>,\r\n",
|
||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[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(),
|
||||
));
|
||||
|
||||
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>, =?utf-8?b?U2U=?=\r\n",
|
||||
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_slice_on_char_boundary_bug() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_bad_stuff() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! \r\n This is \" bad \0. 👋".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Subject: Hello! =?utf-8?b?DQo=?= This is \" bad =?utf-8?b?AC4g8J+Riw==?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_everything() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
|
||||
)
|
||||
);
|
||||
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(),
|
||||
)
|
||||
);
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"Someone <somewhere@example.com>".to_owned(),
|
||||
));
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"quoted-printable".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello! This is lettre, and this\r\n",
|
||||
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||
" guess that's it!\r\n",
|
||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||
"From: Someone <somewhere@example.com>\r\n",
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_653() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjgggggggggfっ".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7?=\r\n",
|
||||
" =?utf-8?b?Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,62 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
use crate::{
|
||||
message::header::{Header, HeaderName, HeaderValue},
|
||||
BoxError,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct MimeVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
major: u8,
|
||||
minor: u8,
|
||||
}
|
||||
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
|
||||
/// MIME version 1.0
|
||||
///
|
||||
/// Should be used in all MIME messages.
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
|
||||
|
||||
impl MimeVersion {
|
||||
pub fn new(major: u8, minor: u8) -> Self {
|
||||
/// Build a new `MimeVersion` header
|
||||
pub const fn new(major: u8, minor: u8) -> Self {
|
||||
MimeVersion { major, minor }
|
||||
}
|
||||
|
||||
/// Get the `major` value of this `MimeVersion` header.
|
||||
#[inline]
|
||||
pub const fn major(self) -> u8 {
|
||||
self.major
|
||||
}
|
||||
|
||||
/// Get the `minor` value of this `MimeVersion` header.
|
||||
#[inline]
|
||||
pub const fn minor(self) -> u8 {
|
||||
self.minor
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for MimeVersion {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("MIME-Version")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mut s = s.split('.');
|
||||
|
||||
let major = s
|
||||
.next()
|
||||
.expect("The first call to next for a Split<char> always succeeds");
|
||||
let minor = s
|
||||
.next()
|
||||
.ok_or_else(|| String::from("MIME-Version header doesn't contain '.'"))?;
|
||||
let major = major.parse()?;
|
||||
let minor = minor.parse()?;
|
||||
Ok(MimeVersion::new(major, minor))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let val = format!("{}.{}", self.major, self.minor);
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MimeVersion {
|
||||
@@ -25,36 +65,12 @@ impl Default for MimeVersion {
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for MimeVersion {
|
||||
fn header_name() -> &'static str {
|
||||
"MIME-Version"
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one().ok_or(HeaderError::Header).and_then(|r| {
|
||||
let mut s = from_utf8(r).map_err(|_| HeaderError::Header)?.split('.');
|
||||
|
||||
let major = s.next().ok_or(HeaderError::Header)?;
|
||||
let minor = s.next().ok_or(HeaderError::Header)?;
|
||||
let major = major.parse().map_err(|_| HeaderError::Header)?;
|
||||
let minor = minor.parse().map_err(|_| HeaderError::Header)?;
|
||||
Ok(MimeVersion::new(major, minor))
|
||||
})
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&format!("{}.{}", self.major, self.minor))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{MimeVersion, MIME_VERSION_1_0};
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_mime_version() {
|
||||
@@ -62,23 +78,29 @@ mod test {
|
||||
|
||||
headers.set(MIME_VERSION_1_0);
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
|
||||
assert_eq!(headers.to_string(), "MIME-Version: 1.0\r\n");
|
||||
|
||||
headers.set(MimeVersion::new(0, 1));
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
|
||||
assert_eq!(headers.to_string(), "MIME-Version: 0.1\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("MIME-Version", "1.0");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("MIME-Version"),
|
||||
"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.set_raw("MIME-Version", "0.1");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("MIME-Version"),
|
||||
"0.1".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
use crate::message::utf8_b;
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
macro_rules! text_header {
|
||||
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
$(#[$attr])*
|
||||
pub struct $type_name(pub String);
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct $type_name(String);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_text)
|
||||
.map($type_name)
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(Self(s.into()))
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
fmt_text(&self.0, f)
|
||||
fn display(&self) -> HeaderValue {
|
||||
HeaderValue::new(Self::name(), self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for $type_name {
|
||||
#[inline]
|
||||
fn from(text: String) -> Self {
|
||||
Self(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for $type_name {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -62,38 +65,37 @@ text_header!(
|
||||
text_header!(
|
||||
/// `Message-Id` header. Contains a unique message identifier,
|
||||
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
Header(MessageId, "Message-Id")
|
||||
Header(MessageId, "Message-ID")
|
||||
);
|
||||
text_header!(
|
||||
/// `User-Agent` header. Contains information about the client,
|
||||
/// defined in [draft-melnikov-email-user-agent-00](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00#section-3)
|
||||
Header(UserAgent, "User-Agent")
|
||||
);
|
||||
|
||||
fn parse_text(raw: &[u8]) -> HyperResult<String> {
|
||||
if let Ok(src) = from_utf8(raw) {
|
||||
if let Some(txt) = utf8_b::decode(src) {
|
||||
return Ok(txt);
|
||||
}
|
||||
}
|
||||
Err(HeaderError::Header)
|
||||
text_header! {
|
||||
/// `Content-Id` header,
|
||||
/// defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
|
||||
Header(ContentId, "Content-ID")
|
||||
}
|
||||
|
||||
fn fmt_text(s: &str, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&utf8_b::encode(s))
|
||||
text_header! {
|
||||
/// `Content-Location` header,
|
||||
/// defined in [RFC2110](https://tools.ietf.org/html/rfc2110#section-4.3)
|
||||
Header(ContentLocation, "Content-Location")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::Subject;
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Sample subject".into()));
|
||||
|
||||
assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n");
|
||||
assert_eq!(headers.to_string(), "Subject: Sample subject\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -102,33 +104,33 @@ mod test {
|
||||
headers.set(Subject("Тема сообщения".into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
headers.to_string(),
|
||||
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn parse_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("Subject", "Sample subject");
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Sample subject".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Sample subject".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_utf8() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw(
|
||||
"Subject",
|
||||
"=?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Тема сообщения".into()))
|
||||
Some(Subject("Sample subject".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod parsers;
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
mod types;
|
||||
|
||||
5
src/message/mailbox/parsers/mod.rs
Normal file
5
src/message/mailbox/parsers/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod rfc2234;
|
||||
mod rfc2822;
|
||||
mod rfc5336;
|
||||
|
||||
pub(crate) use rfc2822::{mailbox, mailbox_list};
|
||||
32
src/message/mailbox/parsers/rfc2234.rs
Normal file
32
src/message/mailbox/parsers/rfc2234.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Partial parsers implementation of [RFC2234]: Augmented BNF for
|
||||
//! Syntax Specifications: ABNF.
|
||||
//!
|
||||
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
|
||||
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
|
||||
// 6.1 Core Rules
|
||||
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1
|
||||
|
||||
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
||||
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.is_ascii_alphabetic())
|
||||
}
|
||||
|
||||
// DIGIT = %x30-39
|
||||
// ; 0-9
|
||||
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.is_ascii_digit())
|
||||
}
|
||||
|
||||
// DQUOTE = %x22
|
||||
// ; " (Double Quote)
|
||||
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
just('"')
|
||||
}
|
||||
|
||||
// WSP = SP / HTAB
|
||||
// ; white space
|
||||
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((just(' '), just('\t')))
|
||||
}
|
||||
248
src/message/mailbox/parsers/rfc2822.rs
Normal file
248
src/message/mailbox/parsers/rfc2822.rs
Normal 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())
|
||||
}
|
||||
17
src/message/mailbox/parsers/rfc5336.rs
Normal file
17
src/message/mailbox/parsers/rfc5336.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
//! Partial parsers implementation of [RFC5336]: SMTP Extension for
|
||||
//! Internationalized Email Addresses.
|
||||
//!
|
||||
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
|
||||
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
|
||||
// 3.3. Extended Mailbox Address Syntax
|
||||
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
|
||||
|
||||
// UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
|
||||
// UTF8-2 = <See Section 4 of RFC 3629>
|
||||
// UTF8-3 = <See Section 4 of RFC 3629>
|
||||
// UTF8-4 = <See Section 4 of RFC 3629>
|
||||
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.len_utf8() > 1)
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
use crate::message::{Mailbox, Mailboxes};
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
use crate::message::{Mailbox, Mailboxes};
|
||||
|
||||
impl Serialize for Mailbox {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
serializer.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ impl Serialize for Mailboxes {
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
serializer.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,9 +154,11 @@ impl<'de> Deserialize<'de> for Mailboxes {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::from_str;
|
||||
|
||||
use super::*;
|
||||
use crate::address::Address;
|
||||
use serde_json::from_str;
|
||||
|
||||
#[test]
|
||||
fn parse_address_string() {
|
||||
@@ -175,7 +179,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_object_address_stirng() {
|
||||
fn parse_mailbox_object_address_string() {
|
||||
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
@@ -194,7 +198,7 @@ mod test {
|
||||
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
@@ -207,7 +211,7 @@ mod test {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use crate::{
|
||||
address::{Address, AddressError},
|
||||
message::utf8_b,
|
||||
};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||
mem,
|
||||
slice::Iter,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use chumsky::prelude::*;
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
use super::parsers;
|
||||
use crate::address::{Address, AddressError};
|
||||
|
||||
/// Represents an email address with an optional name for the sender/recipient.
|
||||
///
|
||||
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
||||
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -67,21 +69,29 @@ impl Mailbox {
|
||||
Mailbox { name, email }
|
||||
}
|
||||
|
||||
/// Encode addressee name using function
|
||||
pub(crate) fn recode_name<F>(&self, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(&str) -> String,
|
||||
{
|
||||
Mailbox::new(self.name.clone().map(|s| f(&s)), self.email.clone())
|
||||
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
||||
if let Some(name) = &self.name {
|
||||
email_encoding::headers::quoted_string::encode(name, w)?;
|
||||
w.space();
|
||||
w.write_char('<')?;
|
||||
}
|
||||
|
||||
w.write_str(self.email.as_ref())?;
|
||||
|
||||
if self.name.is_some() {
|
||||
w.write_char('>')?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mailbox {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
if let Some(ref name) = self.name {
|
||||
if let Some(name) = &self.name {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
f.write_str(&name)?;
|
||||
write_word(f, name)?;
|
||||
f.write_str(" <")?;
|
||||
self.email.fmt(f)?;
|
||||
return f.write_char('>');
|
||||
@@ -100,40 +110,24 @@ impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
|
||||
let (name, address) = header;
|
||||
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
|
||||
}
|
||||
}*/
|
||||
|
||||
impl FromStr for Mailbox {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||
match (src.find('<'), src.find('>')) {
|
||||
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => {
|
||||
let name = src.split_at(addr_open).0;
|
||||
let addr_open = addr_open + 1;
|
||||
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
|
||||
let addr = addr.parse()?;
|
||||
let name = name.trim();
|
||||
let name = if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.into())
|
||||
};
|
||||
Ok(Mailbox::new(name, addr))
|
||||
}
|
||||
(Some(_), _) => Err(AddressError::Unbalanced),
|
||||
_ => {
|
||||
let addr = src.parse()?;
|
||||
Ok(Mailbox::new(None, addr))
|
||||
}
|
||||
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
|
||||
let mailbox = Mailbox::new(name, Address::new(user, domain)?);
|
||||
|
||||
Ok(mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Address> for Mailbox {
|
||||
fn from(value: Address) -> Self {
|
||||
Self::new(None, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +135,7 @@ impl FromStr for Mailbox {
|
||||
///
|
||||
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
||||
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Mailboxes(Vec<Mailbox>);
|
||||
|
||||
@@ -261,6 +255,20 @@ impl Mailboxes {
|
||||
pub fn iter(&self) -> Iter<'_, Mailbox> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
||||
let mut first = true;
|
||||
for mailbox in self.iter() {
|
||||
if !mem::take(&mut first) {
|
||||
w.write_char(',')?;
|
||||
w.space();
|
||||
}
|
||||
|
||||
mailbox.encode(w)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mailboxes {
|
||||
@@ -293,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 {
|
||||
type Item = Mailbox;
|
||||
type IntoIter = ::std::vec::IntoIter<Mailbox>;
|
||||
@@ -302,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 {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut iter = self.iter();
|
||||
@@ -331,29 +343,110 @@ impl FromStr for Mailboxes {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||
src.split(',')
|
||||
.map(|m| {
|
||||
m.trim().parse().and_then(|Mailbox { name, email }| {
|
||||
if let Some(name) = name {
|
||||
if let Some(name) = utf8_b::decode(&name) {
|
||||
Ok(Mailbox::new(Some(name), email))
|
||||
} else {
|
||||
Err(AddressError::InvalidUtf8b)
|
||||
let mut mailboxes = Vec::new();
|
||||
|
||||
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
|
||||
for (name, (user, domain)) in parsed_mailboxes {
|
||||
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?))
|
||||
}
|
||||
} else {
|
||||
Ok(Mailbox::new(None, email))
|
||||
|
||||
Ok(Mailboxes(mailboxes))
|
||||
}
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
|
||||
fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
|
||||
if s.as_bytes().iter().copied().all(is_valid_atom_char) {
|
||||
f.write_str(s)
|
||||
} else {
|
||||
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
||||
f.write_char('"')?;
|
||||
for c in s.chars() {
|
||||
write_quoted_string_char(f, c)?;
|
||||
}
|
||||
f.write_char('"')?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
|
||||
fn is_valid_atom_char(c: u8) -> bool {
|
||||
matches!(c,
|
||||
// Not really allowed but can be inserted between atoms.
|
||||
b'\t' |
|
||||
b' ' |
|
||||
|
||||
b'!' |
|
||||
b'#' |
|
||||
b'$' |
|
||||
b'%' |
|
||||
b'&' |
|
||||
b'\'' |
|
||||
b'*' |
|
||||
b'+' |
|
||||
b'-' |
|
||||
b'/' |
|
||||
b'0'..=b'8' |
|
||||
b'=' |
|
||||
b'?' |
|
||||
b'A'..=b'Z' |
|
||||
b'^' |
|
||||
b'_' |
|
||||
b'`' |
|
||||
b'a'..=b'z' |
|
||||
b'{' |
|
||||
b'|' |
|
||||
b'}' |
|
||||
b'~' |
|
||||
|
||||
// Not technically allowed but will be escaped into allowed characters.
|
||||
128..=255)
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
||||
fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
|
||||
match c {
|
||||
// Can not be encoded.
|
||||
'\n' | '\r' => Err(std::fmt::Error),
|
||||
|
||||
// Note, not qcontent but can be put before or after any qcontent.
|
||||
'\t' | ' ' => f.write_char(c),
|
||||
|
||||
c if match c as u32 {
|
||||
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
|
||||
1..=8 | 11 | 12 | 14..=31 | 127 |
|
||||
|
||||
// The rest of the US-ASCII except \ and "
|
||||
33 |
|
||||
35..=91 |
|
||||
93..=126 |
|
||||
|
||||
// Non-ascii characters will be escaped separately later.
|
||||
128.. => true,
|
||||
_ => false,
|
||||
} =>
|
||||
{
|
||||
f.write_char(c)
|
||||
}
|
||||
|
||||
_ => {
|
||||
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
|
||||
f.write_char('\\')?;
|
||||
f.write_char(c)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(Mailboxes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::Mailbox;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_only() {
|
||||
@@ -373,7 +466,63 @@ mod test {
|
||||
"{}",
|
||||
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"K. <kayo@example.com>"
|
||||
"\"K.\" <kayo@example.com>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_comma() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(
|
||||
Some("Last, First".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
)
|
||||
),
|
||||
r#""Last, First" <kayo@example.com>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn mailbox_format_address_with_color() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(
|
||||
Some("Chris's Wiki :: blog".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
)
|
||||
),
|
||||
r#""Chris's Wiki :: blog" <kayo@example.com>"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -395,7 +544,7 @@ mod test {
|
||||
"{}",
|
||||
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"K. <kayo@example.com>"
|
||||
"\"K.\" <kayo@example.com>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -437,7 +586,7 @@ mod test {
|
||||
#[test]
|
||||
fn parse_address_from_tuple() {
|
||||
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(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use std::{io::Write, iter::repeat_with};
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use crate::message::{
|
||||
header::{ContentTransferEncoding, ContentType, Header, Headers},
|
||||
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
|
||||
EmailFormat, IntoBody,
|
||||
};
|
||||
use mime::Mime;
|
||||
use rand::Rng;
|
||||
|
||||
/// MIME part variants
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Part {
|
||||
pub(super) enum Part {
|
||||
/// Single part with content
|
||||
Single(SinglePart),
|
||||
|
||||
@@ -15,6 +17,16 @@ pub enum Part {
|
||||
Multi(MultiPart),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
#[cfg(feature = "dkim")]
|
||||
pub(super) fn format_body(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
Part::Single(part) => part.format_body(out),
|
||||
Part::Multi(part) => part.format_body(out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for Part {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
@@ -24,18 +36,6 @@ impl EmailFormat for Part {
|
||||
}
|
||||
}
|
||||
|
||||
impl Part {
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Parts of multipart body
|
||||
pub type Parts = Vec<Part>;
|
||||
|
||||
/// Creates builder for single part
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinglePartBuilder {
|
||||
@@ -64,7 +64,7 @@ impl SinglePartBuilder {
|
||||
|
||||
/// Build singlepart using body
|
||||
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
|
||||
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
|
||||
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
|
||||
let body = body.into_body(maybe_encoding);
|
||||
|
||||
self.headers.set(body.encoding());
|
||||
@@ -92,7 +92,7 @@ impl Default for SinglePartBuilder {
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let part = SinglePart::builder()
|
||||
/// .header(header::ContentType("text/plain; charset=utf8".parse()?))
|
||||
/// .header(header::ContentType::TEXT_PLAIN)
|
||||
/// .body(String::from("Текст письма в уникоде"));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -110,6 +110,20 @@ impl SinglePart {
|
||||
SinglePartBuilder::new()
|
||||
}
|
||||
|
||||
/// Directly create a `SinglePart` from a plain UTF-8 content
|
||||
pub fn plain<T: IntoBody>(body: T) -> Self {
|
||||
Self::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(body)
|
||||
}
|
||||
|
||||
/// Directly create a `SinglePart` from a UTF-8 HTML content
|
||||
pub fn html<T: IntoBody>(body: T) -> Self {
|
||||
Self::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(body)
|
||||
}
|
||||
|
||||
/// Get the headers from singlepart
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &Headers {
|
||||
@@ -128,14 +142,20 @@ impl SinglePart {
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Format only the signlepart body
|
||||
fn format_body(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.body);
|
||||
out.extend_from_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for SinglePart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend_from_slice(&self.body);
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
self.format_body(out);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,17 +164,17 @@ impl EmailFormat for SinglePart {
|
||||
pub enum MultiPartKind {
|
||||
/// Mixed kind to combine unrelated content parts
|
||||
///
|
||||
/// For example this kind can be used to mix email message and attachments.
|
||||
/// For example, this kind can be used to mix an email message and attachments.
|
||||
Mixed,
|
||||
|
||||
/// Alternative kind to join several variants of same email contents.
|
||||
///
|
||||
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message.
|
||||
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into a single email message.
|
||||
Alternative,
|
||||
|
||||
/// Related kind to mix content and related resources.
|
||||
///
|
||||
/// For example, you can include images into HTML content using that.
|
||||
/// For example, you can include images in HTML content using that.
|
||||
Related,
|
||||
|
||||
/// Encrypted kind for encrypted messages
|
||||
@@ -165,33 +185,29 @@ pub enum MultiPartKind {
|
||||
}
|
||||
|
||||
/// Create a random MIME boundary.
|
||||
/// (Not cryptographically random)
|
||||
fn make_boundary() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(rand::distributions::Alphanumeric)
|
||||
.take(40)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
repeat_with(fastrand::alphanumeric).take(40).collect()
|
||||
}
|
||||
|
||||
impl MultiPartKind {
|
||||
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
|
||||
let boundary = boundary.map_or_else(make_boundary, |s| s.into());
|
||||
pub(crate) fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
|
||||
let boundary = boundary.map_or_else(make_boundary, Into::into);
|
||||
|
||||
use self::MultiPartKind::*;
|
||||
format!(
|
||||
"multipart/{}; boundary=\"{}\"{}",
|
||||
match self {
|
||||
Mixed => "mixed",
|
||||
Alternative => "alternative",
|
||||
Related => "related",
|
||||
Encrypted { .. } => "encrypted",
|
||||
Signed { .. } => "signed",
|
||||
Self::Mixed => "mixed",
|
||||
Self::Alternative => "alternative",
|
||||
Self::Related => "related",
|
||||
Self::Encrypted { .. } => "encrypted",
|
||||
Self::Signed { .. } => "signed",
|
||||
},
|
||||
boundary,
|
||||
match self {
|
||||
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
|
||||
Signed { protocol, micalg } =>
|
||||
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
|
||||
Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
|
||||
Self::Signed { protocol, micalg } =>
|
||||
format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
|
||||
_ => String::new(),
|
||||
}
|
||||
)
|
||||
@@ -200,18 +216,17 @@ impl MultiPartKind {
|
||||
}
|
||||
|
||||
fn from_mime(m: &Mime) -> Option<Self> {
|
||||
use self::MultiPartKind::*;
|
||||
match m.subtype().as_ref() {
|
||||
"mixed" => Some(Mixed),
|
||||
"alternative" => Some(Alternative),
|
||||
"related" => Some(Related),
|
||||
"mixed" => Some(Self::Mixed),
|
||||
"alternative" => Some(Self::Alternative),
|
||||
"related" => Some(Self::Related),
|
||||
"signed" => m.get_param("protocol").and_then(|p| {
|
||||
m.get_param("micalg").map(|micalg| Signed {
|
||||
m.get_param("micalg").map(|micalg| Self::Signed {
|
||||
protocol: p.as_str().to_owned(),
|
||||
micalg: micalg.as_str().to_owned(),
|
||||
})
|
||||
}),
|
||||
"encrypted" => m.get_param("protocol").map(|p| Encrypted {
|
||||
"encrypted" => m.get_param("protocol").map(|p| Self::Encrypted {
|
||||
protocol: p.as_str().to_owned(),
|
||||
}),
|
||||
_ => None,
|
||||
@@ -219,12 +234,6 @@ impl MultiPartKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MultiPartKind> for Mime {
|
||||
fn from(m: MultiPartKind) -> Self {
|
||||
m.to_mime::<String>(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Multipart builder
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPartBuilder {
|
||||
@@ -247,17 +256,17 @@ impl MultiPartBuilder {
|
||||
|
||||
/// Set `Content-Type` header using [`MultiPartKind`]
|
||||
pub fn kind(self, kind: MultiPartKind) -> Self {
|
||||
self.header(ContentType(kind.into()))
|
||||
self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
|
||||
}
|
||||
|
||||
/// Set custom boundary
|
||||
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
|
||||
pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
|
||||
let kind = {
|
||||
let mime = &self.headers.get::<ContentType>().unwrap().0;
|
||||
MultiPartKind::from_mime(mime).unwrap()
|
||||
let content_type = self.headers.get::<ContentType>().unwrap();
|
||||
MultiPartKind::from_mime(content_type.as_ref()).unwrap()
|
||||
};
|
||||
let mime = kind.to_mime(Some(boundary.as_ref()));
|
||||
self.header(ContentType(mime))
|
||||
let mime = kind.to_mime(Some(boundary));
|
||||
self.header(ContentType::from_mime(mime))
|
||||
}
|
||||
|
||||
/// Creates multipart without parts
|
||||
@@ -268,11 +277,6 @@ impl MultiPartBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates multipart using part
|
||||
pub fn part(self, part: Part) -> MultiPart {
|
||||
self.build().part(part)
|
||||
}
|
||||
|
||||
/// Creates multipart using singlepart
|
||||
pub fn singlepart(self, part: SinglePart) -> MultiPart {
|
||||
self.build().singlepart(part)
|
||||
@@ -294,7 +298,7 @@ impl Default for MultiPartBuilder {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPart {
|
||||
headers: Headers,
|
||||
parts: Parts,
|
||||
parts: Vec<Part>,
|
||||
}
|
||||
|
||||
impl MultiPart {
|
||||
@@ -338,10 +342,11 @@ impl MultiPart {
|
||||
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
|
||||
}
|
||||
|
||||
/// Add part to multipart
|
||||
pub fn part(mut self, part: Part) -> Self {
|
||||
self.parts.push(part);
|
||||
self
|
||||
/// Alias for HTML and plain text versions of an email
|
||||
pub fn alternative_plain_html<T: IntoBody, V: IntoBody>(plain: T, html: V) -> Self {
|
||||
Self::alternative()
|
||||
.singlepart(SinglePart::plain(plain))
|
||||
.singlepart(SinglePart::html(html))
|
||||
}
|
||||
|
||||
/// Add single part to multipart
|
||||
@@ -358,8 +363,13 @@ impl MultiPart {
|
||||
|
||||
/// Get the boundary of multipart contents
|
||||
pub fn boundary(&self) -> String {
|
||||
let content_type = &self.headers.get::<ContentType>().unwrap().0;
|
||||
content_type.get_param("boundary").unwrap().as_str().into()
|
||||
let content_type = self.headers.get::<ContentType>().unwrap();
|
||||
content_type
|
||||
.as_ref()
|
||||
.get_param("boundary")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Get the headers from the multipart
|
||||
@@ -372,29 +382,15 @@ impl MultiPart {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Get the parts from the multipart
|
||||
pub fn parts(&self) -> &Parts {
|
||||
&self.parts
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the parts
|
||||
pub fn parts_mut(&mut self) -> &mut Parts {
|
||||
&mut self.parts
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for MultiPart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
out.extend_from_slice(b"\r\n");
|
||||
|
||||
/// Format only the multipart body
|
||||
fn format_body(&self, out: &mut Vec<u8>) {
|
||||
let boundary = self.boundary();
|
||||
|
||||
for part in &self.parts {
|
||||
@@ -410,24 +406,32 @@ impl EmailFormat for MultiPart {
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for MultiPart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
self.format_body(out);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::message::header;
|
||||
|
||||
#[test]
|
||||
fn single_part_binary() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n"
|
||||
@@ -438,16 +442,14 @@ mod test {
|
||||
#[test]
|
||||
fn single_part_quoted_printable() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::QuotedPrintable)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||
"\r\n",
|
||||
"=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",
|
||||
@@ -459,16 +461,14 @@ mod test {
|
||||
#[test]
|
||||
fn single_part_base64() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
|
||||
@@ -479,75 +479,60 @@ mod test {
|
||||
#[test]
|
||||
fn multi_part_mixed() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")),
|
||||
))
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"example.c".into(),
|
||||
)],
|
||||
})
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentDisposition::attachment("example.c"))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")),
|
||||
);
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/mixed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/mixed;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_encrypted() {
|
||||
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"application/pgp-encrypted".parse().unwrap(),
|
||||
))
|
||||
.body(String::from("Version: 1")),
|
||||
))
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType(
|
||||
"application/octet-stream; name=\"encrypted.asc\""
|
||||
.parse()
|
||||
.header(header::ContentType::parse("application/pgp-encrypted").unwrap())
|
||||
.body(String::from("Version: 1")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(
|
||||
ContentType::parse("application/octet-stream; name=\"encrypted.asc\"")
|
||||
.unwrap(),
|
||||
)
|
||||
.header(header::ContentDisposition::inline_with_name(
|
||||
"encrypted.asc",
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Inline,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"encrypted.asc".into(),
|
||||
)],
|
||||
})
|
||||
.body(String::from(concat!(
|
||||
"-----BEGIN PGP MESSAGE-----\r\n",
|
||||
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
|
||||
@@ -556,17 +541,19 @@ mod test {
|
||||
))),
|
||||
);
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/encrypted;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/encrypted;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
|
||||
" protocol=\"application/pgp-encrypted\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/pgp-encrypted\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Version: 1\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
|
||||
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
@@ -576,7 +563,9 @@ mod test {
|
||||
"...\r\n",
|
||||
"-----END PGP MESSAGE-----\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_signed() {
|
||||
@@ -584,27 +573,19 @@ mod test {
|
||||
"application/pgp-signature".to_owned(),
|
||||
"pgp-sha256".to_owned(),
|
||||
)
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType("text/plain".parse().unwrap()))
|
||||
.body(String::from("Test email for signature")),
|
||||
))
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType(
|
||||
"application/pgp-signature; name=\"signature.asc\""
|
||||
.parse()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Test email for signature")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(
|
||||
ContentType::parse("application/pgp-signature; name=\"signature.asc\"")
|
||||
.unwrap(),
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"signature.asc".into(),
|
||||
)],
|
||||
})
|
||||
)
|
||||
.header(header::ContentDisposition::attachment("signature.asc"))
|
||||
.body(String::from(concat!(
|
||||
"-----BEGIN PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
@@ -616,18 +597,20 @@ mod test {
|
||||
))),
|
||||
);
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/signed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/signed;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
|
||||
" protocol=\"application/pgp-signature\";",
|
||||
" micalg=\"pgp-sha256\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Test email for signature\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
|
||||
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
@@ -640,77 +623,76 @@ mod test {
|
||||
"=3FYZ\r\n",
|
||||
"-----END PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_alternative() {
|
||||
let part = MultiPart::alternative()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(SinglePart::builder()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"))))
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/alternative;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
concat!("Content-Type: multipart/alternative;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/html; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/html; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed_related() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.multipart(MultiPart::related()
|
||||
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("image/png".parse().unwrap()))
|
||||
.header(header::ContentLocation("/image.png".into()))
|
||||
.header(header::ContentType::parse("image/png").unwrap())
|
||||
.header(header::ContentLocation::from(String::from("/image.png")))
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".into())]
|
||||
})
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentDisposition::attachment("example.c"))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")));
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/mixed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
concat!("Content-Type: multipart/mixed;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: multipart/related;",
|
||||
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: multipart/related;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
|
||||
"Content-Type: text/html; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/html; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: image/png\r\n",
|
||||
"Content-Location: /image.png\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
@@ -718,14 +700,14 @@ mod test {
|
||||
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
|
||||
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
|
||||
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,15 +4,8 @@
|
||||
//!
|
||||
//! This section demonstrates how to build messages.
|
||||
//!
|
||||
//! <!--
|
||||
//! style for <details><summary>Blablabla</summary> Lots of stuff</details>
|
||||
//! borrowed from https://docs.rs/time/0.2.23/src/time/lib.rs.html#49-54
|
||||
//! -->
|
||||
//! <style>
|
||||
//! summary, details:not([open]) { cursor: pointer; }
|
||||
//! summary { display: list-item; }
|
||||
//! summary::marker { content: '▶ '; }
|
||||
//! details[open] summary::marker { content: '▼ '; }
|
||||
//! </style>
|
||||
//!
|
||||
//!
|
||||
@@ -21,7 +14,7 @@
|
||||
//! The easiest way of creating a message, which uses a plain text body.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::Message;
|
||||
//! use lettre::message::{header::ContentType, Message};
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -30,6 +23,7 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
@@ -45,6 +39,7 @@
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
||||
//! Content-Type: text/plain; charset=utf-8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! Be happy!
|
||||
@@ -63,7 +58,7 @@
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! use lettre::message::{header, Message, MultiPart, Part, SinglePart};
|
||||
//! use lettre::message::{header, Message, MultiPart, SinglePart};
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let m = Message::builder()
|
||||
@@ -71,21 +66,10 @@
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .multipart(
|
||||
//! MultiPart::alternative()
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
|
||||
//! .body(String::from("Hello, world! :)")),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
|
||||
//! .body(String::from(
|
||||
//! "<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>",
|
||||
//! )),
|
||||
//! ),
|
||||
//! )?;
|
||||
//! .multipart(MultiPart::alternative_plain_html(
|
||||
//! String::from("Hello, world! :)"),
|
||||
//! String::from("<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>"),
|
||||
//! ))?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -124,9 +108,10 @@
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! use lettre::message::{header, Body, Message, MultiPart, Part, SinglePart};
|
||||
//! use std::fs;
|
||||
//!
|
||||
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let image = fs::read("docs/lettre.png")?;
|
||||
//! // this image_body can be cloned and reused between emails.
|
||||
@@ -144,47 +129,22 @@
|
||||
//! MultiPart::mixed()
|
||||
//! .multipart(
|
||||
//! MultiPart::alternative()
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
|
||||
//! .body(String::from("Hello, world! :)")),
|
||||
//! )
|
||||
//! .singlepart(SinglePart::plain(String::from("Hello, world! :)")))
|
||||
//! .multipart(
|
||||
//! MultiPart::related()
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType(
|
||||
//! "text/html; charset=utf8".parse()?,
|
||||
//! ))
|
||||
//! .body(String::from(
|
||||
//! .singlepart(SinglePart::html(String::from(
|
||||
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
//! )))
|
||||
//! .singlepart(
|
||||
//! Attachment::new_inline(String::from("123"))
|
||||
//! .body(image_body, "image/png".parse().unwrap()),
|
||||
//! ),
|
||||
//! ),
|
||||
//! )
|
||||
//! .singlepart(Attachment::new(String::from("example.rs")).body(
|
||||
//! String::from("fn main() { println!(\"Hello, World!\") }"),
|
||||
//! "text/plain".parse().unwrap(),
|
||||
//! )),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("image/png".parse()?))
|
||||
//! .header(header::ContentDisposition {
|
||||
//! disposition: header::DispositionType::Inline,
|
||||
//! parameters: vec![],
|
||||
//! })
|
||||
//! .header(header::ContentId("<123>".into()))
|
||||
//! .body(image_body),
|
||||
//! ),
|
||||
//! ),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
|
||||
//! .header(header::ContentDisposition {
|
||||
//! disposition: header::DispositionType::Attachment,
|
||||
//! parameters: vec![header::DispositionParam::Filename(
|
||||
//! header::Charset::Ext("utf-8".into()),
|
||||
//! None,
|
||||
//! "example.rs".as_bytes().into(),
|
||||
//! )],
|
||||
//! })
|
||||
//! .body(String::from("fn main() { println!(\"Hello, World!\") }")),
|
||||
//! ),
|
||||
//! )?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
@@ -238,25 +198,26 @@
|
||||
//! ```
|
||||
//! </details>
|
||||
|
||||
use std::{io::Write, iter, time::SystemTime};
|
||||
|
||||
pub use attachment::Attachment;
|
||||
pub use body::{Body, IntoBody, MaybeString};
|
||||
#[cfg(feature = "dkim")]
|
||||
pub use dkim::*;
|
||||
pub use mailbox::*;
|
||||
pub use mimebody::*;
|
||||
|
||||
pub use mime;
|
||||
|
||||
mod attachment;
|
||||
mod body;
|
||||
#[cfg(feature = "dkim")]
|
||||
pub mod dkim;
|
||||
pub mod header;
|
||||
mod mailbox;
|
||||
mod mimebody;
|
||||
mod utf8_b;
|
||||
|
||||
use std::{convert::TryFrom, time::SystemTime};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
message::header::{ContentTransferEncoding, EmailDate, Header, Headers, MailboxesHeader},
|
||||
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
|
||||
Error as EmailError,
|
||||
};
|
||||
|
||||
@@ -273,6 +234,7 @@ trait EmailFormat {
|
||||
pub struct MessageBuilder {
|
||||
headers: Headers,
|
||||
envelope: Option<Envelope>,
|
||||
drop_bcc: bool,
|
||||
}
|
||||
|
||||
impl MessageBuilder {
|
||||
@@ -281,71 +243,41 @@ impl MessageBuilder {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
envelope: None,
|
||||
drop_bcc: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>(mut self, header: H) -> Self {
|
||||
if self.headers.has::<H>() {
|
||||
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
|
||||
self
|
||||
} else {
|
||||
self.header(header)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add `Date` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Date(date))`.
|
||||
pub fn date(self, date: EmailDate) -> Self {
|
||||
self.header(header::Date(date))
|
||||
}
|
||||
|
||||
/// Set `Date` header using current date/time
|
||||
///
|
||||
/// Shortcut for `self.date(SystemTime::now())`.
|
||||
pub fn date_now(self) -> Self {
|
||||
self.date(SystemTime::now().into())
|
||||
}
|
||||
|
||||
/// Set `Subject` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Subject(subject.into()))`.
|
||||
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
|
||||
self.header(header::Subject(subject.into()))
|
||||
}
|
||||
|
||||
/// 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(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(mbox.into()))
|
||||
self.mailbox(header::From::from(Mailboxes::from(mbox)))
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// Add `Date` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Date::new(st))`.
|
||||
pub fn date(self, st: SystemTime) -> Self {
|
||||
self.header(header::Date::new(st))
|
||||
}
|
||||
|
||||
/// Set `Date` header using current date/time
|
||||
///
|
||||
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
|
||||
/// if no date has been provided.
|
||||
pub fn date_now(self) -> Self {
|
||||
self.date(SystemTime::now())
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `ReplyTo` header
|
||||
@@ -381,16 +313,24 @@ impl MessageBuilder {
|
||||
/// Set or add message id to [`In-Reply-To`
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
pub fn in_reply_to(self, id: String) -> Self {
|
||||
self.header(header::InReplyTo(id))
|
||||
self.header(header::InReplyTo::from(id))
|
||||
}
|
||||
|
||||
/// Set or add message id to [`References`
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
pub fn references(self, id: String) -> Self {
|
||||
self.header(header::References(id))
|
||||
self.header(header::References::from(id))
|
||||
}
|
||||
|
||||
/// Set [Message-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
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
///
|
||||
/// Should generally be inserted by the mail relay.
|
||||
@@ -399,28 +339,45 @@ impl MessageBuilder {
|
||||
/// `<UUID@HOSTNAME>`.
|
||||
pub fn message_id(self, id: Option<String>) -> Self {
|
||||
match id {
|
||||
Some(i) => self.header(header::MessageId(i)),
|
||||
Some(i) => self.header(header::MessageId::from(i)),
|
||||
None => {
|
||||
#[cfg(feature = "hostname")]
|
||||
let hostname = hostname::get()
|
||||
.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"))]
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
|
||||
|
||||
self.header(header::MessageId(
|
||||
self.header(header::MessageId::from(
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
||||
format!("<{}@{}>", Uuid::new_v4(), hostname),
|
||||
format!("<{}@{}>", make_message_id(), hostname),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.header(header::UserAgent(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)
|
||||
@@ -429,6 +386,20 @@ impl MessageBuilder {
|
||||
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
|
||||
|
||||
/// Create message from body
|
||||
@@ -437,7 +408,7 @@ impl MessageBuilder {
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6
|
||||
|
||||
// Insert Date if missing
|
||||
let res = if self.headers.get::<header::Date>().is_none() {
|
||||
let mut res = if self.headers.get::<header::Date>().is_none() {
|
||||
self.date_now()
|
||||
} else {
|
||||
self
|
||||
@@ -446,7 +417,7 @@ impl MessageBuilder {
|
||||
// Fail is missing correct originator (Sender or From)
|
||||
match res.headers.get::<header::From>() {
|
||||
Some(header::From(f)) => {
|
||||
let from: Vec<Mailbox> = f.clone().into();
|
||||
let from: Vec<Mailbox> = f.into();
|
||||
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
|
||||
return Err(EmailError::TooManyFrom);
|
||||
}
|
||||
@@ -460,6 +431,12 @@ impl MessageBuilder {
|
||||
Some(e) => e,
|
||||
None => Envelope::try_from(&res.headers)?,
|
||||
};
|
||||
|
||||
if res.drop_bcc {
|
||||
// Remove `Bcc` headers now the envelope is set
|
||||
res.headers.remove::<header::Bcc>();
|
||||
}
|
||||
|
||||
Ok(Message {
|
||||
headers: res.headers,
|
||||
body,
|
||||
@@ -473,7 +450,7 @@ impl MessageBuilder {
|
||||
/// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
|
||||
/// for `body`.
|
||||
pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
|
||||
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
|
||||
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
|
||||
let body = body.into_body(maybe_encoding);
|
||||
|
||||
self.headers.set(body.encoding());
|
||||
@@ -489,9 +466,19 @@ impl MessageBuilder {
|
||||
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
|
||||
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
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Message {
|
||||
headers: Headers,
|
||||
@@ -516,6 +503,11 @@ impl Message {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers
|
||||
pub fn headers_mut(&mut self) -> &mut Headers {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Get `Message` envelope
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
@@ -527,17 +519,89 @@ impl Message {
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(feature = "dkim")]
|
||||
/// Format body for signing
|
||||
pub(crate) fn body_raw(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
match &self.body {
|
||||
MessageBody::Mime(p) => p.format_body(&mut out),
|
||||
MessageBody::Raw(r) => out.extend_from_slice(r),
|
||||
};
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out
|
||||
}
|
||||
|
||||
/// Sign the message using Dkim
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// use lettre::{
|
||||
/// message::dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
|
||||
/// Message,
|
||||
/// };
|
||||
///
|
||||
/// let mut message = Message::builder()
|
||||
/// .from("Alice <alice@example.org>".parse().unwrap())
|
||||
/// .reply_to("Bob <bob@example.org>".parse().unwrap())
|
||||
/// .to("Carla <carla@example.net>".parse().unwrap())
|
||||
/// .subject("Hello")
|
||||
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
|
||||
/// .unwrap();
|
||||
/// let key = "-----BEGIN RSA PRIVATE KEY-----
|
||||
/// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
|
||||
/// NLTdc9xfPIOK8l/xGrN7Nd63J4cTATqZukumczkA46O8YKHwa53pNT6NYwCNtDUL
|
||||
/// eBu+7xUW18GmDzkIFkxGO2R5kkTeWPlKvKpEiicIMfl0OmyW/fI3AbtM7e/gmqQ4
|
||||
/// kEYIO0mTjPT+jTgWE4JIi5KUTHudUBtfMKcSFyM2HkUOExl1c9+A4epjRFQwEXMA
|
||||
/// hM5GrqZoOdUm4fIpvGpLIGIxFgHPpZYbyq6yJZzH3+5aKyCHrsHawPuPiCD45zsU
|
||||
/// re31zCE6b6k1sDiiBR4CaRHnbL7hxFp0aNLOVQIDAQABAoIBAGMK3gBrKxaIcUGo
|
||||
/// gQeIf7XrJ6vK72YC9L8uleqI4a9Hy++E7f4MedZ6eBeWta8jrnEL4Yp6xg+beuDc
|
||||
/// A24+Mhng+6Dyp+TLLqj+8pQlPnbrMprRVms7GIXFrrs+wO1RkBNyhy7FmH0roaMM
|
||||
/// pJZzoGW2pE9QdbqjL3rdlWTi/60xRX9eZ42nNxYnbc+RK03SBd46c3UBha6Y9iQX
|
||||
/// 562yWilDnB5WCX2tBoSN39bEhJvuZDzMwOuGw68Q96Hdz82Iz1xVBnRhH+uNStjR
|
||||
/// VnAssSHVxPSpwWrm3sHlhjBHWPnNIaOKIKl1lbL+qWfVQCj/6a5DquC+vYAeYR6L
|
||||
/// 3mA0z0ECgYEA5YkNYcILSXyE0hZ8eA/t58h8eWvYI5iqt3nT4fznCoYJJ74Vukeg
|
||||
/// 6BTlq/CsanwT1lDtvDKrOaJbA7DPTES/bqT0HoeIdOvAw9w/AZI5DAqYp61i6RMK
|
||||
/// xfAQL/Ik5MDFN8gEMLLXRVMe/aR27f6JFZpShJOK/KCzHqikKfYVJ+UCgYEAzI2F
|
||||
/// ZlTyittWSyUSl5UKyfSnFOx2+6vNy+lu5DeMJu8Wh9rqBk388Bxq98CfkCseWESN
|
||||
/// pTCGdYltz9DvVNBdBLwSMdLuYJAI6U+Zd70MWyuNdHFPyWVHUNqMUBvbUtj2w74q
|
||||
/// Hzu0GI0OrRjdX6C63S17PggmT/N2R9X7P4STxbECgYA+AZAD4I98Ao8+0aQ+Ks9x
|
||||
/// 1c8KXf+9XfiAKAD9A3zGcv72JXtpHwBwsXR5xkJNYcdaFfKi7G0k3J8JmDHnwIqW
|
||||
/// MSlhNeu+6hDg2BaNLhsLDbG/Wi9mFybJ4df9m8Qrp4efUgEPxsAwkgvFKTCXijMu
|
||||
/// CspP1iutoxvAJH50d22voQKBgDIsSFtIXNGYaTs3Va8enK3at5zXP3wNsQXiNRP/
|
||||
/// V/44yNL77EktmewfXFF2yuym1uOZtRCerWxpEClYO0wXa6l8pA3aiiPfUIBByQfo
|
||||
/// s/4s2Z6FKKfikrKPWLlRi+NvWl+65kQQ9eTLvJzSq4IIP61+uWsGvrb/pbSLFPyI
|
||||
/// fWKRAoGBALFCStBXvdMptjq4APUzAdJ0vytZzXkOZHxgmc+R0fQn22OiW0huW6iX
|
||||
/// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw
|
||||
/// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25
|
||||
/// -----END RSA PRIVATE KEY-----";
|
||||
/// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
|
||||
/// message.sign(&DkimConfig::default_config(
|
||||
/// "dkimtest".to_owned(),
|
||||
/// "example.org".to_owned(),
|
||||
/// signing_key,
|
||||
/// ));
|
||||
/// println!(
|
||||
/// "message: {}",
|
||||
/// std::str::from_utf8(&message.formatted()).unwrap()
|
||||
/// );
|
||||
/// ```
|
||||
#[cfg(feature = "dkim")]
|
||||
pub fn sign(&mut self, dkim_config: &DkimConfig) {
|
||||
dkim_sign(self, dkim_config);
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for Message {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
|
||||
match &self.body {
|
||||
MessageBody::Mime(p) => p.format(out),
|
||||
MessageBody::Raw(r) => {
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend_from_slice(&r)
|
||||
out.extend_from_slice(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,9 +613,19 @@ impl Default for MessageBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a random message id.
|
||||
/// (Not cryptographically random)
|
||||
fn make_message_id() -> String {
|
||||
iter::repeat_with(fastrand::alphanumeric).take(36).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::message::{header, mailbox::Mailbox, Message, MultiPart, SinglePart};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
|
||||
|
||||
#[test]
|
||||
fn email_missing_originator() {
|
||||
@@ -561,7 +635,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_miminal_message() {
|
||||
fn email_minimal_message() {
|
||||
assert!(Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
@@ -579,11 +653,13 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_message() {
|
||||
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
|
||||
fn email_message_no_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())
|
||||
.header(header::From(
|
||||
vec![Mailbox::new(
|
||||
Some("Каи".into()),
|
||||
@@ -594,16 +670,54 @@ mod test {
|
||||
.header(header::To(
|
||||
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
|
||||
))
|
||||
.header(header::Subject("яңа ел белән!".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 GMT\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
||||
"To: Pony O.P. <pony@domain.tld>\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]
|
||||
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",
|
||||
@@ -614,7 +728,8 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn email_with_png() {
|
||||
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
|
||||
let img = std::fs::read("./docs/lettre.png").unwrap();
|
||||
let m = Message::builder()
|
||||
.date(date)
|
||||
@@ -626,21 +741,16 @@ mod test {
|
||||
MultiPart::related()
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/html; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(String::from(
|
||||
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
)),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType("image/png".parse().unwrap()))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Inline,
|
||||
parameters: vec![],
|
||||
})
|
||||
.header(header::ContentId("<123>".into()))
|
||||
.header(header::ContentType::parse("image/png").unwrap())
|
||||
.header(header::ContentDisposition::inline())
|
||||
.header(header::ContentId::from(String::from("<123>")))
|
||||
.body(img),
|
||||
),
|
||||
)
|
||||
@@ -651,11 +761,27 @@ mod test {
|
||||
let expected = String::from_utf8(file_expected).unwrap();
|
||||
|
||||
for (i, line) in output.lines().zip(expected.lines()).enumerate() {
|
||||
if i == 6 || i == 8 || i == 13 || i == 232 {
|
||||
if i == 7 || i == 9 || i == 14 || i == 233 {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert_eq!(line.0, line.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_message_id() {
|
||||
let mut ids = std::collections::HashSet::with_capacity(10);
|
||||
for _ in 0..1000 {
|
||||
ids.insert(make_message_id());
|
||||
}
|
||||
|
||||
// Ensure there are no duplicates
|
||||
assert_eq!(1000, ids.len());
|
||||
|
||||
// Ensure correct length
|
||||
for id in ids {
|
||||
assert_eq!(36, id.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// https://tools.ietf.org/html/rfc1522
|
||||
|
||||
fn allowed_char(c: char) -> bool {
|
||||
c >= 1 as char && c <= 9 as char
|
||||
|| c == 11 as char
|
||||
|| c == 12 as char
|
||||
|| c >= 14 as char && c <= 127 as char
|
||||
}
|
||||
|
||||
pub fn encode(s: &str) -> String {
|
||||
if s.chars().all(allowed_char) {
|
||||
s.into()
|
||||
} else {
|
||||
format!("=?utf-8?b?{}?=", base64::encode(s))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(s: &str) -> Option<String> {
|
||||
s.strip_prefix("=?utf-8?b?")
|
||||
.and_then(|stripped| stripped.strip_suffix("?="))
|
||||
.map_or_else(
|
||||
|| Some(s.into()),
|
||||
|stripped| {
|
||||
let decoded = base64::decode(stripped).ok()?;
|
||||
let decoded = String::from_utf8(decoded).ok()?;
|
||||
Some(decoded)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{decode, encode};
|
||||
|
||||
#[test]
|
||||
fn encode_ascii() {
|
||||
assert_eq!(&encode("Kayo. ?"), "Kayo. ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ascii() {
|
||||
assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_utf8() {
|
||||
assert_eq!(
|
||||
&encode("Привет, мир!"),
|
||||
"=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8() {
|
||||
assert_eq!(
|
||||
decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="),
|
||||
Some("Привет, мир!".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,97 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
};
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// JSON error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Json(serde_json::Error),
|
||||
use crate::BoxError;
|
||||
|
||||
/// The Errors that may occur when sending an email over SMTP
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Json(ref err) => err.fmt(fmt),
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the error is a file I/O error
|
||||
pub fn is_io(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Io)
|
||||
}
|
||||
|
||||
/// Returns true if the error is an envelope serialization or deserialization error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub fn is_envelope(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Envelope)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// File I/O error
|
||||
Io,
|
||||
/// Envelope serialization/deserialization error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Envelope,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::file::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.inner.kind {
|
||||
Kind::Io => f.write_str("response error")?,
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Kind::Envelope => f.write_str("internal client error")?,
|
||||
};
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Json(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
pub(crate) fn io<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Io, Some(e))
|
||||
}
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Error {
|
||||
Error::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Error::Client(string)
|
||||
}
|
||||
pub(crate) fn envelope<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Envelope, Some(e))
|
||||
}
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
@@ -38,12 +39,13 @@
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::with_envelope(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
@@ -66,11 +68,12 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{AsyncTransport, Tokio1Executor, Message, AsyncFileTransport};
|
||||
//!
|
||||
//! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||
@@ -91,11 +94,12 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{AsyncTransport, AsyncStd1Executor, Message, AsyncFileTransport};
|
||||
//!
|
||||
//! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||
@@ -132,36 +136,41 @@
|
||||
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
|
||||
//! ```
|
||||
|
||||
pub use self::error::Error;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use std::marker::PhantomData;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use self::error::Error;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
|
||||
mod error;
|
||||
|
||||
type Id = String;
|
||||
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Asynchronously writes the content and the envelope information to a file
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
pub struct AsyncFileTransport<E: Executor> {
|
||||
inner: FileTransport,
|
||||
marker_: PhantomData<E>,
|
||||
@@ -199,22 +208,22 @@ impl FileTransport {
|
||||
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||
use std::fs;
|
||||
|
||||
let eml_file = self.path.join(format!("{}.eml", email_id));
|
||||
let eml = fs::read(eml_file)?;
|
||||
let eml_file = self.path.join(format!("{email_id}.eml"));
|
||||
let eml = fs::read(eml_file).map_err(error::io)?;
|
||||
|
||||
let json_file = self.path.join(format!("{}.json", email_id));
|
||||
let json = fs::read(&json_file)?;
|
||||
let envelope = serde_json::from_slice(&json)?;
|
||||
let json_file = self.path.join(format!("{email_id}.json"));
|
||||
let json = fs::read(json_file).map_err(error::io)?;
|
||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||
|
||||
Ok((envelope, eml))
|
||||
}
|
||||
|
||||
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
|
||||
self.path.join(format!("{}.{}", email_id, extension))
|
||||
self.path.join(format!("{email_id}.{extension}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> AsyncFileTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
@@ -246,12 +255,12 @@ where
|
||||
/// Reads the envelope and the raw message content.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||
let eml_file = self.inner.path.join(format!("{}.eml", email_id));
|
||||
let eml = E::fs_read(&eml_file).await?;
|
||||
let eml_file = self.inner.path.join(format!("{email_id}.eml"));
|
||||
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 = E::fs_read(&json_file).await?;
|
||||
let envelope = serde_json::from_slice(&json)?;
|
||||
let json_file = self.inner.path.join(format!("{email_id}.json"));
|
||||
let json = E::fs_read(&json_file).await.map_err(error::io)?;
|
||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||
|
||||
Ok((envelope, eml))
|
||||
}
|
||||
@@ -267,13 +276,16 @@ impl Transport for FileTransport {
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let file = self.path(&email_id, "eml");
|
||||
fs::write(file, email)?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?file, "writing email to");
|
||||
fs::write(file, email).map_err(error::io)?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.save_envelope {
|
||||
let file = self.path(&email_id, "json");
|
||||
fs::write(file, serde_json::to_string(&envelope)?)?;
|
||||
let buf = serde_json::to_string(&envelope).map_err(error::envelope)?;
|
||||
fs::write(file, buf).map_err(error::io)?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
@@ -283,7 +295,7 @@ impl Transport for FileTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
#[async_trait]
|
||||
impl<E> AsyncTransport for AsyncFileTransport<E>
|
||||
where
|
||||
@@ -296,14 +308,16 @@ where
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let file = self.inner.path(&email_id, "eml");
|
||||
E::fs_write(&file, email).await?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?file, "writing email to");
|
||||
E::fs_write(&file, email).await.map_err(error::io)?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.inner.save_envelope {
|
||||
let file = self.inner.path(&email_id, "json");
|
||||
let buf = serde_json::to_vec(&envelope)?;
|
||||
E::fs_write(&file, &buf).await?;
|
||||
let buf = serde_json::to_vec(&envelope).map_err(error::envelope)?;
|
||||
E::fs_write(&file, &buf).await.map_err(error::io)?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
|
||||
@@ -1,37 +1,107 @@
|
||||
//! ### Sending Messages
|
||||
//! ## Transports for sending emails
|
||||
//!
|
||||
//! This section explains how to manipulate emails you have created.
|
||||
//! This module contains `Transport`s for sending emails. A `Transport` implements a high-level API
|
||||
//! for sending emails. It automatically manages the underlying resources and doesn't require any
|
||||
//! specific knowledge of email protocols in order to be used.
|
||||
//!
|
||||
//! This mailer contains several different transports for your emails. To be sendable, the
|
||||
//! emails have to implement `Email`, which is the case for emails created with `lettre::builder`.
|
||||
//! ### Getting started
|
||||
//!
|
||||
//! Sending emails from your programs requires using an email relay, as client libraries are not
|
||||
//! designed to handle email delivery by themselves. Depending on your infrastructure, your relay
|
||||
//! could be:
|
||||
//!
|
||||
//! * a service from your Cloud or hosting provider
|
||||
//! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either
|
||||
//! locally on your servers or accessible over the network
|
||||
//! * a dedicated external service, like Mailchimp, Mailgun, etc.
|
||||
//!
|
||||
//! In most cases, the best option is to:
|
||||
//!
|
||||
//! * Use the [`SMTP`] transport, with the [`relay`] builder (or one of its async counterparts)
|
||||
//! with your server's hostname. They provide modern and secure defaults.
|
||||
//! * Use the [`credentials`] method of the builder to pass your credentials.
|
||||
//!
|
||||
//! These should be enough to safely cover most use cases.
|
||||
//!
|
||||
//! ### Available transports
|
||||
//!
|
||||
//! The following transports are available:
|
||||
//!
|
||||
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
|
||||
//! the preferred way of sending emails.
|
||||
//! * The `SendmailTransport` uses the sendmail command to send messages. It is an alternative to
|
||||
//! the SMTP transport.
|
||||
//! * The `FileTransport` creates a file containing the email content to be sent. It can be used
|
||||
//! for debugging or if you want to keep all sent emails.
|
||||
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the
|
||||
//! logs.
|
||||
//! | Module | Protocol | Sync API | Async API | Description |
|
||||
//! | ------------ | -------- | --------------------- | -------------------------- | ------------------------------------------------------- |
|
||||
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
|
||||
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
|
||||
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
|
||||
//! | [`stub`] | Debug | [`StubTransport`] | [`AsyncStubTransport`] | Drops the email - Useful for debugging |
|
||||
//!
|
||||
//! ## Building an email
|
||||
//!
|
||||
//! Emails can either be built though [`Message`], which is a typed API for constructing emails
|
||||
//! (find out more about it by going over the [`message`][crate::message] module),
|
||||
//! or via external means.
|
||||
//!
|
||||
//! [`Message`]s can be sent via [`Transport::send`] or [`AsyncTransport::send`], while messages
|
||||
//! built without lettre's [`message`][crate::message] APIs can be sent via [`Transport::send_raw`]
|
||||
//! or [`AsyncTransport::send_raw`].
|
||||
//!
|
||||
//! ## Brief example
|
||||
//!
|
||||
//! This example shows how to build an email and send it via an SMTP relay server.
|
||||
//! It is in no way a complete example, but it shows how to get started with lettre.
|
||||
//! More examples can be found by looking at the specific modules, linked in the _Module_ column
|
||||
//! of the [table above](#transports-for-sending-emails).
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
//!
|
||||
//! // Open a remote connection to the SMTP relay server
|
||||
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
|
||||
//! .credentials(creds)
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email
|
||||
//! match mailer.send(&email) {
|
||||
//! Ok(_) => println!("Email sent successfully!"),
|
||||
//! Err(e) => panic!("Could not send email: {e:?}"),
|
||||
//! }
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # #[cfg(not(all(feature = "builder", feature = "smtp-transport")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent
|
||||
//! [`SMTP`]: crate::transport::smtp
|
||||
//! [`relay`]: crate::SmtpTransport::relay
|
||||
//! [`starttls_relay`]: crate::SmtpTransport::starttls_relay
|
||||
//! [`credentials`]: crate::transport::smtp::SmtpTransportBuilder::credentials
|
||||
//! [`Message`]: crate::Message
|
||||
//! [`file`]: self::file
|
||||
//! [`SmtpTransport`]: crate::SmtpTransport
|
||||
//! [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
//! [`SendmailTransport`]: crate::SendmailTransport
|
||||
//! [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
//! [`FileTransport`]: crate::FileTransport
|
||||
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
||||
//! [`AsyncStubTransport`]: crate::transport::stub::AsyncStubTransport
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "use lettre::AsyncStd1Transport")]
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub use self::AsyncTransport as AsyncStd1Transport;
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "use lettre::Tokio1Transport")]
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::AsyncTransport as Tokio1Transport;
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "use lettre::Tokio02Transport")]
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub use self::AsyncTransport as Tokio02Transport;
|
||||
|
||||
use crate::Envelope;
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::Message;
|
||||
@@ -58,6 +128,9 @@ pub trait Transport {
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("starting to send an email");
|
||||
|
||||
let raw = message.formatted();
|
||||
self.send_raw(message.envelope(), &raw)
|
||||
}
|
||||
@@ -66,11 +139,8 @@ pub trait Transport {
|
||||
}
|
||||
|
||||
/// Async Transport method for emails
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
|
||||
)]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[async_trait]
|
||||
pub trait AsyncTransport {
|
||||
/// Response produced by the Transport
|
||||
@@ -83,9 +153,12 @@ pub trait AsyncTransport {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
// TODO take &Message
|
||||
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 envelope = message.envelope();
|
||||
self.send_raw(&envelope, &raw).await
|
||||
self.send_raw(envelope, &raw).await
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
|
||||
@@ -1,52 +1,93 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(String),
|
||||
/// Error parsing UTF8 in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
use crate::BoxError;
|
||||
|
||||
/// The Errors that may occur when sending an email over sendmail
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Client(ref err) => err.fmt(fmt),
|
||||
Utf8Parsing(ref err) => err.fmt(fmt),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the error is from client
|
||||
pub fn is_client(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Client)
|
||||
}
|
||||
|
||||
/// Returns true if the error comes from the response
|
||||
pub fn is_response(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Response)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// Error parsing a response
|
||||
Response,
|
||||
/// Internal client error
|
||||
Client,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::sendmail::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.inner.kind {
|
||||
Kind::Response => f.write_str("response error")?,
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
};
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Response, Some(e))
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Client, Some(e))
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//! The sendmail transport sends the email using the local `sendmail` command.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! # use lettre::{Message, Transport, SendmailTransport};
|
||||
//! use lettre::{Message, SendmailTransport, Transport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
@@ -26,37 +26,16 @@
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 0.2 example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "tokio02", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, Tokio02Executor, AsyncSendmailTransport, SendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 1.x example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, Tokio1Executor, AsyncSendmailTransport, SendmailTransport};
|
||||
//! use lettre::{
|
||||
//! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
@@ -76,7 +55,7 @@
|
||||
//!
|
||||
//!```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
|
||||
@@ -95,50 +74,56 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub use self::error::Error;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use std::marker::PhantomData;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
io::prelude::*,
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub use self::error::Error;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
|
||||
mod error;
|
||||
|
||||
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
|
||||
const DEFAULT_SENDMAIL: &str = "sendmail";
|
||||
|
||||
/// Sends an email using the `sendmail` command
|
||||
/// Sends emails using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
|
||||
pub struct SendmailTransport {
|
||||
command: OsString,
|
||||
}
|
||||
|
||||
/// Asynchronously sends emails using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSendmailTransport<E: Executor> {
|
||||
inner: SendmailTransport,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
/// Creates a new transport with the `sendmail` command
|
||||
///
|
||||
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
|
||||
/// use [SendmailTransport::new_with_command].
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: DEFAUT_SENDMAIL.into(),
|
||||
command: DEFAULT_SENDMAIL.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,12 +149,15 @@ impl SendmailTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> AsyncSendmailTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
/// Creates a new transport with the `sendmail` command
|
||||
///
|
||||
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
|
||||
/// use [AsyncSendmailTransport::new_with_command].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: SendmailTransport::new(),
|
||||
@@ -185,24 +173,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
|
||||
use tokio02_crate::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.inner.command);
|
||||
c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
|
||||
use tokio1_crate::process::Command;
|
||||
@@ -247,7 +217,7 @@ impl Default for SendmailTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> Default for AsyncSendmailTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
@@ -262,16 +232,25 @@ impl Transport for SendmailTransport {
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
// Spawn the sendmail command
|
||||
let mut process = self.command(envelope).spawn()?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(command = ?self.command, "sending email with");
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(email)?;
|
||||
let output = process.wait_with_output()?;
|
||||
// Spawn the sendmail command
|
||||
let mut process = self.command(envelope).spawn().map_err(error::client)?;
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(email)
|
||||
.map_err(error::client)?;
|
||||
let output = process.wait_with_output().map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error::Error::Client(String::from_utf8(output.stderr)?))
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,43 +264,28 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
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);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
let mut process = command.spawn().map_err(error::client)?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email).await?;
|
||||
let output = process.output().await?;
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(email)
|
||||
.await
|
||||
.map_err(error::client)?;
|
||||
let output = process.output().await.map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSendmailTransport<Tokio02Executor> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio02_crate::io::AsyncWriteExt;
|
||||
|
||||
let mut command = self.tokio02_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email).await?;
|
||||
let output = process.wait_with_output().await?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -335,18 +299,28 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio1_crate::io::AsyncWriteExt;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(command = ?self.inner.command, "sending email with");
|
||||
|
||||
let mut command = self.tokio1_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
let mut process = command.spawn().map_err(error::client)?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email).await?;
|
||||
let output = process.wait_with_output().await?;
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(email)
|
||||
.await
|
||||
.map_err(error::client)?;
|
||||
let output = process.wait_with_output().await.map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,37 @@
|
||||
use std::marker::PhantomData;
|
||||
#[cfg(feature = "pool")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
marker::PhantomData,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
use super::pool::async_impl::Pool;
|
||||
#[cfg(feature = "pool")]
|
||||
use super::PoolConfig;
|
||||
use super::{
|
||||
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
|
||||
};
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{Envelope, Executor};
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct AsyncSmtpTransport<E> {
|
||||
// TODO: pool
|
||||
/// Asynchronously sends emails using the SMTP protocol
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransport<E: Executor> {
|
||||
#[cfg(feature = "pool")]
|
||||
inner: Arc<Pool<E>>,
|
||||
#[cfg(not(feature = "pool"))]
|
||||
inner: AsyncSmtpClient<E>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSmtpTransport<Tokio02Executor> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
|
||||
@@ -51,6 +44,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -86,13 +80,19 @@ where
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
)))
|
||||
)]
|
||||
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT};
|
||||
|
||||
@@ -103,7 +103,7 @@ where
|
||||
.tls(Tls::Wrapper(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
|
||||
/// Simple and secure transport, using STARTTLS to obtain encrypted connections
|
||||
///
|
||||
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
|
||||
/// that don't take SMTPS connections.
|
||||
@@ -115,13 +115,19 @@ where
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
)))
|
||||
)]
|
||||
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{Tls, TlsParameters, SUBMISSION_PORT};
|
||||
|
||||
@@ -145,17 +151,131 @@ where
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60-seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
AsyncSmtpTransportBuilder { info: new }
|
||||
AsyncSmtpTransportBuilder::new(server)
|
||||
}
|
||||
|
||||
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// The protocol, credentials, host and port can be provided in a single URL.
|
||||
/// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
|
||||
/// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
|
||||
/// The path section of the url can be used to set an alternative name for
|
||||
/// the HELO / EHLO command.
|
||||
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
|
||||
/// will set the HELO / EHLO name `client.example.com`.
|
||||
///
|
||||
/// <table>
|
||||
/// <thead>
|
||||
/// <tr>
|
||||
/// <th>scheme</th>
|
||||
/// <th>tls parameter</th>
|
||||
/// <th>example</th>
|
||||
/// <th>remarks</th>
|
||||
/// </tr>
|
||||
/// </thead>
|
||||
/// <tbody>
|
||||
/// <tr>
|
||||
/// <td>smtps</td>
|
||||
/// <td>-</td>
|
||||
/// <td>smtps://smtp.example.com</td>
|
||||
/// <td>SMTP over TLS, recommended method</td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>required</td>
|
||||
/// <td>smtp://smtp.example.com?tls=required</td>
|
||||
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>opportunistic</td>
|
||||
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
|
||||
/// <td>
|
||||
/// SMTP with optionally STARTTLS when supported by the server.
|
||||
/// Caution: this method is vulnerable to a man-in-the-middle attack.
|
||||
/// Not recommended for production use.
|
||||
/// </td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>-</td>
|
||||
/// <td>smtp://smtp.example.com</td>
|
||||
/// <td>Unencrypted SMTP, not recommended for production use.</td>
|
||||
/// </tr>
|
||||
/// </tbody>
|
||||
/// </table>
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use lettre::{
|
||||
/// message::header::ContentType, transport::smtp::authentication::Credentials,
|
||||
/// AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
/// };
|
||||
/// # use tokio1_crate as tokio;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let email = Message::builder()
|
||||
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
/// .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
/// .subject("Happy new year")
|
||||
/// .header(ContentType::TEXT_PLAIN)
|
||||
/// .body(String::from("Be happy!"))
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Open a remote connection to gmail
|
||||
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
|
||||
/// "smtps://username:password@smtp.example.com:465",
|
||||
/// )
|
||||
/// .unwrap()
|
||||
/// .build();
|
||||
///
|
||||
/// // Send the email
|
||||
/// match mailer.send(email).await {
|
||||
/// Ok(_) => println!("Email sent successfully!"),
|
||||
/// Err(e) => panic!("Could not send email: {e:?}"),
|
||||
/// }
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
super::connection_url::from_connection_url(connection_url)
|
||||
}
|
||||
|
||||
/// Tests the SMTP connection
|
||||
///
|
||||
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
||||
/// The connection is closed afterward if a connection pool is not used.
|
||||
pub async fn test_connection(&self) -> Result<bool, Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let is_connected = conn.test_connected().await;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(is_connected)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Debug for AsyncSmtpTransport<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("AsyncSmtpTransport");
|
||||
builder.field("inner", &self.inner);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +285,9 @@ where
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "pool")]
|
||||
inner: Arc::clone(&self.inner),
|
||||
#[cfg(not(feature = "pool"))]
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
@@ -172,14 +295,30 @@ where
|
||||
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `AsyncSmtpTransport`
|
||||
impl AsyncSmtpTransportBuilder {
|
||||
// Create new builder with default parameters
|
||||
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
|
||||
let info = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
AsyncSmtpTransportBuilder {
|
||||
info,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
@@ -204,21 +343,43 @@ impl AsyncSmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.info.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
)))
|
||||
)]
|
||||
pub fn tls(mut self, tls: super::Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport (with default pool if enabled)
|
||||
/// Use a custom configuration for the connection pool
|
||||
///
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
#[cfg(feature = "pool")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
|
||||
self.pool_config = pool_config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport
|
||||
pub fn build<E>(self) -> AsyncSmtpTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
@@ -227,14 +388,18 @@ impl AsyncSmtpTransportBuilder {
|
||||
info: self.info,
|
||||
marker_: PhantomData,
|
||||
};
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
let client = Pool::new(self.pool_config, client);
|
||||
|
||||
AsyncSmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
pub struct AsyncSmtpClient<C> {
|
||||
pub struct AsyncSmtpClient<E> {
|
||||
info: SmtpInfo,
|
||||
marker_: PhantomData<C>,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E> AsyncSmtpClient<E>
|
||||
@@ -248,18 +413,29 @@ where
|
||||
let mut conn = E::connect(
|
||||
&self.info.server,
|
||||
self.info.port,
|
||||
self.info.timeout,
|
||||
&self.info.hello_name,
|
||||
&self.info.tls,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, &credentials).await?;
|
||||
conn.auth(&self.info.authentication, credentials).await?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Debug for AsyncSmtpClient<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("AsyncSmtpClient");
|
||||
builder.field("info", &self.info);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// `clone` is unused when the `pool` feature is on
|
||||
#[allow(dead_code)]
|
||||
impl<E> AsyncSmtpClient<E>
|
||||
where
|
||||
E: Executor,
|
||||
@@ -271,22 +447,3 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "use lettre::Executor instead")]
|
||||
pub use crate::Executor as AsyncSmtpConnector;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "use lettre::Tokio02Executor instead")]
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub type Tokio02Connector = crate::Tokio02Executor;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "use lettre::Tokio1Executor instead")]
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub type Tokio1Connector = crate::Tokio1Executor;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "use lettre::AsyncStd1Executor instead")]
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub type AsyncStd1Connector = crate::AsyncStd1Executor;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
//! Provides limited SASL authentication mechanisms
|
||||
|
||||
use crate::transport::smtp::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
|
||||
use crate::transport::smtp::error::{self, Error};
|
||||
|
||||
/// Accepted authentication mechanisms
|
||||
///
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Contains user credentials
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Credentials {
|
||||
authentication_identity: String,
|
||||
@@ -35,6 +37,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Credentials {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Credentials").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
@@ -43,7 +51,7 @@ pub enum Mechanism {
|
||||
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
|
||||
Plain,
|
||||
/// LOGIN authentication mechanism
|
||||
/// Obsolete but needed for some providers (like office365)
|
||||
/// Obsolete but needed for some providers (like Office 365)
|
||||
///
|
||||
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
|
||||
Login,
|
||||
@@ -63,7 +71,7 @@ impl Display for Mechanism {
|
||||
}
|
||||
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports initial response
|
||||
/// Does the mechanism support initial response?
|
||||
pub fn supports_initial_response(self) -> bool {
|
||||
match self {
|
||||
Mechanism::Plain | Mechanism::Xoauth2 => true,
|
||||
@@ -80,28 +88,30 @@ impl Mechanism {
|
||||
) -> Result<String, Error> {
|
||||
match self {
|
||||
Mechanism::Plain => match challenge {
|
||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"\u{0}{}\u{0}{}",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
)),
|
||||
},
|
||||
Mechanism::Login => {
|
||||
let decoded_challenge =
|
||||
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
|
||||
let decoded_challenge = challenge
|
||||
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.authentication_identity.to_string());
|
||||
if ["User Name", "Username:", "Username", "User Name\0"]
|
||||
.contains(&decoded_challenge)
|
||||
{
|
||||
return Ok(credentials.authentication_identity.clone());
|
||||
}
|
||||
|
||||
if vec!["Password", "Password:"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.secret.to_string());
|
||||
if ["Password", "Password:", "Password\0"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.secret.clone());
|
||||
}
|
||||
|
||||
Err(Error::Client("Unrecognized challenge"))
|
||||
Err(error::client("Unrecognized challenge"))
|
||||
}
|
||||
Mechanism::Xoauth2 => match challenge {
|
||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"user={}\x01auth=Bearer {}\x01\x01",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
@@ -119,7 +129,7 @@ mod test {
|
||||
fn test_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!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
@@ -132,7 +142,7 @@ mod test {
|
||||
fn test_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!(
|
||||
mechanism.response(&credentials, Some("Username")).unwrap(),
|
||||
@@ -150,8 +160,8 @@ mod test {
|
||||
let mechanism = Mechanism::Xoauth2;
|
||||
|
||||
let credentials = Credentials::new(
|
||||
"username".to_string(),
|
||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
|
||||
"username".to_owned(),
|
||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -164,7 +174,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_from_user_pass_for_credentials() {
|
||||
assert_eq!(
|
||||
Credentials::new("alice".to_string(), "wonderland".to_string()),
|
||||
Credentials::new("alice".to_owned(), "wonderland".to_owned()),
|
||||
Credentials::from(("alice", "wonderland"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
use std::{fmt::Display, io};
|
||||
use std::{fmt::Display, net::IpAddr, time::Duration};
|
||||
|
||||
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
use super::async_net::AsyncTokioStream;
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::*,
|
||||
commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
|
||||
error,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
@@ -14,9 +19,6 @@ use crate::{
|
||||
Envelope,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
@@ -41,35 +43,65 @@ pub struct AsyncSmtpConnection {
|
||||
}
|
||||
|
||||
impl AsyncSmtpConnection {
|
||||
/// Get information about the server
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
/// Connects with existing async stream
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub async fn connect_tokio02(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub async fn connect_with_transport(
|
||||
stream: Box<dyn AsyncTokioStream>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_tokio02(hostname, port, tls_parameters).await?;
|
||||
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_address` is `Some`, then the address provided shall be used to bind the
|
||||
/// connection to a specific local address using [`tokio1_crate::net::TcpSocket::bind`].
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::time::Duration;
|
||||
/// # use lettre::transport::smtp::{client::{AsyncSmtpConnection, TlsParameters}, extension::ClientId};
|
||||
/// # use tokio1_crate::{self as tokio, net::ToSocketAddrs as _};
|
||||
/// #
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let connection = AsyncSmtpConnection::connect_tokio1(
|
||||
/// ("example.com", 465),
|
||||
/// Some(Duration::from_secs(60)),
|
||||
/// &ClientId::default(),
|
||||
/// Some(TlsParameters::new("example.com".to_owned())?),
|
||||
/// None,
|
||||
/// )
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub async fn connect_tokio1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
local_address: Option<IpAddr>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_tokio1(hostname, port, tls_parameters).await?;
|
||||
let stream =
|
||||
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
|
||||
.await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
@@ -77,13 +109,13 @@ impl AsyncSmtpConnection {
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub async fn connect_asyncstd1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_asyncstd1(hostname, port, tls_parameters).await?;
|
||||
let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
@@ -121,17 +153,17 @@ impl AsyncSmtpConnection {
|
||||
if envelope.has_non_ascii_addresses() {
|
||||
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
|
||||
// don't try to send non-ascii addresses (per RFC)
|
||||
return Err(Error::Client(
|
||||
return Err(error::client(
|
||||
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
// Check for non-ascii content in message
|
||||
// Check for non-ascii content in the message
|
||||
if !email.is_ascii() {
|
||||
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
return Err(Error::Client(
|
||||
return Err(error::client(
|
||||
"Message contains non-ascii chars but server does not support 8BITMIME",
|
||||
));
|
||||
}
|
||||
@@ -168,6 +200,12 @@ impl AsyncSmtpConnection {
|
||||
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
||||
}
|
||||
|
||||
/// Upgrade the connection using `STARTTLS`.
|
||||
///
|
||||
/// As described in [rfc3207]. Note that this mechanism has been deprecated in [rfc8314].
|
||||
///
|
||||
/// [rfc3207]: https://www.rfc-editor.org/rfc/rfc3207
|
||||
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
|
||||
#[allow(unused_variables)]
|
||||
pub async fn starttls(
|
||||
&mut self,
|
||||
@@ -176,17 +214,14 @@ impl AsyncSmtpConnection {
|
||||
) -> Result<(), Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
try_smtp!(self.command(Starttls).await, self);
|
||||
try_smtp!(
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters).await,
|
||||
self
|
||||
);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters).await?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name).await, self);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||
Err(error::client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +242,7 @@ impl AsyncSmtpConnection {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit).await;
|
||||
}
|
||||
let _ = self.stream.close().await;
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
@@ -224,7 +260,7 @@ impl AsyncSmtpConnection {
|
||||
self.command(Noop).await.is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
/// Sends an AUTH command with the given mechanism, and handles the challenge if needed
|
||||
pub async fn auth(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
@@ -233,12 +269,10 @@ impl AsyncSmtpConnection {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or(Error::Client(
|
||||
"No compatible authentication mechanism was found",
|
||||
))?;
|
||||
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
let mut challenges: u8 = 10;
|
||||
let mut response = self
|
||||
.command(Auth::new(mechanism, credentials.clone(), None)?)
|
||||
.await?;
|
||||
@@ -257,7 +291,7 @@ impl AsyncSmtpConnection {
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
Err(error::response("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
@@ -281,8 +315,16 @@ impl AsyncSmtpConnection {
|
||||
|
||||
/// Writes a string to the server
|
||||
async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream.get_mut().write_all(string).await?;
|
||||
self.stream.get_mut().flush().await?;
|
||||
self.stream
|
||||
.get_mut()
|
||||
.write_all(string)
|
||||
.await
|
||||
.map_err(error::network)?;
|
||||
self.stream
|
||||
.get_mut()
|
||||
.flush()
|
||||
.await
|
||||
.map_err(error::network)?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
@@ -293,27 +335,42 @@ impl AsyncSmtpConnection {
|
||||
pub async fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer).await? > 0 {
|
||||
while self
|
||||
.stream
|
||||
.read_line(&mut buffer)
|
||||
.await
|
||||
.map_err(error::network)?
|
||||
> 0
|
||||
{
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
if response.is_positive() {
|
||||
return Ok(response);
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(error::code(
|
||||
response.code(),
|
||||
Some(response.message().collect()),
|
||||
))
|
||||
}
|
||||
|
||||
return Err(response.into());
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(Error::Parsing(e.code));
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(Error::Parsing(e.code));
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
|
||||
Err(error::response("incomplete response"))
|
||||
}
|
||||
|
||||
/// The X509 certificate of the server (DER encoded)
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
self.stream.get_ref().peer_certificate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,87 @@
|
||||
#[cfg(any(
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
fmt, io, mem,
|
||||
net::{IpAddr, SocketAddr},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
use async_native_tls::TlsStream as AsyncStd1TlsStream;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
|
||||
use futures_io::{
|
||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
|
||||
Result as IoResult,
|
||||
};
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||
#[cfg(any(feature = "tokio1-rustls-tls", feature = "async-std1-rustls-tls"))]
|
||||
use rustls::pki_types::ServerName;
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
use tokio1_boring::SslStream as Tokio1SslStream;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
use async_std::net::TcpStream as AsyncStd1TcpStream;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
|
||||
use tokio1_crate::io::{AsyncRead, AsyncWrite, ReadBuf as Tokio1ReadBuf};
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::net::TcpStream as Tokio1TcpStream;
|
||||
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
use async_native_tls::TlsStream as AsyncStd1TlsStream;
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
use tokio02_native_tls_crate::TlsStream as Tokio02TlsStream;
|
||||
use tokio1_crate::net::{
|
||||
TcpSocket as Tokio1TcpSocket, TcpStream as Tokio1TcpStream,
|
||||
ToSocketAddrs as Tokio1ToSocketAddrs,
|
||||
};
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
use tokio02_rustls::client::TlsStream as Tokio02RustlsTlsStream;
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "tokio1-boring-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
use crate::transport::smtp::Error;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::transport::smtp::client::net::resolved_address_filter;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// A network stream
|
||||
#[derive(Debug)]
|
||||
pub struct AsyncNetworkStream {
|
||||
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
|
||||
// usually only one TLS backend at a time is going to be enabled,
|
||||
// so clippy::large_enum_variant doesn't make sense here
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum InnerAsyncNetworkStream {
|
||||
/// Plain Tokio 0.2 TCP stream
|
||||
#[cfg(feature = "tokio02")]
|
||||
Tokio02Tcp(Tokio02TcpStream),
|
||||
/// Encrypted Tokio 0.2 TCP stream
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
Tokio02NativeTls(Tokio02TlsStream<Tokio02TcpStream>),
|
||||
/// Encrypted Tokio 0.2 TCP stream
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
Tokio02RustlsTls(Tokio02RustlsTlsStream<Tokio02TcpStream>),
|
||||
/// Plain Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1")]
|
||||
Tokio1Tcp(Tokio1TcpStream),
|
||||
Tokio1Tcp(Box<dyn AsyncTokioStream>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>),
|
||||
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[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
|
||||
#[cfg(feature = "async-std1")]
|
||||
AsyncStd1Tcp(AsyncStd1TcpStream),
|
||||
@@ -105,29 +106,23 @@ impl AsyncNetworkStream {
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref s) => {
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Err(IoError::new(
|
||||
@@ -138,30 +133,71 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub async fn connect_tokio02(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = Tokio02TcpStream::connect((hostname, port)).await?;
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
|
||||
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub async fn connect_tokio1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = Tokio1TcpStream::connect((hostname, port)).await?;
|
||||
async fn try_connect<T: Tokio1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<Tokio1TcpStream, Error> {
|
||||
let addrs = tokio1_crate::net::lookup_host(server)
|
||||
.await
|
||||
.map_err(error::connection)?
|
||||
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
let socket = match addr.ip() {
|
||||
IpAddr::V4(_) => Tokio1TcpSocket::new_v4(),
|
||||
IpAddr::V6(_) => Tokio1TcpSocket::new_v6(),
|
||||
}
|
||||
.map_err(error::connection)?;
|
||||
if let Some(local_addr) = local_addr {
|
||||
socket
|
||||
.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 {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any supported address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = try_connect(server, timeout, local_addr).await?;
|
||||
let mut stream =
|
||||
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
@@ -169,12 +205,48 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub async fn connect_asyncstd1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port)).await?;
|
||||
// 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>(
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
) -> Result<AsyncStd1TcpStream, Error> {
|
||||
let addrs = server.to_socket_addrs().await.map_err(error::connection)?;
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
let connect_future = AsyncStd1TcpStream::connect(&addr);
|
||||
match async_std::future::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",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(match last_err {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
Some(t) => try_connect_timeout(server, t).await?,
|
||||
None => AsyncStd1TcpStream::connect(server)
|
||||
.await
|
||||
.map_err(error::connection)?,
|
||||
};
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
@@ -185,46 +257,35 @@ impl AsyncNetworkStream {
|
||||
|
||||
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(all(
|
||||
feature = "tokio02",
|
||||
not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(
|
||||
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(_) => {
|
||||
let _ = tls_parameters;
|
||||
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(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters).await?;
|
||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(
|
||||
@@ -239,13 +300,15 @@ impl AsyncNetworkStream {
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters).await?;
|
||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
@@ -253,54 +316,16 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
async fn upgrade_tokio02_tls(
|
||||
tcp_stream: Tokio02TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = std::mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
#[cfg(not(feature = "tokio02-native-tls"))]
|
||||
panic!("built without the tokio02-native-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
return {
|
||||
use tokio02_native_tls_crate::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(connector);
|
||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "tokio02-rustls-tls"))]
|
||||
panic!("built without the tokio02-rustls-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
return {
|
||||
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = connector.connect(domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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(
|
||||
tcp_stream: Tokio1TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
tcp_stream: Box<dyn AsyncTokioStream>,
|
||||
tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = std::mem::take(&mut tls_parameters.domain);
|
||||
let domain = tls_parameters.domain().to_owned();
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
@@ -313,7 +338,10 @@ impl AsyncNetworkStream {
|
||||
use tokio1_native_tls_crate::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(connector);
|
||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
||||
let stream = connector
|
||||
.connect(&domain, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
|
||||
};
|
||||
}
|
||||
@@ -324,25 +352,49 @@ impl AsyncNetworkStream {
|
||||
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
return {
|
||||
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
use tokio1_rustls::TlsConnector;
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
|
||||
let domain = ServerName::try_from(domain.as_str())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = connector.connect(domain, tcp_stream).await?;
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
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)]
|
||||
#[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(
|
||||
tcp_stream: AsyncStd1TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = std::mem::take(&mut tls_parameters.domain);
|
||||
let domain = mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
@@ -372,32 +424,36 @@ impl AsyncNetworkStream {
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
return {
|
||||
use async_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
use futures_rustls::TlsConnector;
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
|
||||
let domain = ServerName::try_from(domain.as_str())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = connector.connect(domain, tcp_stream).await?;
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
.connect(domain.to_owned(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(connector) => {
|
||||
panic!("boring-tls isn't supported with async-std yet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => false,
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
@@ -407,6 +463,55 @@ impl AsyncNetworkStream {
|
||||
InnerAsyncNetworkStream::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.peer_certificate()
|
||||
.map_err(error::tls)?
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_vec()),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
||||
.ssl()
|
||||
.peer_certificate()
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(t) => panic!("Unsupported"),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_vec()),
|
||||
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
@@ -415,15 +520,9 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
@@ -432,7 +531,7 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => {
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
@@ -441,7 +540,16 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => {
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
@@ -450,15 +558,11 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
||||
Pin::new(s).poll_read(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
||||
Pin::new(s).poll_read(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_read(cx, buf),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
@@ -473,29 +577,21 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
||||
Pin::new(s).poll_write(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
||||
Pin::new(s).poll_write(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
@@ -504,25 +600,21 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
@@ -531,25 +623,21 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
net::ToSocketAddrs,
|
||||
net::{IpAddr, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
use super::{ClientCodec, NetworkStream, TlsParameters};
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::*,
|
||||
commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
|
||||
error,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
@@ -44,6 +44,7 @@ pub struct SmtpConnection {
|
||||
}
|
||||
|
||||
impl SmtpConnection {
|
||||
/// Get information about the server
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
@@ -58,15 +59,16 @@ impl SmtpConnection {
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
local_address: Option<IpAddr>,
|
||||
) -> 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 mut conn = SmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
conn.set_timeout(timeout)?;
|
||||
conn.set_timeout(timeout).map_err(error::network)?;
|
||||
// TODO log
|
||||
let _response = conn.read_response()?;
|
||||
|
||||
@@ -91,17 +93,17 @@ impl SmtpConnection {
|
||||
if envelope.has_non_ascii_addresses() {
|
||||
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
|
||||
// don't try to send non-ascii addresses (per RFC)
|
||||
return Err(Error::Client(
|
||||
return Err(error::client(
|
||||
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
// Check for non-ascii content in message
|
||||
// Check for non-ascii content in the message
|
||||
if !email.is_ascii() {
|
||||
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
return Err(Error::Client(
|
||||
return Err(error::client(
|
||||
"Message contains non-ascii chars but server does not support 8BITMIME",
|
||||
));
|
||||
}
|
||||
@@ -141,22 +143,26 @@ impl SmtpConnection {
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
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.stream.get_mut().upgrade_tls(tls_parameters), self);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name), self);
|
||||
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
|
||||
// when a TLS library is enabled
|
||||
unreachable!("TLS support required but not supported");
|
||||
} else {
|
||||
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||
Err(error::client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +183,7 @@ impl SmtpConnection {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit);
|
||||
}
|
||||
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
@@ -200,7 +207,7 @@ impl SmtpConnection {
|
||||
self.command(Noop).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
/// Sends an AUTH command with the given mechanism, and handles the challenge if needed
|
||||
pub fn auth(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
@@ -209,9 +216,7 @@ impl SmtpConnection {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or(Error::Client(
|
||||
"No compatible authentication mechanism was found",
|
||||
))?;
|
||||
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
@@ -230,7 +235,7 @@ impl SmtpConnection {
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
Err(error::response("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
@@ -238,11 +243,12 @@ impl SmtpConnection {
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
let mut out_buf = Vec::with_capacity(message.len());
|
||||
codec.encode(message, &mut out_buf);
|
||||
self.write(out_buf.as_slice())?;
|
||||
self.write(b"\r\n.\r\n")?;
|
||||
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
@@ -254,8 +260,11 @@ impl SmtpConnection {
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream.get_mut().write_all(string)?;
|
||||
self.stream.get_mut().flush()?;
|
||||
self.stream
|
||||
.get_mut()
|
||||
.write_all(string)
|
||||
.map_err(error::network)?;
|
||||
self.stream.get_mut().flush().map_err(error::network)?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
@@ -266,27 +275,36 @@ impl SmtpConnection {
|
||||
pub fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer)? > 0 {
|
||||
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
if response.is_positive() {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
return Err(response.into());
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(error::code(
|
||||
response.code(),
|
||||
Some(response.message().collect()),
|
||||
))
|
||||
};
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(Error::Parsing(e.code));
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(Error::Parsing(e.code));
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
|
||||
Err(error::response("incomplete response"))
|
||||
}
|
||||
|
||||
/// The X509 certificate of the server (DER encoded)
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
self.stream.get_ref().peer_certificate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Write},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
pub type MockCursor = Cursor<Vec<u8>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockStream {
|
||||
reader: Arc<Mutex<MockCursor>>,
|
||||
writer: Arc<Mutex<MockCursor>>,
|
||||
}
|
||||
|
||||
impl Default for MockStream {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MockStream {
|
||||
pub fn new() -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_vec(vec: Vec<u8>) -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_vec(&mut self) -> Vec<u8> {
|
||||
let mut cursor = self.writer.lock().unwrap();
|
||||
let vec = cursor.get_ref().to_vec();
|
||||
cursor.set_position(0);
|
||||
cursor.get_mut().clear();
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn next_vec(&mut self, vec: &[u8]) {
|
||||
let mut cursor = self.reader.lock().unwrap();
|
||||
cursor.set_position(0);
|
||||
cursor.get_mut().clear();
|
||||
cursor.get_mut().extend_from_slice(vec);
|
||||
}
|
||||
|
||||
pub fn swap(&mut self) {
|
||||
let mut cur_write = self.writer.lock().unwrap();
|
||||
let mut cur_read = self.reader.lock().unwrap();
|
||||
let vec_write = cur_write.get_ref().to_vec();
|
||||
let vec_read = cur_read.get_ref().to_vec();
|
||||
cur_write.set_position(0);
|
||||
cur_read.set_position(0);
|
||||
cur_write.get_mut().clear();
|
||||
cur_read.get_mut().clear();
|
||||
// swap cursors
|
||||
cur_read.get_mut().extend_from_slice(vec_write.as_slice());
|
||||
cur_write.get_mut().extend_from_slice(vec_read.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for MockStream {
|
||||
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
|
||||
self.writer.lock().unwrap().write(msg)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for MockStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.reader.lock().unwrap().read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::MockStream;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[test]
|
||||
fn write_take_test() {
|
||||
let mut mock = MockStream::new();
|
||||
// write to mock stream
|
||||
mock.write_all(&[1, 2, 3]).unwrap();
|
||||
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_with_vec_test() {
|
||||
let mut mock = MockStream::with_vec(vec![4, 5]);
|
||||
let mut vec = Vec::new();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut cloned = mock.clone();
|
||||
mock.write_all(&[6, 7]).unwrap();
|
||||
assert_eq!(cloned.take_vec(), vec![6, 7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut vec = Vec::new();
|
||||
mock.write_all(&[8, 9, 10]).unwrap();
|
||||
mock.swap();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![8, 9, 10]);
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,14 @@
|
||||
//!
|
||||
//! # #[cfg(feature = "smtp-transport")]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection};
|
||||
//! use lettre::transport::smtp::{
|
||||
//! client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT,
|
||||
//! };
|
||||
//!
|
||||
//! let hello = ClientId::Domain("my_hostname".to_string());
|
||||
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None)?;
|
||||
//! client.command(
|
||||
//! Mail::new(Some("user@example.com".parse()?), vec![])
|
||||
//! )?;
|
||||
//! client.command(
|
||||
//! Rcpt::new("user@example.org".parse()?, vec![])
|
||||
//! )?;
|
||||
//! let hello = ClientId::Domain("my_hostname".to_owned());
|
||||
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?;
|
||||
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
|
||||
//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
|
||||
//! client.command(Data)?;
|
||||
//! client.message("Test email".as_bytes())?;
|
||||
//! client.command(Quit)?;
|
||||
@@ -27,75 +25,82 @@
|
||||
#[cfg(feature = "serde")]
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
pub(crate) use self::async_connection::AsyncSmtpConnection;
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
pub(crate) use self::async_net::AsyncNetworkStream;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_connection::AsyncSmtpConnection;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_net::AsyncNetworkStream;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::async_net::AsyncTokioStream;
|
||||
use self::net::NetworkStream;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
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::{
|
||||
connection::SmtpConnection,
|
||||
mock::MockStream,
|
||||
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
|
||||
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_connection;
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_net;
|
||||
mod connection;
|
||||
mod mock;
|
||||
mod net;
|
||||
mod tls;
|
||||
|
||||
/// The codec used for transparency
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug)]
|
||||
struct ClientCodec {
|
||||
escape_count: u8,
|
||||
status: CodecStatus,
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub fn new() -> Self {
|
||||
ClientCodec::default()
|
||||
Self {
|
||||
status: CodecStatus::StartOfNewLine,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds transparency
|
||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
|
||||
match frame.len() {
|
||||
0 => {
|
||||
match self.escape_count {
|
||||
0 => buf.extend_from_slice(b"\r\n.\r\n"),
|
||||
1 => buf.extend_from_slice(b"\n.\r\n"),
|
||||
2 => buf.extend_from_slice(b".\r\n"),
|
||||
_ => unreachable!(),
|
||||
for &b in frame {
|
||||
buf.push(b);
|
||||
match (b, self.status) {
|
||||
(b'\r', _) => {
|
||||
self.status = CodecStatus::StartingNewLine;
|
||||
}
|
||||
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 { 0 },
|
||||
_ => unreachable!(),
|
||||
(_, CodecStatus::StartingNewLine) => {
|
||||
self.status = CodecStatus::MiddleOfLine;
|
||||
}
|
||||
if self.escape_count == 3 {
|
||||
self.escape_count = 0;
|
||||
buf.extend_from_slice(&frame[start..idx]);
|
||||
buf.extend_from_slice(b".");
|
||||
start = idx;
|
||||
(b'.', CodecStatus::StartOfNewLine) => {
|
||||
self.status = CodecStatus::MiddleOfLine;
|
||||
buf.push(b'.');
|
||||
}
|
||||
(_, CodecStatus::StartOfNewLine) => {
|
||||
self.status = CodecStatus::MiddleOfLine;
|
||||
}
|
||||
buf.extend_from_slice(&frame[start..]);
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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\>"
|
||||
/// Used for debug displays
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -109,10 +114,12 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_codec() {
|
||||
let mut buf = Vec::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\r\n", &mut buf);
|
||||
codec.encode(b".\r\n", &mut buf);
|
||||
codec.encode(b"\r\ntest", &mut buf);
|
||||
codec.encode(b"te\r\n.\r\nst", &mut buf);
|
||||
@@ -121,9 +128,13 @@ mod test {
|
||||
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\r\n", &mut buf);
|
||||
codec.encode(b".test\r\n", &mut buf);
|
||||
codec.encode(b"test.\r\n", &mut buf);
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
"test\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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||
mem,
|
||||
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
use boring::ssl::SslStream;
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsStream;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::{ClientSession, StreamOwned};
|
||||
use rustls::{pki_types::ServerName, ClientConnection, 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::{MockStream, TlsParameters};
|
||||
use crate::transport::smtp::Error;
|
||||
use super::TlsParameters;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// A network stream
|
||||
pub struct NetworkStream {
|
||||
@@ -34,44 +37,56 @@ enum InnerNetworkStream {
|
||||
NativeTls(TlsStream<TcpStream>),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(StreamOwned<ClientSession, TcpStream>),
|
||||
/// Mock stream
|
||||
Mock(MockStream),
|
||||
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(SslStream<TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
impl NetworkStream {
|
||||
fn new(inner: InnerNetworkStream) -> Self {
|
||||
NetworkStream { inner }
|
||||
if let InnerNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
}
|
||||
|
||||
pub fn new_mock(mock: MockStream) -> Self {
|
||||
Self::new(InnerNetworkStream::Mock(mock))
|
||||
NetworkStream { inner }
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
InnerNetworkStream::RustlsTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
80,
|
||||
))),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdowns the connection
|
||||
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.shutdown(how),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
InnerNetworkStream::RustlsTls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,25 +94,49 @@ impl NetworkStream {
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<NetworkStream, Error> {
|
||||
fn try_connect_timeout<T: ToSocketAddrs>(
|
||||
fn try_connect<T: ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
timeout: Option<Duration>,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<TcpStream, Error> {
|
||||
let addrs = server.to_socket_addrs()?;
|
||||
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;
|
||||
|
||||
for addr in addrs {
|
||||
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
|
||||
return Ok(result);
|
||||
let socket = socket2::Socket::new(
|
||||
Domain::for_address(addr),
|
||||
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),
|
||||
}
|
||||
}
|
||||
Err(Error::Client("Could not connect"))
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
Some(t) => try_connect_timeout(server, t)?,
|
||||
None => TcpStream::connect(server)?,
|
||||
};
|
||||
Err(match last_err {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = try_connect(server, timeout, local_addr)?;
|
||||
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters)?;
|
||||
@@ -107,17 +146,20 @@ impl NetworkStream {
|
||||
|
||||
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
#[cfg(not(any(
|
||||
feature = "native-tls",
|
||||
feature = "rustls-tls",
|
||||
feature = "boring-tls"
|
||||
)))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream =
|
||||
std::mem::replace(&mut self.inner, InnerNetworkStream::Mock(MockStream::new()));
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
@@ -130,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(
|
||||
tcp_stream: TcpStream,
|
||||
tls_parameters: &TlsParameters,
|
||||
@@ -140,101 +182,204 @@ impl NetworkStream {
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
let stream = connector
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(|err| Error::Io(io::Error::new(io::ErrorKind::Other, err)))?;
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::NativeTls(stream)
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(connector) => {
|
||||
use webpki::DNSNameRef;
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())?;
|
||||
let stream = StreamOwned::new(
|
||||
ClientSession::new(&Arc::new(connector.clone()), domain),
|
||||
tcp_stream,
|
||||
);
|
||||
|
||||
let domain = ServerName::try_from(tls_parameters.domain())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
let connection = ClientConnection::new(Arc::clone(connector), domain.to_owned())
|
||||
.map_err(error::connection)?;
|
||||
let stream = StreamOwned::new(connection, tcp_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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(_) | InnerNetworkStream::Mock(_) => false,
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => false,
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => true,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(_) => true,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(_) => true,
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(stream) => Ok(stream
|
||||
.peer_certificate()
|
||||
.map_err(error::tls)?
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(stream) => Ok(stream
|
||||
.conn
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_vec()),
|
||||
#[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"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set write timeout for IO calls
|
||||
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(stream) => stream.set_write_timeout(duration),
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
InnerNetworkStream::RustlsTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.read(buf),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.read(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::NativeTls(s) => s.read(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::RustlsTls(s) => s.read(buf),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.read(buf),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.write(buf),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.write(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::NativeTls(s) => s.write(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::RustlsTls(s) => s.write(buf),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.write(buf),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.flush(),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.flush(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::NativeTls(s) => s.flush(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::RustlsTls(s) => s.flush(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.flush(),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,60 @@
|
||||
use std::fmt::{self, Debug};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
use std::{io, sync::Arc};
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
use boring::{
|
||||
ssl::{SslConnector, SslVersion},
|
||||
x509::store::X509StoreBuilder,
|
||||
};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use webpki::DNSNameRef;
|
||||
use rustls::{
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
crypto::{verify_tls12_signature, verify_tls13_signature},
|
||||
pki_types::{CertificateDer, ServerName, UnixTime},
|
||||
ClientConfig, DigitallySignedStruct, Error as TlsError, RootCertStore, SignatureScheme,
|
||||
};
|
||||
|
||||
use crate::transport::smtp::error::Error;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
// This is also rustls' default behavior
|
||||
#[cfg(feature = "native-tls")]
|
||||
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
|
||||
/// TLS protocol versions.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
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
|
||||
#[derive(Clone)]
|
||||
@@ -23,32 +63,86 @@ pub enum Tls {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
None,
|
||||
/// 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", feature = "boring-tls")))
|
||||
)]
|
||||
Opportunistic(TlsParameters),
|
||||
/// 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", feature = "boring-tls")))
|
||||
)]
|
||||
Required(TlsParameters),
|
||||
/// 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", feature = "boring-tls")))
|
||||
)]
|
||||
Wrapper(TlsParameters),
|
||||
}
|
||||
|
||||
impl Debug for Tls {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::None => f.pad("None"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
Self::Opportunistic(_) => f.pad("Opportunistic"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
Self::Required(_) => f.pad("Required"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
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
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct TlsParameters {
|
||||
pub(crate) connector: InnerTlsParameters,
|
||||
/// The domain name which is expected in the TLS certificate from the server
|
||||
pub(super) domain: String,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
pub(super) accept_invalid_hostnames: bool,
|
||||
}
|
||||
|
||||
/// Builder for `TlsParameters`
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TlsParametersBuilder {
|
||||
domain: String,
|
||||
cert_store: CertificateStore,
|
||||
root_certs: Vec<Certificate>,
|
||||
accept_invalid_hostnames: bool,
|
||||
accept_invalid_certs: bool,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
min_tls_version: TlsVersion,
|
||||
}
|
||||
|
||||
impl TlsParametersBuilder {
|
||||
@@ -56,12 +150,21 @@ impl TlsParametersBuilder {
|
||||
pub fn new(domain: String) -> Self {
|
||||
Self {
|
||||
domain,
|
||||
cert_store: CertificateStore::Default,
|
||||
root_certs: Vec::new(),
|
||||
accept_invalid_hostnames: 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
|
||||
///
|
||||
/// Can be used to safely connect to a server using a self signed certificate, for example.
|
||||
@@ -83,13 +186,22 @@ impl TlsParametersBuilder {
|
||||
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
||||
///
|
||||
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
#[cfg(any(feature = "native-tls", feature = "boring-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
|
||||
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
|
||||
self.accept_invalid_hostnames = accept_invalid_hostnames;
|
||||
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
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
@@ -111,22 +223,20 @@ impl TlsParametersBuilder {
|
||||
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
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
// TODO: remove below line once native-tls is supported with async-std
|
||||
#[allow(unreachable_code)]
|
||||
#[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 build(self) -> Result<TlsParameters, Error> {
|
||||
// TODO: remove once native-tls is supported with async-std
|
||||
#[cfg(all(feature = "rustls-tls", feature = "async-std1"))]
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
return self.build_rustls();
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
|
||||
return self.build_native();
|
||||
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
return self.build_rustls();
|
||||
#[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
|
||||
return self.build_boring();
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls with the provided configuration
|
||||
@@ -135,17 +245,93 @@ impl TlsParametersBuilder {
|
||||
pub fn build_native(self) -> Result<TlsParameters, Error> {
|
||||
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 {
|
||||
tls_builder.add_root_certificate(cert.native_tls);
|
||||
}
|
||||
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
|
||||
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
|
||||
|
||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
|
||||
let connector = tls_builder.build()?;
|
||||
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)?;
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::NativeTls(connector),
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,47 +339,98 @@ impl TlsParametersBuilder {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||
use webpki_roots::TLS_SERVER_ROOTS;
|
||||
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 mut tls = ClientConfig::new();
|
||||
let tls = ClientConfig::builder_with_protocol_versions(supported_versions);
|
||||
|
||||
let tls = if self.accept_invalid_certs {
|
||||
tls.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
|
||||
} else {
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
|
||||
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
|
||||
let (added, ignored) = store.add_parsable_certificates(native_certs);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(
|
||||
"loaded platform certs with {added} valid and {ignored} ignored (invalid) certs"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
fn load_webpki_roots(store: &mut RootCertStore) {
|
||||
store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
}
|
||||
|
||||
match self.cert_store {
|
||||
CertificateStore::Default => {
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
load_native_roots(&mut root_cert_store)?;
|
||||
#[cfg(not(feature = "rustls-native-certs"))]
|
||||
load_webpki_roots(&mut root_cert_store);
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
CertificateStore::WebpkiRoots => {
|
||||
load_webpki_roots(&mut root_cert_store);
|
||||
}
|
||||
CertificateStore::None => {}
|
||||
}
|
||||
for cert in self.root_certs {
|
||||
for rustls_cert in cert.rustls {
|
||||
tls.root_store
|
||||
.add(&rustls_cert)
|
||||
.map_err(|_| Error::InvalidCertificate)?;
|
||||
root_cert_store.add(rustls_cert).map_err(error::tls)?;
|
||||
}
|
||||
}
|
||||
if self.accept_invalid_certs {
|
||||
tls.dangerous()
|
||||
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {}));
|
||||
}
|
||||
|
||||
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
|
||||
tls.with_root_certificates(root_cert_store)
|
||||
};
|
||||
let tls = tls.with_no_client_auth();
|
||||
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::RustlsTls(tls),
|
||||
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
||||
domain: self.domain,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum InnerTlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsConnector),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(ClientConfig),
|
||||
RustlsTls(Arc<ClientConfig>),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(SslConnector),
|
||||
}
|
||||
|
||||
impl TlsParameters {
|
||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(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", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn new(domain: String) -> Result<Self, Error> {
|
||||
TlsParametersBuilder::new(domain).build()
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` builder
|
||||
pub fn builder(domain: String) -> TlsParametersBuilder {
|
||||
TlsParametersBuilder::new(domain)
|
||||
}
|
||||
@@ -212,6 +449,13 @@ impl TlsParameters {
|
||||
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 {
|
||||
&self.domain
|
||||
}
|
||||
@@ -224,37 +468,47 @@ pub struct Certificate {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls::Certificate,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: Vec<rustls::Certificate>,
|
||||
rustls: Vec<CertificateDer<'static>>,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring::x509::X509,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
impl Certificate {
|
||||
/// Create a `Certificate` from a DER encoded certificate
|
||||
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
|
||||
#[cfg(feature = "native-tls")]
|
||||
let native_tls_cert =
|
||||
native_tls::Certificate::from_der(&der).map_err(|_| Error::InvalidCertificate)?;
|
||||
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 {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: vec![rustls::Certificate(der)],
|
||||
rustls: vec![der.into()],
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring_tls_cert,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a `Certificate` from a PEM encoded certificate
|
||||
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
|
||||
#[cfg(feature = "native-tls")]
|
||||
let native_tls_cert =
|
||||
native_tls::Certificate::from_pem(pem).map_err(|_| Error::InvalidCertificate)?;
|
||||
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")]
|
||||
let rustls_cert = {
|
||||
use rustls::internal::pemfile;
|
||||
use std::io::Cursor;
|
||||
|
||||
let mut pem = Cursor::new(pem);
|
||||
pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)?
|
||||
rustls_pemfile::certs(&mut pem)
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(|_| error::tls("invalid certificates"))?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
@@ -262,22 +516,66 @@ impl Certificate {
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: rustls_cert,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: boring_tls_cert,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Certificate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Certificate").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[derive(Debug)]
|
||||
struct InvalidCertsVerifier;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_roots: &RootCertStore,
|
||||
_presented_certs: &[rustls::Certificate],
|
||||
_dns_name: DNSNameRef<'_>,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
) -> Result<ServerCertVerified, TLSError> {
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, TlsError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||
verify_tls12_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, TlsError> {
|
||||
verify_tls13_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
//! SMTP commands
|
||||
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use crate::{
|
||||
address::Address,
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
error::Error,
|
||||
error::{self, Error},
|
||||
extension::{ClientId, MailParameter, RcptParameter},
|
||||
response::Response,
|
||||
},
|
||||
Address,
|
||||
};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Ehlo {
|
||||
client_id: ClientId,
|
||||
@@ -32,7 +33,7 @@ impl Ehlo {
|
||||
}
|
||||
|
||||
/// STARTTLS command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Starttls;
|
||||
|
||||
@@ -43,7 +44,7 @@ impl Display for Starttls {
|
||||
}
|
||||
|
||||
/// MAIL command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Mail {
|
||||
sender: Option<Address>,
|
||||
@@ -55,10 +56,10 @@ impl Display for Mail {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
|
||||
self.sender.as_ref().map_or("", |s| s.as_ref())
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
write!(f, " {parameter}")?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
@@ -72,7 +73,7 @@ impl Mail {
|
||||
}
|
||||
|
||||
/// RCPT command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rcpt {
|
||||
recipient: Address,
|
||||
@@ -83,7 +84,7 @@ impl Display for Rcpt {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
write!(f, " {parameter}")?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
@@ -100,7 +101,7 @@ impl Rcpt {
|
||||
}
|
||||
|
||||
/// DATA command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Data;
|
||||
|
||||
@@ -111,7 +112,7 @@ impl Display for Data {
|
||||
}
|
||||
|
||||
/// QUIT command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Quit;
|
||||
|
||||
@@ -122,7 +123,7 @@ impl Display for Quit {
|
||||
}
|
||||
|
||||
/// NOOP command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Noop;
|
||||
|
||||
@@ -133,7 +134,7 @@ impl Display for Noop {
|
||||
}
|
||||
|
||||
/// HELP command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Help {
|
||||
argument: Option<String>,
|
||||
@@ -143,7 +144,7 @@ impl Display for Help {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("HELP")?;
|
||||
if let Some(argument) = &self.argument {
|
||||
write!(f, " {}", argument)?;
|
||||
write!(f, " {argument}")?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
@@ -157,7 +158,7 @@ impl Help {
|
||||
}
|
||||
|
||||
/// VRFY command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Vrfy {
|
||||
argument: String,
|
||||
@@ -177,7 +178,7 @@ impl Vrfy {
|
||||
}
|
||||
|
||||
/// EXPN command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Expn {
|
||||
argument: String,
|
||||
@@ -197,7 +198,7 @@ impl Expn {
|
||||
}
|
||||
|
||||
/// RSET command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rset;
|
||||
|
||||
@@ -208,7 +209,7 @@ impl Display for Rset {
|
||||
}
|
||||
|
||||
/// AUTH command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Auth {
|
||||
mechanism: Mechanism,
|
||||
@@ -219,7 +220,7 @@ pub struct Auth {
|
||||
|
||||
impl Display for Auth {
|
||||
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() {
|
||||
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
||||
@@ -261,16 +262,17 @@ impl Auth {
|
||||
response: &Response,
|
||||
) -> Result<Auth, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
return Err(error::response("Expecting a challenge"));
|
||||
}
|
||||
|
||||
let encoded_challenge = response
|
||||
.first_word()
|
||||
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
|
||||
.ok_or_else(|| error::response("Could not read auth challenge"))?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
||||
|
||||
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
|
||||
let decoded_base64 = crate::base64::decode(encoded_challenge).map_err(error::response)?;
|
||||
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
@@ -287,21 +289,22 @@ impl Auth {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::transport::smtp::extension::MailBodyParameter;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
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 mail_parameter = MailParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
keyword: "TEST".to_owned(),
|
||||
value: Some("value".to_owned()),
|
||||
};
|
||||
let rcpt_parameter = RcptParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
keyword: "TEST".to_owned(),
|
||||
value: Some("value".to_owned()),
|
||||
};
|
||||
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(
|
||||
@@ -338,24 +341,18 @@ mod test {
|
||||
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Quit), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", Data), "DATA\r\n");
|
||||
assert_eq!(format!("{}", Noop), "NOOP\r\n");
|
||||
assert_eq!(format!("{Quit}"), "QUIT\r\n");
|
||||
assert_eq!(format!("{Data}"), "DATA\r\n");
|
||||
assert_eq!(format!("{Noop}"), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", Help::new(Some("test".to_string()))),
|
||||
format!("{}", Help::new(Some("test".to_owned()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Vrfy::new("test".to_string())),
|
||||
"VRFY test\r\n"
|
||||
);
|
||||
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!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
|
||||
assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\r\n");
|
||||
assert_eq!(format!("{Rset}"), "RSET\r\n");
|
||||
let credentials = Credentials::new("user".to_owned(), "password".to_owned());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
|
||||
129
src/transport/smtp/connection_url.rs
Normal file
129
src/transport/smtp/connection_url.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use url::Url;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
use super::client::{Tls, TlsParameters};
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use super::AsyncSmtpTransportBuilder;
|
||||
use super::{
|
||||
authentication::Credentials, error, extension::ClientId, Error, SmtpTransportBuilder,
|
||||
SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT,
|
||||
};
|
||||
|
||||
pub(crate) trait TransportBuilder {
|
||||
fn new<T: Into<String>>(server: T) -> Self;
|
||||
fn tls(self, tls: super::Tls) -> Self;
|
||||
fn port(self, port: u16) -> Self;
|
||||
fn credentials(self, credentials: Credentials) -> Self;
|
||||
fn hello_name(self, name: ClientId) -> Self;
|
||||
}
|
||||
|
||||
impl TransportBuilder for SmtpTransportBuilder {
|
||||
fn new<T: Into<String>>(server: T) -> Self {
|
||||
Self::new(server)
|
||||
}
|
||||
|
||||
fn tls(self, tls: super::Tls) -> Self {
|
||||
self.tls(tls)
|
||||
}
|
||||
|
||||
fn port(self, port: u16) -> Self {
|
||||
self.port(port)
|
||||
}
|
||||
|
||||
fn credentials(self, credentials: Credentials) -> Self {
|
||||
self.credentials(credentials)
|
||||
}
|
||||
|
||||
fn hello_name(self, name: ClientId) -> Self {
|
||||
self.hello_name(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
impl TransportBuilder for AsyncSmtpTransportBuilder {
|
||||
fn new<T: Into<String>>(server: T) -> Self {
|
||||
Self::new(server)
|
||||
}
|
||||
|
||||
fn tls(self, tls: super::Tls) -> Self {
|
||||
self.tls(tls)
|
||||
}
|
||||
|
||||
fn port(self, port: u16) -> Self {
|
||||
self.port(port)
|
||||
}
|
||||
|
||||
fn credentials(self, credentials: Credentials) -> Self {
|
||||
self.credentials(credentials)
|
||||
}
|
||||
|
||||
fn hello_name(self, name: ClientId) -> Self {
|
||||
self.hello_name(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new SmtpTransportBuilder or AsyncSmtpTransportBuilder from a connection URL
|
||||
pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) -> Result<B, Error> {
|
||||
let connection_url = Url::parse(connection_url).map_err(error::connection)?;
|
||||
let tls: Option<String> = connection_url
|
||||
.query_pairs()
|
||||
.find(|(k, _)| k == "tls")
|
||||
.map(|(_, v)| v.to_string());
|
||||
|
||||
let host = connection_url
|
||||
.host_str()
|
||||
.ok_or_else(|| error::connection("smtp host undefined"))?;
|
||||
|
||||
let mut builder = B::new(host);
|
||||
|
||||
match (connection_url.scheme(), tls.as_deref()) {
|
||||
("smtp", None) => {
|
||||
builder = builder.port(connection_url.port().unwrap_or(SMTP_PORT));
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
("smtp", Some("required")) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
|
||||
.tls(Tls::Required(TlsParameters::new(host.into())?))
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
("smtp", Some("opportunistic")) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
|
||||
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?))
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
("smtps", _) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSIONS_PORT))
|
||||
.tls(Tls::Wrapper(TlsParameters::new(host.into())?))
|
||||
}
|
||||
(scheme, tls) => {
|
||||
return Err(error::connection(format!(
|
||||
"Unknown scheme '{scheme}' or tls parameter '{tls:?}', note that a transport with TLS requires one of the TLS features"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// use the path segment of the URL as name in the name in the HELO / EHLO command
|
||||
if connection_url.path().len() > 1 {
|
||||
let name = connection_url.path().trim_matches('/').to_owned();
|
||||
builder = builder.hello_name(ClientId::Domain(name));
|
||||
}
|
||||
|
||||
if let Some(password) = connection_url.password() {
|
||||
let percent_decode = |s: &str| {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8()
|
||||
.map(|cow| cow.into_owned())
|
||||
.map_err(error::connection)
|
||||
};
|
||||
let credentials = Credentials::new(
|
||||
percent_decode(connection_url.username())?,
|
||||
percent_decode(password)?,
|
||||
);
|
||||
builder = builder.credentials(credentials);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
@@ -1,162 +1,191 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use self::Error::*;
|
||||
use crate::transport::smtp::response::{Response, Severity};
|
||||
use base64::DecodeError;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
use crate::{
|
||||
transport::smtp::response::{Code, Severity},
|
||||
BoxError,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Transient(Response),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Permanent(Response),
|
||||
/// Error parsing a response
|
||||
ResponseParsing(&'static str),
|
||||
/// Error parsing a base64 string in response
|
||||
ChallengeParsing(DecodeError),
|
||||
/// Error parsing UTF8 in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// DNS resolution error
|
||||
Resolution,
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// TLS error
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
Tls(native_tls::Error),
|
||||
/// Parsing error
|
||||
Parsing(nom::error::ErrorKind),
|
||||
/// Invalid hostname
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
InvalidDNSName(webpki::InvalidDNSNameError),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
InvalidCertificate,
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
Pool(r2d2::Error),
|
||||
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
|
||||
|
||||
/// The Errors that may occur when sending an email over SMTP
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
// Try to display the first line of the server's response that usually
|
||||
// contains a short humanly readable error message
|
||||
Transient(ref err) => fmt.write_str(
|
||||
err.first_line()
|
||||
.unwrap_or("transient error during SMTP transaction"),
|
||||
),
|
||||
Permanent(ref err) => fmt.write_str(
|
||||
err.first_line()
|
||||
.unwrap_or("permanent error during SMTP transaction"),
|
||||
),
|
||||
ResponseParsing(err) => fmt.write_str(err),
|
||||
ChallengeParsing(ref err) => err.fmt(fmt),
|
||||
Utf8Parsing(ref err) => err.fmt(fmt),
|
||||
Resolution => fmt.write_str("could not resolve hostname"),
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(ref err) => err.fmt(fmt),
|
||||
Parsing(ref err) => fmt.write_str(err.description()),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InvalidDNSName(ref err) => err.fmt(fmt),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
InvalidCertificate => fmt.write_str("invalid certificate"),
|
||||
#[cfg(feature = "r2d2")]
|
||||
Pool(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
ChallengeParsing(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
Io(ref err) => Some(&*err),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(ref err) => Some(&*err),
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the error is from response
|
||||
pub fn is_response(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Response)
|
||||
}
|
||||
|
||||
/// Returns true if the error is from client
|
||||
pub fn is_client(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Client)
|
||||
}
|
||||
|
||||
/// Returns true if the error is a transient SMTP error
|
||||
pub fn is_transient(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Transient(_))
|
||||
}
|
||||
|
||||
/// Returns true if the error is a permanent SMTP error
|
||||
pub fn is_permanent(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Permanent(_))
|
||||
}
|
||||
|
||||
/// Returns true if the error is caused by a timeout
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
let mut source = self.source();
|
||||
|
||||
while let Some(err) = source {
|
||||
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
|
||||
return io_err.kind() == std::io::ErrorKind::TimedOut;
|
||||
}
|
||||
|
||||
source = err.source();
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if the error is from TLS
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn is_tls(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Tls)
|
||||
}
|
||||
|
||||
/// Returns the status code, if the error was generated from a response.
|
||||
pub fn status(&self) -> Option<Code> {
|
||||
match self.inner.kind {
|
||||
Kind::Transient(code) | Kind::Permanent(code) => Some(code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Transient(Code),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Permanent(Code),
|
||||
/// Error parsing a response
|
||||
Response,
|
||||
/// Internal client error
|
||||
Client,
|
||||
/// Connection error
|
||||
Connection,
|
||||
/// Underlying network i/o error
|
||||
Network,
|
||||
/// TLS error
|
||||
#[cfg_attr(
|
||||
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,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::smtp::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
impl From<native_tls::Error> for Error {
|
||||
fn from(err: native_tls::Error) -> Error {
|
||||
Tls(err)
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.inner.kind {
|
||||
Kind::Response => f.write_str("response error")?,
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
Kind::Network => f.write_str("network error")?,
|
||||
Kind::Connection => f.write_str("Connection error")?,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
Kind::Tls => f.write_str("tls error")?,
|
||||
Kind::Transient(code) => {
|
||||
write!(f, "transient error ({code})")?;
|
||||
}
|
||||
Kind::Permanent(code) => {
|
||||
write!(f, "permanent error ({code})")?;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<nom::error::Error<&str>>> for Error {
|
||||
fn from(err: nom::Err<nom::error::Error<&str>>) -> Error {
|
||||
Parsing(match err {
|
||||
nom::Err::Incomplete(_) => nom::error::ErrorKind::Complete,
|
||||
nom::Err::Failure(e) => e.code,
|
||||
nom::Err::Error(e) => e.code,
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for Error {
|
||||
fn from(err: DecodeError) -> Error {
|
||||
ChallengeParsing(err)
|
||||
pub(crate) fn code(c: Code, s: Option<String>) -> Error {
|
||||
match c.severity {
|
||||
Severity::TransientNegativeCompletion => Error::new(Kind::Transient(c), s),
|
||||
Severity::PermanentNegativeCompletion => Error::new(Kind::Permanent(c), s),
|
||||
_ => client("Unknown error code"),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Response, Some(e))
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
impl From<webpki::InvalidDNSNameError> for Error {
|
||||
fn from(err: webpki::InvalidDNSNameError) -> Error {
|
||||
InvalidDNSName(err)
|
||||
}
|
||||
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Client, Some(e))
|
||||
}
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
impl From<r2d2::Error> for Error {
|
||||
fn from(err: r2d2::Error) -> Error {
|
||||
Pool(err)
|
||||
}
|
||||
pub(crate) fn network<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Network, Some(e))
|
||||
}
|
||||
|
||||
impl From<Response> for Error {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.code.severity {
|
||||
Severity::TransientNegativeCompletion => Transient(response),
|
||||
Severity::PermanentNegativeCompletion => Permanent(response),
|
||||
_ => Client("Unknown error code"),
|
||||
}
|
||||
}
|
||||
pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Connection, Some(e))
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Client(string)
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Tls, Some(e))
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
//! ESMTP features
|
||||
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism, error::Error, response::Response, util::XText,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{self, Display, Formatter},
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
result::Result,
|
||||
};
|
||||
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism,
|
||||
error::{self, Error},
|
||||
response::Response,
|
||||
util::XText,
|
||||
};
|
||||
|
||||
/// Client identifier, the parameter to `EHLO`
|
||||
@@ -49,15 +52,16 @@ impl Default for ClientId {
|
||||
|
||||
impl Display for ClientId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Domain(ref value) => f.write_str(value),
|
||||
Self::Ipv4(ref value) => write!(f, "[{}]", value),
|
||||
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value),
|
||||
match self {
|
||||
Self::Domain(value) => f.write_str(value),
|
||||
Self::Ipv4(value) => write!(f, "[{value}]"),
|
||||
Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientId {
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
|
||||
/// Creates a new `ClientId` from a fully qualified domain name
|
||||
pub fn new(domain: String) -> Self {
|
||||
@@ -68,6 +72,7 @@ impl ClientId {
|
||||
/// Supported ESMTP keywords
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum Extension {
|
||||
/// 8BITMIME keyword
|
||||
///
|
||||
@@ -87,11 +92,11 @@ pub enum Extension {
|
||||
|
||||
impl Display for Extension {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
match self {
|
||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
Extension::StartTls => f.write_str("STARTTLS"),
|
||||
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
|
||||
Extension::Authentication(mechanism) => write!(f, "AUTH {mechanism}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,17 +108,17 @@ pub struct ServerInfo {
|
||||
/// Server name
|
||||
///
|
||||
/// The name given in the server banner
|
||||
pub name: String,
|
||||
name: String,
|
||||
/// ESMTP features supported by the server
|
||||
///
|
||||
/// It contains the features supported by the server and known by the `Extension` module.
|
||||
pub features: HashSet<Extension>,
|
||||
features: HashSet<Extension>,
|
||||
}
|
||||
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let features = if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
"no supported features".to_owned()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
};
|
||||
@@ -126,12 +131,12 @@ impl ServerInfo {
|
||||
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||
let name = match response.first_word() {
|
||||
Some(name) => name,
|
||||
None => return Err(Error::ResponseParsing("Could not read server name")),
|
||||
None => return Err(error::response("Could not read server name")),
|
||||
};
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
|
||||
for line in response.message.as_slice() {
|
||||
for line in response.message() {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -168,7 +173,7 @@ impl ServerInfo {
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
name: name.to_string(),
|
||||
name: name.to_owned(),
|
||||
features,
|
||||
})
|
||||
}
|
||||
@@ -184,7 +189,7 @@ impl ServerInfo {
|
||||
.contains(&Extension::Authentication(mechanism))
|
||||
}
|
||||
|
||||
/// Gets a compatible mechanism from list
|
||||
/// Gets a compatible mechanism from a list
|
||||
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
|
||||
for mechanism in mechanisms {
|
||||
if self.supports_auth_mechanism(*mechanism) {
|
||||
@@ -193,6 +198,11 @@ impl ServerInfo {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The name given in the server banner
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A `MAIL FROM` extension parameter
|
||||
@@ -216,16 +226,16 @@ pub enum MailParameter {
|
||||
|
||||
impl Display for MailParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
|
||||
MailParameter::Size(size) => write!(f, "SIZE={}", size),
|
||||
match self {
|
||||
MailParameter::Body(value) => write!(f, "BODY={value}"),
|
||||
MailParameter::Size(size) => write!(f, "SIZE={size}"),
|
||||
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
MailParameter::Other {
|
||||
ref keyword,
|
||||
value: Some(ref value),
|
||||
keyword,
|
||||
value: Some(value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
MailParameter::Other {
|
||||
ref keyword,
|
||||
keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
}
|
||||
@@ -266,13 +276,13 @@ pub enum RcptParameter {
|
||||
|
||||
impl Display for RcptParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
match &self {
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
value: Some(ref value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
keyword,
|
||||
value: Some(value),
|
||||
} => write!(f, "{keyword}={}", XText(value)),
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
}
|
||||
@@ -281,32 +291,27 @@ impl Display for RcptParameter {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism,
|
||||
response::{Category, Code, Detail, Response, Severity},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use crate::transport::smtp::response::{Category, Code, Detail, Severity};
|
||||
|
||||
#[test]
|
||||
fn test_clientid_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", ClientId::Domain("test".to_string())),
|
||||
"test".to_string()
|
||||
format!("{}", ClientId::Domain("test".to_owned())),
|
||||
"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]
|
||||
fn test_extension_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string()
|
||||
"8BITMIME".to_owned()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string()
|
||||
"AUTH PLAIN".to_owned()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -319,11 +324,11 @@ mod test {
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
name: "name".to_owned(),
|
||||
features: eightbitmime,
|
||||
}
|
||||
),
|
||||
"name with {EightBitMime}".to_string()
|
||||
"name with {EightBitMime}".to_owned()
|
||||
);
|
||||
|
||||
let empty = HashSet::new();
|
||||
@@ -332,11 +337,11 @@ mod test {
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
name: "name".to_owned(),
|
||||
features: empty,
|
||||
}
|
||||
),
|
||||
"name with no supported features".to_string()
|
||||
"name with no supported features".to_owned()
|
||||
);
|
||||
|
||||
let mut plain = HashSet::new();
|
||||
@@ -346,11 +351,11 @@ mod test {
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
name: "name".to_owned(),
|
||||
features: plain,
|
||||
}
|
||||
),
|
||||
"name with {Authentication(Plain)}".to_string()
|
||||
"name with {Authentication(Plain)}".to_owned()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -362,18 +367,14 @@ mod test {
|
||||
Category::Unspecified4,
|
||||
Detail::One,
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
|
||||
);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
assert!(features.insert(Extension::EightBitMime));
|
||||
|
||||
let server_info = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
name: "me".to_owned(),
|
||||
features,
|
||||
};
|
||||
|
||||
@@ -389,10 +390,10 @@ mod test {
|
||||
Detail::One,
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"me".to_owned(),
|
||||
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -402,7 +403,7 @@ mod test {
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
|
||||
|
||||
let server_info2 = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
name: "me".to_owned(),
|
||||
features: features2,
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{Message, Transport, SmtpTransport};
|
||||
//! use lettre::{Message, SmtpTransport, Transport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
@@ -43,8 +43,7 @@
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Create TLS transport on port 465
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! .build();
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?.build();
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
@@ -59,7 +58,13 @@
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::{PoolConfig, authentication::{Credentials, Mechanism}}};
|
||||
//! use lettre::{
|
||||
//! transport::smtp::{
|
||||
//! authentication::{Credentials, Mechanism},
|
||||
//! PoolConfig,
|
||||
//! },
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
@@ -71,11 +76,14 @@
|
||||
//! // Create TLS transport on port 587 with STARTTLS
|
||||
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials(Credentials::new("username".to_string(), "password".to_string()))
|
||||
//! .credentials(Credentials::new(
|
||||
//! "username".to_owned(),
|
||||
//! "password".to_owned(),
|
||||
//! ))
|
||||
//! // Configure expected authentication mechanism
|
||||
//! .authentication(vec![Mechanism::Plain])
|
||||
//! // Connection pool settings
|
||||
//! .pool_config( PoolConfig::new().max_size(20))
|
||||
//! .pool_config(PoolConfig::new().max_size(20))
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
@@ -90,7 +98,10 @@
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::client::{TlsParameters, Tls}};
|
||||
//! use lettre::{
|
||||
//! transport::smtp::client::{Tls, TlsParameters},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
@@ -100,8 +111,9 @@
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Custom TLS configuration
|
||||
//! let tls = TlsParameters::builder("smtp.example.com".to_string())
|
||||
//! .dangerous_accept_invalid_certs(true).build()?;
|
||||
//! let tls = TlsParameters::builder("smtp.example.com".to_owned())
|
||||
//! .dangerous_accept_invalid_certs(true)
|
||||
//! .build()?;
|
||||
//!
|
||||
//! // Create TLS transport on port 465
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
@@ -116,19 +128,19 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_transport::{
|
||||
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder,
|
||||
};
|
||||
#[cfg(feature = "r2d2")]
|
||||
use std::time::Duration;
|
||||
|
||||
use client::Tls;
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
|
||||
#[cfg(feature = "pool")]
|
||||
pub use self::pool::PoolConfig;
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub(crate) use self::transport::SmtpClient;
|
||||
pub use self::{
|
||||
error::Error,
|
||||
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::{
|
||||
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||
@@ -136,34 +148,20 @@ use crate::transport::smtp::{
|
||||
extension::ClientId,
|
||||
response::Response,
|
||||
};
|
||||
use client::Tls;
|
||||
use std::time::Duration;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub use self::async_transport::AsyncStd1Connector;
|
||||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub use self::async_transport::Tokio02Connector;
|
||||
#[doc(hidden)]
|
||||
#[allow(deprecated)]
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::async_transport::Tokio1Connector;
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_transport;
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
mod connection_url;
|
||||
mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg(feature = "pool")]
|
||||
mod pool;
|
||||
pub mod response;
|
||||
mod transport;
|
||||
pub mod util;
|
||||
pub(super) mod util;
|
||||
|
||||
// Registered port numbers:
|
||||
// https://www.iana.
|
||||
@@ -179,10 +177,9 @@ pub const SUBMISSION_PORT: u16 = 587;
|
||||
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
/// Default timeout
|
||||
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct SmtpInfo {
|
||||
/// Name sent during EHLO
|
||||
hello_name: ClientId,
|
||||
@@ -204,7 +201,7 @@ struct SmtpInfo {
|
||||
impl Default for SmtpInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
server: "localhost".to_string(),
|
||||
server: "localhost".to_owned(),
|
||||
port: SMTP_PORT,
|
||||
hello_name: ClientId::default(),
|
||||
credentials: None,
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
|
||||
|
||||
use r2d2::{ManageConnection, Pool};
|
||||
|
||||
/// Configuration for a connection pool
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
pub struct PoolConfig {
|
||||
min_idle: u32,
|
||||
max_size: u32,
|
||||
connection_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl PoolConfig {
|
||||
/// Create a new pool configuration with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Minimum number of idle connections
|
||||
///
|
||||
/// Defaults to `0`
|
||||
pub fn min_idle(mut self, min_idle: u32) -> Self {
|
||||
self.min_idle = min_idle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of pooled connections
|
||||
///
|
||||
/// Defaults to `10`
|
||||
pub fn max_size(mut self, max_size: u32) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection timeout
|
||||
///
|
||||
/// Defaults to `30 seconds`
|
||||
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
|
||||
self.connection_timeout = connection_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection idle timeout
|
||||
///
|
||||
/// Defaults to `60 seconds`
|
||||
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
|
||||
self.idle_timeout = idle_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build<C: ManageConnection>(&self, client: C) -> Pool<C> {
|
||||
Pool::builder()
|
||||
.min_idle(Some(self.min_idle))
|
||||
.max_size(self.max_size)
|
||||
.connection_timeout(self.connection_timeout)
|
||||
.idle_timeout(Some(self.idle_timeout))
|
||||
.build_unchecked(client)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_idle: 0,
|
||||
max_size: 10,
|
||||
connection_timeout: Duration::from_secs(30),
|
||||
idle_timeout: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageConnection for SmtpClient {
|
||||
type Connection = SmtpConnection;
|
||||
type Error = Error;
|
||||
|
||||
fn connect(&self) -> Result<Self::Connection, Error> {
|
||||
self.connection()
|
||||
}
|
||||
|
||||
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
||||
if conn.test_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::Client("is not connected anymore"))
|
||||
}
|
||||
|
||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||
conn.has_broken()
|
||||
}
|
||||
}
|
||||
300
src/transport/smtp/pool/async_impl.rs
Normal file
300
src/transport/smtp/pool/async_impl.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use futures_util::{
|
||||
lock::Mutex,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{client::AsyncSmtpConnection, Error},
|
||||
PoolConfig,
|
||||
};
|
||||
use crate::{executor::SpawnHandle, transport::smtp::async_transport::AsyncSmtpClient, Executor};
|
||||
|
||||
pub struct Pool<E: Executor> {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Vec<ParkedConnection>>,
|
||||
client: AsyncSmtpClient<E>,
|
||||
handle: OnceLock<E::Handle>,
|
||||
}
|
||||
|
||||
struct ParkedConnection {
|
||||
conn: AsyncSmtpConnection,
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
pub struct PooledConnection<E: Executor> {
|
||||
conn: Option<AsyncSmtpConnection>,
|
||||
pool: Arc<Pool<E>>,
|
||||
}
|
||||
|
||||
impl<E: Executor> Pool<E> {
|
||||
pub fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
|
||||
let pool = Arc::new(Self {
|
||||
config,
|
||||
connections: Mutex::new(Vec::new()),
|
||||
client,
|
||||
handle: OnceLock::new(),
|
||||
});
|
||||
|
||||
{
|
||||
let pool_ = Arc::clone(&pool);
|
||||
|
||||
let min_idle = pool_.config.min_idle;
|
||||
let idle_timeout = pool_.config.idle_timeout;
|
||||
let pool = Arc::downgrade(&pool_);
|
||||
|
||||
let handle = E::spawn(async move {
|
||||
loop {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("running cleanup tasks");
|
||||
|
||||
match pool.upgrade() {
|
||||
Some(pool) => {
|
||||
#[allow(clippy::needless_collect)]
|
||||
let (count, dropped) = {
|
||||
let mut connections = pool.connections.lock().await;
|
||||
|
||||
let to_drop = connections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
let dropped = to_drop
|
||||
.into_iter()
|
||||
.map(|i| connections.remove(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(connections.len(), dropped)
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mut created = 0;
|
||||
for _ in count..=(min_idle as usize) {
|
||||
let conn = match pool.client.connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("couldn't create idle connection {}", err);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let _ = err;
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut connections = pool.connections.lock().await;
|
||||
connections.push(ParkedConnection::park(conn));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
if created > 0 {
|
||||
tracing::debug!("created {} idle connections", created);
|
||||
}
|
||||
|
||||
if !dropped.is_empty() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropped {} idle connections", dropped.len());
|
||||
|
||||
abort_concurrent(dropped.into_iter().map(|conn| conn.unpark()))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!(
|
||||
"breaking out of task - no more references to Pool are available"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
E::sleep(idle_timeout).await;
|
||||
}
|
||||
});
|
||||
pool_
|
||||
.handle
|
||||
.set(handle)
|
||||
.expect("handle hasn't been set yet");
|
||||
}
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
|
||||
loop {
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock().await;
|
||||
connections.pop()
|
||||
};
|
||||
|
||||
match conn {
|
||||
Some(conn) => {
|
||||
let mut conn = conn.unpark();
|
||||
|
||||
// TODO: handle the client try another connection if this one isn't good
|
||||
if !conn.test_connected().await {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection");
|
||||
|
||||
conn.abort().await;
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("reusing a pooled connection");
|
||||
|
||||
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("creating a new connection");
|
||||
|
||||
let conn = self.client.connection().await?;
|
||||
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recycle(&self, mut conn: AsyncSmtpConnection) {
|
||||
if conn.has_broken() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection instead of recycling it");
|
||||
|
||||
conn.abort().await;
|
||||
drop(conn);
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("recycling connection");
|
||||
|
||||
let mut connections = self.connections.lock().await;
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections);
|
||||
conn.abort().await;
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Debug for Pool<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Pool")
|
||||
.field("config", &self.config)
|
||||
.field(
|
||||
"connections",
|
||||
&match self.connections.try_lock() {
|
||||
Some(connections) => format!("{} connections", connections.len()),
|
||||
|
||||
None => "LOCKED".to_owned(),
|
||||
},
|
||||
)
|
||||
.field("client", &self.client)
|
||||
.field(
|
||||
"handle",
|
||||
&match self.handle.get() {
|
||||
Some(_) => "Some(JoinHandle)",
|
||||
None => "None",
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Drop for Pool<E> {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping Pool");
|
||||
|
||||
let connections = mem::take(self.connections.get_mut());
|
||||
let handle = self.handle.take();
|
||||
E::spawn(async move {
|
||||
if let Some(handle) = handle {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
|
||||
abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ParkedConnection {
|
||||
fn park(conn: AsyncSmtpConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
since: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_duration(&self) -> Duration {
|
||||
self.since.elapsed()
|
||||
}
|
||||
|
||||
fn unpark(self) -> AsyncSmtpConnection {
|
||||
self.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> PooledConnection<E> {
|
||||
fn wrap(conn: AsyncSmtpConnection, pool: Arc<Pool<E>>) -> Self {
|
||||
Self {
|
||||
conn: Some(conn),
|
||||
pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Deref for PooledConnection<E> {
|
||||
type Target = AsyncSmtpConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.conn.as_ref().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> DerefMut for PooledConnection<E> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.conn.as_mut().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Drop for PooledConnection<E> {
|
||||
fn drop(&mut self) {
|
||||
let conn = self
|
||||
.conn
|
||||
.take()
|
||||
.expect("AsyncSmtpConnection hasn't been taken yet");
|
||||
let pool = Arc::clone(&self.pool);
|
||||
|
||||
E::spawn(async move {
|
||||
pool.recycle(conn).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn abort_concurrent<I>(iter: I)
|
||||
where
|
||||
I: Iterator<Item = AsyncSmtpConnection>,
|
||||
{
|
||||
stream::iter(iter)
|
||||
.for_each_concurrent(8, |mut conn| async move {
|
||||
conn.abort().await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
66
src/transport/smtp/pool/mod.rs
Normal file
66
src/transport/smtp/pool/mod.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub mod async_impl;
|
||||
pub mod sync_impl;
|
||||
|
||||
/// Configuration for a connection pool
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub struct PoolConfig {
|
||||
min_idle: u32,
|
||||
max_size: u32,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl PoolConfig {
|
||||
/// Create a new pool configuration with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Minimum number of idle connections
|
||||
///
|
||||
/// Defaults to `0`
|
||||
pub fn min_idle(mut self, min_idle: u32) -> Self {
|
||||
self.min_idle = min_idle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of pooled connections
|
||||
///
|
||||
/// Defaults to `10`
|
||||
pub fn max_size(mut self, max_size: u32) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection timeout
|
||||
///
|
||||
/// Defaults to `30 seconds`
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "The Connection timeout is already configured on the SMTP transport")]
|
||||
pub fn connection_timeout(self, connection_timeout: Duration) -> Self {
|
||||
let _ = connection_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection idle timeout
|
||||
///
|
||||
/// Defaults to `60 seconds`
|
||||
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
|
||||
self.idle_timeout = idle_timeout;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_idle: 0,
|
||||
max_size: 10,
|
||||
idle_timeout: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
259
src/transport/smtp/pool/sync_impl.rs
Normal file
259
src/transport/smtp/pool/sync_impl.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, Mutex, TryLockError},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{client::SmtpConnection, Error},
|
||||
PoolConfig,
|
||||
};
|
||||
use crate::transport::smtp::transport::SmtpClient;
|
||||
|
||||
pub struct Pool {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Vec<ParkedConnection>>,
|
||||
client: SmtpClient,
|
||||
}
|
||||
|
||||
struct ParkedConnection {
|
||||
conn: SmtpConnection,
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
pub struct PooledConnection {
|
||||
conn: Option<SmtpConnection>,
|
||||
pool: Arc<Pool>,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
pub fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
|
||||
let pool = Arc::new(Self {
|
||||
config,
|
||||
connections: Mutex::new(Vec::new()),
|
||||
client,
|
||||
});
|
||||
|
||||
{
|
||||
let pool_ = Arc::clone(&pool);
|
||||
|
||||
let min_idle = pool_.config.min_idle;
|
||||
let idle_timeout = pool_.config.idle_timeout;
|
||||
let pool = Arc::downgrade(&pool_);
|
||||
|
||||
thread::Builder::new()
|
||||
.name("lettre-connection-pool".into())
|
||||
.spawn(move || {
|
||||
while let Some(pool) = pool.upgrade() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("running cleanup tasks");
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
let (count, dropped) = {
|
||||
let mut connections = pool.connections.lock().unwrap();
|
||||
|
||||
let to_drop = connections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
let dropped = to_drop
|
||||
.into_iter()
|
||||
.map(|i| connections.remove(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(connections.len(), dropped)
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mut created = 0;
|
||||
for _ in count..=(min_idle as usize) {
|
||||
let conn = match pool.client.connection() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("couldn't create idle connection {}", err);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let _ = err;
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut connections = pool.connections.lock().unwrap();
|
||||
connections.push(ParkedConnection::park(conn));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
if created > 0 {
|
||||
tracing::debug!("created {} idle connections", created);
|
||||
}
|
||||
|
||||
if !dropped.is_empty() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropped {} idle connections", dropped.len());
|
||||
|
||||
for conn in dropped {
|
||||
let mut conn = conn.unpark();
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(idle_timeout);
|
||||
}
|
||||
})
|
||||
.expect("couldn't spawn the Pool thread");
|
||||
}
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
|
||||
loop {
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock().unwrap();
|
||||
connections.pop()
|
||||
};
|
||||
|
||||
match conn {
|
||||
Some(conn) => {
|
||||
let mut conn = conn.unpark();
|
||||
|
||||
// TODO: handle the client try another connection if this one isn't good
|
||||
if !conn.test_connected() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection");
|
||||
|
||||
conn.abort();
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("reusing a pooled connection");
|
||||
|
||||
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("creating a new connection");
|
||||
|
||||
let conn = self.client.connection()?;
|
||||
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recycle(&self, mut conn: SmtpConnection) {
|
||||
if conn.has_broken() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection instead of recycling it");
|
||||
|
||||
conn.abort();
|
||||
drop(conn);
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("recycling connection");
|
||||
|
||||
let mut connections = self.connections.lock().unwrap();
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections);
|
||||
conn.abort();
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Pool {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Pool")
|
||||
.field("config", &self.config)
|
||||
.field(
|
||||
"connections",
|
||||
&match self.connections.try_lock() {
|
||||
Ok(connections) => format!("{} connections", connections.len()),
|
||||
|
||||
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
|
||||
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
|
||||
},
|
||||
)
|
||||
.field("client", &self.client)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pool {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping Pool");
|
||||
|
||||
let connections = mem::take(&mut *self.connections.get_mut().unwrap());
|
||||
for conn in connections {
|
||||
let mut conn = conn.unpark();
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParkedConnection {
|
||||
fn park(conn: SmtpConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
since: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_duration(&self) -> Duration {
|
||||
self.since.elapsed()
|
||||
}
|
||||
|
||||
fn unpark(self) -> SmtpConnection {
|
||||
self.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl PooledConnection {
|
||||
fn wrap(conn: SmtpConnection, pool: Arc<Pool>) -> Self {
|
||||
Self {
|
||||
conn: Some(conn),
|
||||
pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PooledConnection {
|
||||
type Target = SmtpConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.conn.as_ref().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for PooledConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.conn.as_mut().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PooledConnection {
|
||||
fn drop(&mut self) {
|
||||
let conn = self
|
||||
.conn
|
||||
.take()
|
||||
.expect("SmtpConnection hasn't been taken yet");
|
||||
self.pool.recycle(conn);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text
|
||||
//! message
|
||||
|
||||
use crate::transport::smtp::Error;
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
result,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::streaming::{tag, take_until},
|
||||
@@ -10,14 +15,10 @@ use nom::{
|
||||
sequence::{preceded, tuple},
|
||||
IResult,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
result,
|
||||
str::FromStr,
|
||||
string::ToString,
|
||||
};
|
||||
|
||||
/// First digit indicates severity
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// The first digit indicates severity
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Severity {
|
||||
@@ -120,6 +121,20 @@ impl Code {
|
||||
detail,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells if the response is positive
|
||||
pub fn is_positive(self) -> bool {
|
||||
matches!(
|
||||
self.severity,
|
||||
Severity::PositiveCompletion | Severity::PositiveIntermediate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Code> for u16 {
|
||||
fn from(code: Code) -> Self {
|
||||
code.detail as u16 + 10 * code.category as u16 + 100 * code.severity as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an SMTP reply, with separated code and message
|
||||
@@ -129,17 +144,19 @@ impl Code {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Response {
|
||||
/// Response code
|
||||
pub code: Code,
|
||||
code: Code,
|
||||
/// Server response string (optional)
|
||||
/// Handle multiline responses
|
||||
pub message: Vec<String>,
|
||||
message: Vec<String>,
|
||||
}
|
||||
|
||||
impl FromStr for Response {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> result::Result<Response, Error> {
|
||||
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
|
||||
parse_response(s)
|
||||
.map(|(_, r)| r)
|
||||
.map_err(|e| error::response(e.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +168,7 @@ impl Response {
|
||||
|
||||
/// Tells if the response is positive
|
||||
pub fn is_positive(&self) -> bool {
|
||||
matches!(
|
||||
self.code.severity,
|
||||
Severity::PositiveCompletion | Severity::PositiveIntermediate
|
||||
)
|
||||
self.code.is_positive()
|
||||
}
|
||||
|
||||
/// Tests code equality
|
||||
@@ -173,6 +187,16 @@ impl Response {
|
||||
pub fn first_line(&self) -> Option<&str> {
|
||||
self.message.first().map(String::as_str)
|
||||
}
|
||||
|
||||
/// Response code
|
||||
pub fn code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
|
||||
/// Server response string (array of lines)
|
||||
pub fn message(&self) -> impl Iterator<Item = &str> {
|
||||
self.message.iter().map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
// Parsers (originally from tokio-smtp)
|
||||
@@ -298,6 +322,17 @@ mod test {
|
||||
assert_eq!(code.to_string(), "421");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_to_u16() {
|
||||
let code = Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: Detail::One,
|
||||
};
|
||||
let c: u16 = code.into();
|
||||
assert_eq!(c, 421);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_from_str() {
|
||||
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
|
||||
@@ -310,10 +345,10 @@ mod test {
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
message: vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5".to_string(),
|
||||
"me".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
"AUTH PLAIN CRAM-MD5".to_owned(),
|
||||
],
|
||||
}
|
||||
);
|
||||
@@ -333,11 +368,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.is_positive());
|
||||
assert!(!Response::new(
|
||||
@@ -346,11 +377,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.is_positive());
|
||||
}
|
||||
@@ -363,11 +390,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.has_code(451));
|
||||
assert!(!Response::new(
|
||||
@@ -376,11 +399,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.has_code(251));
|
||||
}
|
||||
@@ -394,11 +413,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.first_word(),
|
||||
Some("me")
|
||||
@@ -411,9 +426,9 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"me mo".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
],
|
||||
)
|
||||
.first_word(),
|
||||
@@ -438,7 +453,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
vec![" ".to_owned()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
@@ -450,7 +465,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
vec![" ".to_owned()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
@@ -462,7 +477,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
vec!["".to_owned()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
@@ -475,7 +490,7 @@ mod test {
|
||||
let res = parse_response(raw_response);
|
||||
match res {
|
||||
Err(nom::Err::Incomplete(_)) => {}
|
||||
_ => panic!("Expected incomplete response, got {:?}", res),
|
||||
_ => panic!("Expected incomplete response, got {res:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,11 +503,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||
)
|
||||
.first_line(),
|
||||
Some("me")
|
||||
@@ -505,9 +516,9 @@ mod test {
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"me mo".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
],
|
||||
)
|
||||
.first_line(),
|
||||
@@ -532,7 +543,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
vec![" ".to_owned()],
|
||||
)
|
||||
.first_line(),
|
||||
Some(" ")
|
||||
@@ -544,7 +555,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
vec![" ".to_owned()],
|
||||
)
|
||||
.first_line(),
|
||||
Some(" ")
|
||||
@@ -556,7 +567,7 @@ mod test {
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
vec!["".to_owned()],
|
||||
)
|
||||
.first_line(),
|
||||
Some("")
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
use std::time::Duration;
|
||||
#[cfg(feature = "pool")]
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
use r2d2::Pool;
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg(feature = "pool")]
|
||||
use super::pool::sync_impl::Pool;
|
||||
#[cfg(feature = "pool")]
|
||||
use super::PoolConfig;
|
||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use crate::{address::Envelope, Transport};
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
/// Sends emails using the SMTP protocol
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
#[derive(Clone)]
|
||||
/// Transport using the SMTP protocol
|
||||
pub struct SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: Pool<SmtpClient>,
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
#[cfg(feature = "pool")]
|
||||
inner: Arc<Pool>,
|
||||
#[cfg(not(feature = "pool"))]
|
||||
inner: SmtpClient,
|
||||
}
|
||||
|
||||
@@ -26,20 +27,25 @@ impl Transport for SmtpTransport {
|
||||
|
||||
/// Sends an email
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
#[cfg(feature = "r2d2")]
|
||||
let mut conn = self.inner.get()?;
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
let result = conn.send(envelope, email)?;
|
||||
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit()?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SmtpTransport {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut builder = f.debug_struct("SmtpTransport");
|
||||
builder.field("inner", &self.inner);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpTransport {
|
||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||
///
|
||||
@@ -47,7 +53,11 @@ impl SmtpTransport {
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[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 relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
@@ -56,7 +66,7 @@ impl SmtpTransport {
|
||||
.tls(Tls::Wrapper(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
|
||||
/// Simple and secure transport, using STARTTLS to obtain encrypted connections
|
||||
///
|
||||
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
|
||||
/// that don't take SMTPS connections.
|
||||
@@ -67,7 +77,11 @@ impl SmtpTransport {
|
||||
///
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[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 starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
@@ -89,38 +103,143 @@ impl SmtpTransport {
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
/// * A 60-seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`SmtpTransport::relay`](#method.relay) or
|
||||
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
SmtpTransportBuilder {
|
||||
info: new,
|
||||
#[cfg(feature = "r2d2")]
|
||||
pool_config: PoolConfig::default(),
|
||||
SmtpTransportBuilder::new(server)
|
||||
}
|
||||
|
||||
/// Creates a `SmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// The protocol, credentials, host and port can be provided in a single URL.
|
||||
/// Use the scheme `smtp` for an unencrypted relay (optionally in combination with the
|
||||
/// `tls` parameter to allow/require STARTTLS) or `smtps` for SMTP over TLS.
|
||||
/// The path section of the url can be used to set an alternative name for
|
||||
/// the HELO / EHLO command.
|
||||
/// For example `smtps://username:password@smtp.example.com/client.example.com:465`
|
||||
/// will set the HELO / EHLO name `client.example.com`.
|
||||
///
|
||||
/// <table>
|
||||
/// <thead>
|
||||
/// <tr>
|
||||
/// <th>scheme</th>
|
||||
/// <th>tls parameter</th>
|
||||
/// <th>example</th>
|
||||
/// <th>remarks</th>
|
||||
/// </tr>
|
||||
/// </thead>
|
||||
/// <tbody>
|
||||
/// <tr>
|
||||
/// <td>smtps</td>
|
||||
/// <td>-</td>
|
||||
/// <td>smtps://smtp.example.com</td>
|
||||
/// <td>SMTP over TLS, recommended method</td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>required</td>
|
||||
/// <td>smtp://smtp.example.com?tls=required</td>
|
||||
/// <td>SMTP with STARTTLS required, when SMTP over TLS is not available</td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>opportunistic</td>
|
||||
/// <td>smtp://smtp.example.com?tls=opportunistic</td>
|
||||
/// <td>
|
||||
/// SMTP with optionally STARTTLS when supported by the server.
|
||||
/// Caution: this method is vulnerable to a man-in-the-middle attack.
|
||||
/// Not recommended for production use.
|
||||
/// </td>
|
||||
/// </tr>
|
||||
/// <tr>
|
||||
/// <td>smtp</td>
|
||||
/// <td>-</td>
|
||||
/// <td>smtp://smtp.example.com</td>
|
||||
/// <td>Unencrypted SMTP, not recommended for production use.</td>
|
||||
/// </tr>
|
||||
/// </tbody>
|
||||
/// </table>
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use lettre::{
|
||||
/// message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||
/// SmtpTransport, Transport,
|
||||
/// };
|
||||
///
|
||||
/// let email = Message::builder()
|
||||
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
/// .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
/// .subject("Happy new year")
|
||||
/// .header(ContentType::TEXT_PLAIN)
|
||||
/// .body(String::from("Be happy!"))
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Open a remote connection to example
|
||||
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com:465")
|
||||
/// .unwrap()
|
||||
/// .build();
|
||||
///
|
||||
/// // Send the email
|
||||
/// match mailer.send(&email) {
|
||||
/// Ok(_) => println!("Email sent successfully!"),
|
||||
/// Err(e) => panic!("Could not send email: {e:?}"),
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
super::connection_url::from_connection_url(connection_url)
|
||||
}
|
||||
|
||||
/// Tests the SMTP connection
|
||||
///
|
||||
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
||||
/// The connection is closed afterward if a connection pool is not used.
|
||||
pub fn test_connection(&self) -> Result<bool, Error> {
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
let is_connected = conn.test_connected();
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit()?;
|
||||
|
||||
Ok(is_connected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`SmtpTransport`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
impl SmtpTransportBuilder {
|
||||
// Create new builder with default parameters
|
||||
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
info: new,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
@@ -152,7 +271,11 @@ impl SmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// 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", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
@@ -161,8 +284,8 @@ impl SmtpTransportBuilder {
|
||||
/// Use a custom configuration for the connection pool
|
||||
///
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
#[cfg(feature = "pool")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
|
||||
self.pool_config = pool_config;
|
||||
self
|
||||
@@ -170,21 +293,20 @@ impl SmtpTransportBuilder {
|
||||
|
||||
/// Build the transport
|
||||
///
|
||||
/// If the `r2d2` feature is enabled an `Arc` wrapped pool is be created.
|
||||
/// If the `pool` feature is enabled, an `Arc` wrapped pool is created.
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
let client = SmtpClient { info: self.info };
|
||||
SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: self.pool_config.build(client),
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
inner: client,
|
||||
}
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
let client = Pool::new(self.pool_config, client);
|
||||
|
||||
SmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpClient {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
@@ -195,9 +317,9 @@ impl SmtpClient {
|
||||
/// Handles encryption and authentication
|
||||
pub fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match self.info.tls {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
let tls_parameters = match &self.info.tls {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -207,24 +329,100 @@ impl SmtpClient {
|
||||
self.info.timeout,
|
||||
&self.info.hello_name,
|
||||
tls_parameters,
|
||||
None,
|
||||
)?;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
match self.info.tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||
match &self.info.tls {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, &credentials)?;
|
||||
conn.auth(&self.info.authentication, credentials)?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
transport::smtp::{authentication::Credentials, client::Tls},
|
||||
SmtpTransport,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn transport_from_url() {
|
||||
let builder = SmtpTransport::from_url("smtp://127.0.0.1:2525").unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 2525);
|
||||
assert!(matches!(builder.info.tls, Tls::None));
|
||||
assert_eq!(builder.info.server, "127.0.0.1");
|
||||
|
||||
let builder =
|
||||
SmtpTransport::from_url("smtps://username:password@smtp.example.com:465").unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 465);
|
||||
assert_eq!(
|
||||
builder.info.credentials,
|
||||
Some(Credentials::new(
|
||||
"username".to_owned(),
|
||||
"password".to_owned()
|
||||
))
|
||||
);
|
||||
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||
assert_eq!(builder.info.server, "smtp.example.com");
|
||||
|
||||
let builder = SmtpTransport::from_url(
|
||||
"smtps://user%40example.com:pa$$word%3F%22!@smtp.example.com:465",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 465);
|
||||
assert_eq!(
|
||||
builder.info.credentials,
|
||||
Some(Credentials::new(
|
||||
"user@example.com".to_owned(),
|
||||
"pa$$word?\"!".to_owned()
|
||||
))
|
||||
);
|
||||
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||
assert_eq!(builder.info.server, "smtp.example.com");
|
||||
|
||||
let builder =
|
||||
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 587);
|
||||
assert_eq!(
|
||||
builder.info.credentials,
|
||||
Some(Credentials::new(
|
||||
"username".to_owned(),
|
||||
"password".to_owned()
|
||||
))
|
||||
);
|
||||
assert!(matches!(builder.info.tls, Tls::Required(_)));
|
||||
|
||||
let builder = SmtpTransport::from_url(
|
||||
"smtp://username:password@smtp.example.com:587?tls=opportunistic",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 587);
|
||||
assert!(matches!(builder.info.tls, Tls::Opportunistic(_)));
|
||||
|
||||
let builder = SmtpTransport::from_url("smtps://smtp.example.com").unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 465);
|
||||
assert_eq!(builder.info.credentials, None);
|
||||
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
||||
/// Encode a string as xtext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct XText<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Display for XText<'a> {
|
||||
@@ -42,7 +41,7 @@ mod tests {
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
assert_eq!(format!("{}", XText(input)), expect.to_string());
|
||||
assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//! The stub transport only logs message envelope and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//! The stub transport logs message envelopes as well as contents. It can be useful for testing
|
||||
//! purposes.
|
||||
//!
|
||||
//! #### Stub Transport
|
||||
//! # Stub Transport
|
||||
//!
|
||||
//! The stub transport returns provided result and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//! The stub transport logs message envelopes as well as contents. It can be useful for testing
|
||||
//! purposes.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "builder")]
|
||||
@@ -12,7 +14,7 @@
|
||||
//! use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! # fn try_main() -> Result<(), Box<dyn Error>> {
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
@@ -23,18 +25,36 @@
|
||||
//! let mut sender = StubTransport::new_ok();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! assert_eq!(
|
||||
//! sender.messages(),
|
||||
//! vec![(
|
||||
//! email.envelope().clone(),
|
||||
//! String::from_utf8(email.formatted()).unwrap()
|
||||
//! )],
|
||||
//! );
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # try_main().unwrap();
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
sync::{Arc, Mutex as StdMutex},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use futures_util::lock::Mutex as FuturesMutex;
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
use async_trait::async_trait;
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
/// An error returned by the stub transport
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Error;
|
||||
|
||||
@@ -46,47 +66,113 @@ impl fmt::Display for Error {
|
||||
|
||||
impl StdError for Error {}
|
||||
|
||||
/// This transport logs the message envelope and returns the given response
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// This transport logs messages and always returns the given response
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StubTransport {
|
||||
response: Result<(), Error>,
|
||||
message_log: Arc<StdMutex<Vec<(Envelope, String)>>>,
|
||||
}
|
||||
|
||||
/// This transport logs messages and always returns the given response
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncStubTransport {
|
||||
response: Result<(), Error>,
|
||||
message_log: Arc<FuturesMutex<Vec<(Envelope, String)>>>,
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
/// Creates a new transport that always returns the given Result
|
||||
pub fn new(response: Result<(), Error>) -> StubTransport {
|
||||
StubTransport { response }
|
||||
pub fn new(response: Result<(), Error>) -> Self {
|
||||
Self {
|
||||
response,
|
||||
message_log: Arc::new(StdMutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_ok() -> StubTransport {
|
||||
StubTransport { response: Ok(()) }
|
||||
pub fn new_ok() -> Self {
|
||||
Self {
|
||||
response: Ok(()),
|
||||
message_log: Arc::new(StdMutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns an error
|
||||
pub fn new_error() -> StubTransport {
|
||||
StubTransport {
|
||||
pub fn new_error() -> Self {
|
||||
Self {
|
||||
response: Err(Error),
|
||||
message_log: Arc::new(StdMutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all logged messages sent using [`Transport::send_raw`]
|
||||
pub fn messages(&self) -> Vec<(Envelope, String)> {
|
||||
self.message_log
|
||||
.lock()
|
||||
.expect("Couldn't acquire lock to write message log")
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl AsyncStubTransport {
|
||||
/// Creates a new transport that always returns the given Result
|
||||
pub fn new(response: Result<(), Error>) -> Self {
|
||||
Self {
|
||||
response,
|
||||
message_log: Arc::new(FuturesMutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_ok() -> Self {
|
||||
Self {
|
||||
response: Ok(()),
|
||||
message_log: Arc::new(FuturesMutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns an error
|
||||
pub fn new_error() -> Self {
|
||||
Self {
|
||||
response: Err(Error),
|
||||
message_log: Arc::new(FuturesMutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all logged messages sent using [`AsyncTransport::send_raw`]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub async fn messages(&self) -> Vec<(Envelope, String)> {
|
||||
self.message_log.lock().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for StubTransport {
|
||||
type Ok = ();
|
||||
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> {
|
||||
self.message_log
|
||||
.lock()
|
||||
.expect("Couldn't acquire lock to write message log")
|
||||
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for StubTransport {
|
||||
impl AsyncTransport for AsyncStubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.message_log
|
||||
.lock()
|
||||
.await
|
||||
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
7
testdata/coredns.conf
vendored
Normal file
7
testdata/coredns.conf
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
. {
|
||||
bind 127.0.0.54
|
||||
forward . 9.9.9.9 8.8.8.8 1.1.1.1 {
|
||||
except example.org
|
||||
}
|
||||
file testdata/db.example.org example.org
|
||||
}
|
||||
2
testdata/db.example.org
vendored
Normal file
2
testdata/db.example.org
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
@ 600 IN SOA ns.example.org hostmaster.example.org 1 10800 3600 604800 3600
|
||||
dkimtest._domainkey 600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+FHbM8BwkBBz/Ux5OYLQ5Bp1HVuCHTP6Rr3HXTnome/2cGl/ze0tsmmFbCjjsS89MXbMGs9xJhjv18LmL1N0UTllblOizzVjorQyN4RwBOfG34j7SS56pwzrA738Ry8FAbL5InPWEgVzbOhXuTCs8yuzcqTnm4sH/csnIl7cMWeQkVn1FR9LKMtUG0fjhDPkdX0jx3qTX1L3Z7a7gX6geY191yNd9i9DvE2/+wMigMYz1LAts4alk2g86MQhtbjc8AOR7EC15hSw37/lmamlunYLa3wC+PzHNMA8sAfnmkgNvipssjh8LnelD9qn+VtsjQB5ppkeQx3TcUPvz5z+QIDAQAB"
|
||||
7
testdata/email_with_png.eml
vendored
7
testdata/email_with_png.eml
vendored
@@ -1,13 +1,14 @@
|
||||
Date: Tue, 15 Nov 1994 08:12:31 GMT
|
||||
Date: Tue, 15 Nov 1994 08:12:31 +0000
|
||||
From: NoBody <nobody@domain.tld>
|
||||
Reply-To: Yuin <yuin@domain.tld>
|
||||
To: Hei <hei@domain.tld>
|
||||
Subject: Happy new year
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/related; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
|
||||
Content-Type: multipart/related;
|
||||
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
|
||||
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
Content-Type: text/html; charset=utf8
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
#[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
fn default_date() -> std::time::SystemTime {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
mod test {
|
||||
use lettre::{transport::file::FileTransport, Message};
|
||||
mod sync {
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{read_to_string, remove_file},
|
||||
};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
use lettre::{FileTransport, Message, Transport};
|
||||
|
||||
use crate::default_date;
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
use lettre::Transport;
|
||||
let sender = FileTransport::new(temp_dir());
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(&email);
|
||||
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();
|
||||
|
||||
assert_eq!(
|
||||
@@ -36,7 +43,7 @@ mod test {
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
@@ -48,24 +55,23 @@ mod test {
|
||||
#[test]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
fn file_transport_with_envelope() {
|
||||
use lettre::Transport;
|
||||
let sender = FileTransport::with_envelope(temp_dir());
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(&email);
|
||||
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 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();
|
||||
|
||||
assert_eq!(
|
||||
@@ -75,7 +81,7 @@ mod test {
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
@@ -95,63 +101,37 @@ mod test {
|
||||
remove_file(eml_file).unwrap();
|
||||
remove_file(json_file).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_std::test]
|
||||
async fn file_transport_asyncstd1() {
|
||||
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport};
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{read_to_string, remove_file},
|
||||
};
|
||||
|
||||
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let id = result.unwrap();
|
||||
use crate::default_date;
|
||||
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn file_transport_tokio02() {
|
||||
use lettre::{AsyncFileTransport, AsyncTransport, Tokio02Executor};
|
||||
|
||||
let sender = AsyncFileTransport::<Tokio02Executor>::new(temp_dir());
|
||||
async fn file_transport_tokio1() {
|
||||
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
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();
|
||||
|
||||
assert_eq!(
|
||||
@@ -161,7 +141,58 @@ mod test {
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "file-transport",
|
||||
feature = "builder",
|
||||
feature = "async-std1"
|
||||
))]
|
||||
mod asyncstd_1 {
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{read_to_string, remove_file},
|
||||
};
|
||||
|
||||
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
|
||||
use crate::default_date;
|
||||
|
||||
#[async_std::test]
|
||||
async fn file_transport_asyncstd1() {
|
||||
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||
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")
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let id = result.unwrap();
|
||||
|
||||
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
mod test {
|
||||
use lettre::{transport::sendmail::SendmailTransport, Message};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
mod sync {
|
||||
use lettre::{Message, SendmailTransport, Transport};
|
||||
|
||||
#[test]
|
||||
fn sendmail_transport() {
|
||||
use lettre::Transport;
|
||||
let sender = SendmailTransport::new();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
@@ -19,47 +15,60 @@ mod test {
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(&email);
|
||||
println!("{:?}", result);
|
||||
println!("{result:?}");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
feature = "builder",
|
||||
feature = "tokio1"
|
||||
))]
|
||||
mod tokio_1 {
|
||||
use lettre::{AsyncSendmailTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sendmail_transport_tokio1() {
|
||||
let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
|
||||
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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
println!("{result:?}");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
feature = "builder",
|
||||
feature = "async-std1"
|
||||
))]
|
||||
mod asyncstd_1 {
|
||||
use lettre::{AsyncSendmailTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_std::test]
|
||||
async fn sendmail_transport_asyncstd1() {
|
||||
use lettre::{AsyncSendmailTransport, AsyncStd1Executor, AsyncTransport};
|
||||
|
||||
let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn sendmail_transport_tokio02() {
|
||||
use lettre::{AsyncSendmailTransport, Tokio02Executor, Tokio02Transport};
|
||||
|
||||
let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
println!("{:?}", result);
|
||||
println!("{result:?}");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "builder"))]
|
||||
mod test {
|
||||
mod sync {
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
#[test]
|
||||
@@ -12,10 +12,61 @@ mod test {
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build()
|
||||
.send(&email)
|
||||
.unwrap();
|
||||
.build();
|
||||
sender.send(&email).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn smtp_transport_simple_tokio1() {
|
||||
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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let sender: AsyncSmtpTransport<Tokio1Executor> =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
sender.send(email).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
feature = "builder",
|
||||
feature = "async-std1"
|
||||
))]
|
||||
mod asyncstd_1 {
|
||||
use lettre::{AsyncSmtpTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
|
||||
#[async_std::test]
|
||||
async fn smtp_transport_simple_asyncstd1() {
|
||||
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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let sender: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
AsyncSmtpTransport::<AsyncStd1Executor>::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
sender.send(email).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
|
||||
mod test {
|
||||
use lettre::{address::Envelope, SmtpTransport, Transport};
|
||||
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "pool"))]
|
||||
mod sync {
|
||||
use std::{sync::mpsc, thread};
|
||||
|
||||
use lettre::{address::Envelope, SmtpTransport, Transport};
|
||||
|
||||
fn envelope() -> Envelope {
|
||||
Envelope::new(
|
||||
Some("user@localhost".parse().unwrap()),
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "builder")]
|
||||
mod test {
|
||||
use lettre::{transport::stub::StubTransport, Message};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
mod sync {
|
||||
use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
use lettre::Transport;
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
@@ -21,45 +17,68 @@ mod test {
|
||||
|
||||
sender_ok.send(&email).unwrap();
|
||||
sender_ko.send(&email).unwrap_err();
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_std::test]
|
||||
async fn stub_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn stub_transport_tokio02() {
|
||||
use lettre::Tokio02Transport;
|
||||
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
let expected_messages = [(
|
||||
email.envelope().clone(),
|
||||
String::from_utf8(email.formatted()).unwrap(),
|
||||
)];
|
||||
assert_eq!(sender_ok.messages(), expected_messages);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "builder", feature = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn stub_transport_tokio1() {
|
||||
let sender_ok = AsyncStubTransport::new_ok();
|
||||
let sender_ko = AsyncStubTransport::new_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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email.clone()).await.unwrap_err();
|
||||
|
||||
let expected_messages = [(
|
||||
email.envelope().clone(),
|
||||
String::from_utf8(email.formatted()).unwrap(),
|
||||
)];
|
||||
assert_eq!(sender_ok.messages().await, expected_messages);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "builder", feature = "async-std1"))]
|
||||
mod asyncstd_1 {
|
||||
use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
|
||||
|
||||
#[async_std::test]
|
||||
async fn stub_transport_asyncstd1() {
|
||||
let sender_ok = AsyncStubTransport::new_ok();
|
||||
let sender_ko = AsyncStubTransport::new_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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email.clone()).await.unwrap_err();
|
||||
|
||||
let expected_messages = [(
|
||||
email.envelope().clone(),
|
||||
String::from_utf8(email.formatted()).unwrap(),
|
||||
)];
|
||||
assert_eq!(sender_ok.messages().await, expected_messages);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user