Compare commits
88 Commits
v0.10.0-rc
...
v0.10.x
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a81ab0309 | ||
|
|
ce363273fe | ||
|
|
e59ecc20e7 | ||
|
|
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 |
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
|
||||||
56
.github/workflows/test.yml
vendored
56
.github/workflows/test.yml
vendored
@@ -13,7 +13,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rustfmt:
|
rustfmt:
|
||||||
name: rustfmt / stable
|
name: rustfmt / nightly-2022-11-12
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: |
|
run: |
|
||||||
rustup update --no-self-update stable
|
rustup default nightly-2022-11-12
|
||||||
rustup component add rustfmt
|
rustup component add rustfmt
|
||||||
|
|
||||||
- name: cargo fmt
|
- name: cargo fmt
|
||||||
@@ -52,17 +52,11 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-check
|
|
||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: rustup update --no-self-update stable
|
run: rustup update --no-self-update stable
|
||||||
|
|
||||||
|
- name: Setup cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Install cargo hack
|
- name: Install cargo hack
|
||||||
run: cargo install cargo-hack --debug
|
run: cargo install cargo-hack --debug
|
||||||
@@ -81,27 +75,21 @@ jobs:
|
|||||||
rust: stable
|
rust: stable
|
||||||
- name: beta
|
- name: beta
|
||||||
rust: beta
|
rust: beta
|
||||||
- name: 1.52.1
|
- name: 1.60.0
|
||||||
rust: 1.52.1
|
rust: 1.60.0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}
|
|
||||||
|
|
||||||
- name: Install rust
|
- name: Install rust
|
||||||
run: |
|
run: |
|
||||||
rustup default ${{ matrix.rust }}
|
rustup default ${{ matrix.rust }}
|
||||||
rustup update --no-self-update ${{ matrix.rust }}
|
rustup update --no-self-update ${{ matrix.rust }}
|
||||||
|
|
||||||
|
- name: Setup cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Install postfix
|
- name: Install postfix
|
||||||
run: |
|
run: |
|
||||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update
|
DEBIAN_FRONTEND=noninteractive sudo apt-get update
|
||||||
@@ -110,15 +98,35 @@ jobs:
|
|||||||
- name: Run SMTP server
|
- name: Run SMTP server
|
||||||
run: smtp-sink 2525 1000&
|
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 async-global-executor --precise 2.0.4
|
||||||
|
|
||||||
- name: Test with no default features
|
- name: Test with no default features
|
||||||
run: cargo test --no-default-features
|
run: cargo test --no-default-features
|
||||||
|
|
||||||
- name: Test with default features
|
- name: Test with default features
|
||||||
run: cargo test
|
run: cargo test
|
||||||
|
|
||||||
- name: Test with all features
|
- name: Test with all features (-native-tls)
|
||||||
run: cargo test --all-features
|
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,boring,boring-tls,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-native-certs,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-boring-tls,tokio1-rustls-tls,tokio1_boring,tokio1_crate,tokio1_rustls,tracing,uuid,webpki-roots
|
||||||
|
|
||||||
|
- name: Test with all features (-boring-tls)
|
||||||
|
run: cargo test --no-default-features --features async-std,async-std1,async-std1-rustls-tls,async-trait,base64,builder,dkim,ed25519-dalek,email-encoding,fastrand,file-transport,file-transport-envelope,futures-io,futures-rustls,futures-util,hostname,httpdate,mime,mime03,native-tls,nom,once_cell,pool,quoted_printable,rsa,rustls,rustls-native-certs,rustls-pemfile,rustls-tls,sendmail-transport,serde,serde_json,sha2,smtp-transport,socket2,tokio1,tokio1-native-tls,tokio1-rustls-tls,tokio1_crate,tokio1_native_tls_crate,tokio1_rustls,tracing,uuid,webpki-roots
|
||||||
|
|
||||||
# coverage:
|
# coverage:
|
||||||
# name: Coverage
|
# name: Coverage
|
||||||
# runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
|
|||||||
103
CHANGELOG.md
103
CHANGELOG.md
@@ -1,11 +1,109 @@
|
|||||||
|
<a name="v0.10.4"></a>
|
||||||
|
### v0.10.4 (2023-04-02)
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Bumped rustls to 0.21 and all related dependencies ([#867])
|
||||||
|
|
||||||
|
[#867]: https://github.com/lettre/lettre/pull/867
|
||||||
|
|
||||||
|
<a name="v0.10.3"></a>
|
||||||
|
### v0.10.3 (2023-02-20)
|
||||||
|
|
||||||
|
#### Announcements
|
||||||
|
|
||||||
|
It was found that what had been used until now as a basic lettre 0.10
|
||||||
|
`MessageBuilder::body` example failed to mention that for maximum
|
||||||
|
compatibility with various email clients a `Content-Type` header
|
||||||
|
should always be present in the message.
|
||||||
|
|
||||||
|
##### Before
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Message::builder()
|
||||||
|
// [...] some headers skipped for brevity
|
||||||
|
.body(String::from("A plaintext or html body"))?
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Patch
|
||||||
|
|
||||||
|
```diff
|
||||||
|
Message::builder()
|
||||||
|
// [...] some headers skipped for brevity
|
||||||
|
+ .header(ContentType::TEXT_PLAIN) // or `TEXT_HTML` if the body is html
|
||||||
|
.body(String::from("A plaintext or html body"))?
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* Add support for rustls-native-certs when using rustls ([#843])
|
||||||
|
|
||||||
|
[#843]: https://github.com/lettre/lettre/pull/843
|
||||||
|
|
||||||
|
<a name="v0.10.2"></a>
|
||||||
|
### v0.10.2 (2023-01-29)
|
||||||
|
|
||||||
|
#### Upgrade notes
|
||||||
|
|
||||||
|
* MSRV is now 1.60 ([#828])
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* Allow providing a custom `tokio` stream for `AsyncSmtpTransport` ([#805])
|
||||||
|
* Return whole SMTP error message ([#821])
|
||||||
|
|
||||||
|
#### Bug fixes
|
||||||
|
|
||||||
|
* Mailbox displays wrongly when containing a comma and a non-ascii char in its name ([#827])
|
||||||
|
* Require `quoted_printable` ^0.4.6 in order to fix encoding of tabs and spaces at the end of line ([#837])
|
||||||
|
|
||||||
|
#### Misc
|
||||||
|
|
||||||
|
* Increase tracing ([#848])
|
||||||
|
* Bump `idna` to 0.3 ([#816])
|
||||||
|
* Update `base64` to 0.21 ([#840] and [#851])
|
||||||
|
* Update `rsa` to 0.8 ([#829] and [#852])
|
||||||
|
|
||||||
|
[#805]: https://github.com/lettre/lettre/pull/805
|
||||||
|
[#816]: https://github.com/lettre/lettre/pull/816
|
||||||
|
[#821]: https://github.com/lettre/lettre/pull/821
|
||||||
|
[#827]: https://github.com/lettre/lettre/pull/827
|
||||||
|
[#828]: https://github.com/lettre/lettre/pull/828
|
||||||
|
[#829]: https://github.com/lettre/lettre/pull/829
|
||||||
|
[#837]: https://github.com/lettre/lettre/pull/837
|
||||||
|
[#840]: https://github.com/lettre/lettre/pull/840
|
||||||
|
[#848]: https://github.com/lettre/lettre/pull/848
|
||||||
|
[#851]: https://github.com/lettre/lettre/pull/851
|
||||||
|
[#852]: https://github.com/lettre/lettre/pull/852
|
||||||
|
|
||||||
|
<a name="v0.10.1"></a>
|
||||||
|
### v0.10.1 (2022-07-20)
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
* Add `boring-tls` support for `SmtpTransport` and `AsyncSmtpTransport`. The latter is only supported with the tokio runtime. ([#797]) ([#798])
|
||||||
|
* Make the minimum TLS version configurable. ([#799]) ([#800])
|
||||||
|
|
||||||
|
#### Bug Fixes
|
||||||
|
|
||||||
|
* Ensure connections are closed on abort. ([#801])
|
||||||
|
* Fix SMTP dot stuffing. ([#803])
|
||||||
|
|
||||||
|
[#797]: https://github.com/lettre/lettre/pull/797
|
||||||
|
[#798]: https://github.com/lettre/lettre/pull/798
|
||||||
|
[#799]: https://github.com/lettre/lettre/pull/799
|
||||||
|
[#800]: https://github.com/lettre/lettre/pull/800
|
||||||
|
[#801]: https://github.com/lettre/lettre/pull/801
|
||||||
|
[#803]: https://github.com/lettre/lettre/pull/803
|
||||||
|
|
||||||
<a name="v0.10.0"></a>
|
<a name="v0.10.0"></a>
|
||||||
### v0.10.0 (unreleased)
|
### v0.10.0 (2022-06-29)
|
||||||
|
|
||||||
#### Upgrade notes
|
#### Upgrade notes
|
||||||
|
|
||||||
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
|
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
|
||||||
|
|
||||||
* MSRV is now 1.52.1
|
* MSRV is now 1.56.0
|
||||||
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message`
|
* 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).
|
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,
|
* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
|
||||||
@@ -29,6 +127,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
|||||||
* Refactor `TlsParameters` implementation to not expose the internal TLS library
|
* Refactor `TlsParameters` implementation to not expose the internal TLS library
|
||||||
* `FileTransport` writes emails into `.eml` instead of `.json`
|
* `FileTransport` writes emails into `.eml` instead of `.json`
|
||||||
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
|
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
|
||||||
|
* The `sendmail` and `file` transports aren't enabled by default anymore.
|
||||||
* The `new` method of `ClientId` is deprecated
|
* The `new` method of `ClientId` is deprecated
|
||||||
* Rename `serde-impls` feature to `serde`
|
* Rename `serde-impls` feature to `serde`
|
||||||
* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of
|
* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of
|
||||||
|
|||||||
70
Cargo.toml
70
Cargo.toml
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lettre"
|
name = "lettre"
|
||||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||||
version = "0.10.0-rc.4"
|
version = "0.10.4"
|
||||||
description = "Email client"
|
description = "Email client"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://lettre.rs"
|
homepage = "https://lettre.rs"
|
||||||
@@ -10,7 +10,8 @@ license = "MIT"
|
|||||||
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
|
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
|
||||||
categories = ["email", "network-programming"]
|
categories = ["email", "network-programming"]
|
||||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
rust-version = "1.60"
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||||
@@ -18,32 +19,35 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
|||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
idna = "0.2"
|
idna = "0.3"
|
||||||
once_cell = "1"
|
once_cell = { version = "1", optional = true }
|
||||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
||||||
|
|
||||||
# builder
|
# builder
|
||||||
httpdate = { version = "1", optional = true }
|
httpdate = { version = "1", optional = true }
|
||||||
mime = { version = "0.3.4", optional = true }
|
mime = { version = "0.3.4", optional = true }
|
||||||
fastrand = { version = "1.4", optional = true }
|
fastrand = { version = "1.4", optional = true }
|
||||||
quoted_printable = { version = "0.4", optional = true }
|
quoted_printable = { version = "0.4.6", optional = true }
|
||||||
base64 = { version = "0.13", optional = true }
|
base64 = { version = "0.21", optional = true }
|
||||||
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
|
email-encoding = { version = "0.2", optional = true }
|
||||||
|
|
||||||
# file transport
|
# file transport
|
||||||
uuid = { version = "0.8", features = ["v4"], optional = true }
|
uuid = { version = "1", features = ["v4"], optional = true }
|
||||||
serde = { version = "1", optional = true, features = ["derive"] }
|
serde = { version = "1", optional = true, features = ["derive"] }
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
|
|
||||||
# smtp
|
# smtp-transport
|
||||||
nom = { version = "7", optional = true }
|
nom = { version = "7", optional = true }
|
||||||
hostname = { version = "0.3", optional = true } # feature
|
hostname = { version = "0.3", optional = true } # feature
|
||||||
|
socket2 = { version = "0.4.4", optional = true }
|
||||||
|
|
||||||
## tls
|
## tls
|
||||||
native-tls = { version = "0.2", optional = true } # feature
|
native-tls = { version = "0.2", optional = true } # feature
|
||||||
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
|
rustls = { version = "0.21", features = ["dangerous_configuration"], optional = true }
|
||||||
rustls-pemfile = { version = "0.2.1", optional = true }
|
rustls-pemfile = { version = "1", optional = true }
|
||||||
webpki-roots = { version = "0.22", optional = true }
|
rustls-native-certs = { version = "0.6.2", optional = true }
|
||||||
|
webpki-roots = { version = "0.23", optional = true }
|
||||||
|
boring = { version = "2.0.0", optional = true }
|
||||||
|
|
||||||
# async
|
# async
|
||||||
futures-io = { version = "0.3.7", optional = true }
|
futures-io = { version = "0.3.7", optional = true }
|
||||||
@@ -51,24 +55,35 @@ futures-util = { version = "0.3.7", default-features = false, features = ["io"],
|
|||||||
async-trait = { version = "0.1", optional = true }
|
async-trait = { version = "0.1", optional = true }
|
||||||
|
|
||||||
## async-std
|
## async-std
|
||||||
async-std = { version = "1.8", optional = true, features = ["unstable"] }
|
async-std = { version = "1.8", optional = true }
|
||||||
#async-native-tls = { version = "0.3.3", optional = true }
|
#async-native-tls = { version = "0.3.3", optional = true }
|
||||||
futures-rustls = { version = "0.22", optional = true }
|
futures-rustls = { version = "0.24", optional = true }
|
||||||
|
|
||||||
## tokio
|
## tokio
|
||||||
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "process", "time", "net", "io-util"], optional = true }
|
tokio1_crate = { package = "tokio", version = "1", optional = true }
|
||||||
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
|
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
|
||||||
tokio1_rustls = { package = "tokio-rustls", version = "0.23", optional = true }
|
tokio1_rustls = { package = "tokio-rustls", version = "0.24", optional = true }
|
||||||
|
tokio1_boring = { package = "tokio-boring", version = "2.1.4", optional = true }
|
||||||
|
|
||||||
|
## dkim
|
||||||
|
sha2 = { version = "0.10", optional = true, features = ["oid"] }
|
||||||
|
rsa = { version = "0.8", optional = true }
|
||||||
|
ed25519-dalek = { version = "1.0.1", optional = true }
|
||||||
|
|
||||||
|
# email formats
|
||||||
|
email_address = { version = "0.2.1", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3"
|
pretty_assertions = "1"
|
||||||
tracing-subscriber = "0.2.10"
|
criterion = "0.4"
|
||||||
|
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
async-std = { version = "1.8", features = ["attributes"] }
|
async-std = { version = "1.8", features = ["attributes"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
maud = "0.22.1"
|
maud = "0.24"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
harness = false
|
harness = false
|
||||||
@@ -76,19 +91,21 @@ name = "transport_smtp"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
||||||
builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable"]
|
builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"]
|
||||||
mime03 = ["mime"]
|
mime03 = ["mime"]
|
||||||
|
|
||||||
# transports
|
# transports
|
||||||
file-transport = ["uuid"]
|
file-transport = ["uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||||
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
||||||
sendmail-transport = []
|
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
||||||
smtp-transport = ["base64", "nom"]
|
smtp-transport = ["base64", "nom", "socket2", "once_cell", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||||
|
|
||||||
pool = ["futures-util"]
|
pool = ["futures-util"]
|
||||||
|
|
||||||
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"]
|
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"]
|
||||||
|
|
||||||
|
boring-tls = ["boring"]
|
||||||
|
|
||||||
# async
|
# async
|
||||||
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
|
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
|
||||||
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
|
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
|
||||||
@@ -96,11 +113,18 @@ async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"]
|
|||||||
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
|
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
|
||||||
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
||||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
||||||
|
tokio1-boring-tls = ["tokio1", "boring-tls", "tokio1_boring"]
|
||||||
|
|
||||||
|
dkim = ["base64", "sha2", "rsa", "ed25519-dalek"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
|
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "autoconfigure"
|
||||||
|
required-features = ["smtp-transport", "native-tls"]
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "basic_html"
|
name = "basic_html"
|
||||||
required-features = ["file-transport", "builder"]
|
required-features = ["file-transport", "builder"]
|
||||||
|
|||||||
4
LICENSE
4
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me>
|
Copyright (c) 2014-2022 Alexis Mousset <contact@amousset.me>
|
||||||
Copyright (c) 2019-2020 Paolo Barbolini <paolo@paolo565.org>
|
Copyright (c) 2019-2022 Paolo Barbolini <paolo@paolo565.org>
|
||||||
Copyright (c) 2018 K. <kayo@illumium.org>
|
Copyright (c) 2018 K. <kayo@illumium.org>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any
|
Permission is hereby granted, free of charge, to any
|
||||||
|
|||||||
53
README.md
53
README.md
@@ -28,27 +28,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://deps.rs/crate/lettre/0.10.0-rc.4">
|
<a href="https://deps.rs/crate/lettre/0.10.4">
|
||||||
<img src="https://deps.rs/crate/lettre/0.10.0-rc.4/status.svg"
|
<img src="https://deps.rs/crate/lettre/0.10.4/status.svg"
|
||||||
alt="dependency status" />
|
alt="dependency status" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**NOTE**: this readme refers to the 0.10 version of lettre, which is
|
|
||||||
in release candidate state. Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x)
|
|
||||||
branch for the previous stable release.
|
|
||||||
|
|
||||||
0.10 is already widely used and is already thought to be more reliable than 0.9, so it should generally be used
|
|
||||||
for new projects.
|
|
||||||
|
|
||||||
We'd love to hear your feedback about 0.10 design and APIs before final release!
|
|
||||||
Start a [discussion](https://github.com/lettre/lettre/discussions) in the repository, whether for
|
|
||||||
feedback or if you need help or advice using or upgrading lettre 0.10.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Lettre provides the following features:
|
Lettre provides the following features:
|
||||||
@@ -63,18 +50,24 @@ Lettre does not provide (for now):
|
|||||||
|
|
||||||
* Email parsing
|
* Email parsing
|
||||||
|
|
||||||
|
## Supported Rust Versions
|
||||||
|
|
||||||
|
Lettre supports all Rust versions released in the last 6 months. At the time of writing
|
||||||
|
the minimum supported Rust version is 1.60, but this could change at any time either from
|
||||||
|
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
This library requires Rust 1.52.1 or newer.
|
This library requires Rust 1.60 or newer.
|
||||||
To use this library, add the following to your `Cargo.toml`:
|
To use this library, add the following to your `Cargo.toml`:
|
||||||
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lettre = "0.10.0-rc.4"
|
lettre = "0.10"
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust,no_run
|
```rust,no_run
|
||||||
|
use lettre::message::header::ContentType;
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
use lettre::{Message, SmtpTransport, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
|
|
||||||
@@ -83,10 +76,11 @@ let email = Message::builder()
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new year")
|
.subject("Happy new year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy!"))
|
.body(String::from("Be happy!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to gmail
|
// Open a remote connection to gmail
|
||||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||||
@@ -101,13 +95,32 @@ match mailer.send(&email) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Not sure of which connect options to use?
|
||||||
|
|
||||||
|
Clone the lettre git repository and run the following command (replacing `SMTP_HOST` with your SMTP server's hostname)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run --example autoconfigure SMTP_HOST
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
|
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
|
||||||
such a server can be launched with `python -m smtpd -n -c DebuggingServer localhost:2525`
|
such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
|
||||||
|
|
||||||
Alternatively only unit tests can be run by doing `cargo test --lib`.
|
Alternatively only unit tests can be run by doing `cargo test --lib`.
|
||||||
|
|
||||||
|
## 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
|
## Code of conduct
|
||||||
|
|
||||||
Anyone who interacts with Lettre in any space, including but not limited to
|
Anyone who interacts with Lettre in any space, including but not limited to
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use lettre::{
|
use lettre::{
|
||||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
|
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||||
AsyncTransport, Message,
|
AsyncStd1Executor, AsyncTransport, Message,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
@@ -12,10 +12,11 @@ async fn main() {
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new async year")
|
.subject("Happy new async year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy with async!"))
|
.body(String::from("Be happy with async!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to gmail using STARTTLS
|
// Open a remote connection to gmail using STARTTLS
|
||||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||||
@@ -27,6 +28,6 @@ async fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(email).await {
|
match mailer.send(email).await {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use lettre::{
|
use lettre::{
|
||||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
|
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||||
AsyncTransport, Message,
|
AsyncStd1Executor, AsyncTransport, Message,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
@@ -12,10 +12,11 @@ async fn main() {
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new async year")
|
.subject("Happy new async year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy with async!"))
|
.body(String::from("Be happy with async!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to gmail
|
// Open a remote connection to gmail
|
||||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||||
@@ -27,6 +28,6 @@ async fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(email).await {
|
match mailer.send(email).await {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
93
examples/autoconfigure.rs
Normal file
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 updating it via the SMTP STARTTLS extension", smtp_host);
|
||||||
|
|
||||||
|
let transport = SmtpTransport::starttls_relay(&smtp_host)
|
||||||
|
.expect("build SmtpTransport::starttls_relay")
|
||||||
|
.timeout(Some(Duration::from_secs(10)))
|
||||||
|
.build();
|
||||||
|
match transport.test_connection() {
|
||||||
|
Ok(true) => {
|
||||||
|
tracing::info!("Successfully connected to {} via a plaintext connection which then got upgraded to TLS via the SMTP STARTTLS extension (SmtpTransport::starttls_relay). This is the second best option after the previous TLS wrapped option", smtp_host);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Couldn't connect to {} via STARTTLS. No more information is available",
|
||||||
|
smtp_host
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(err = %err, "Couldn't connect to {} via STARTTLS", smtp_host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Plaintext connection (very insecure)
|
||||||
|
{
|
||||||
|
tracing::info!(
|
||||||
|
"Trying to establish a plaintext connection to {}",
|
||||||
|
smtp_host
|
||||||
|
);
|
||||||
|
|
||||||
|
let transport = SmtpTransport::builder_dangerous(&smtp_host)
|
||||||
|
.timeout(Some(Duration::from_secs(10)))
|
||||||
|
.build();
|
||||||
|
match transport.test_connection() {
|
||||||
|
Ok(true) => {
|
||||||
|
tracing::info!("Successfully connected to {} via a plaintext connection. This option is very insecure and shouldn't be used on the public internet (SmtpTransport::builder_dangerous)", smtp_host);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Couldn't connect to {} via a plaintext connection. No more information is available",
|
||||||
|
smtp_host
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(err = %err, "Couldn't connect to {} via a plaintext connection", smtp_host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use lettre::{Message, SmtpTransport, Transport};
|
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
@@ -8,6 +8,7 @@ fn main() {
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new year")
|
.subject("Happy new year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy!"))
|
.body(String::from("Be happy!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -17,6 +18,6 @@ fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use lettre::{
|
use lettre::{
|
||||||
|
message::header::ContentType,
|
||||||
transport::smtp::{
|
transport::smtp::{
|
||||||
authentication::Credentials,
|
authentication::Credentials,
|
||||||
client::{Certificate, Tls, TlsParameters},
|
client::{Certificate, Tls, TlsParameters},
|
||||||
@@ -16,18 +17,19 @@ fn main() {
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new year")
|
.subject("Happy new year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy!"))
|
.body(String::from("Be happy!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Use a custom certificate stored on disk to securely verify the server's certificate
|
// Use a custom certificate stored on disk to securely verify the server's certificate
|
||||||
let pem_cert = fs::read("certificate.pem").unwrap();
|
let pem_cert = fs::read("certificate.pem").unwrap();
|
||||||
let cert = Certificate::from_pem(&pem_cert).unwrap();
|
let cert = Certificate::from_pem(&pem_cert).unwrap();
|
||||||
let tls = TlsParameters::builder("smtp.server.com".to_string())
|
let tls = TlsParameters::builder("smtp.server.com".to_owned())
|
||||||
.add_root_certificate(cert)
|
.add_root_certificate(cert)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to the smtp server
|
// Open a remote connection to the smtp server
|
||||||
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
|
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
|
||||||
@@ -39,6 +41,6 @@ fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
use lettre::{
|
||||||
|
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||||
|
SmtpTransport, Transport,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
@@ -8,10 +11,11 @@ fn main() {
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new year")
|
.subject("Happy new year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy!"))
|
.body(String::from("Be happy!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to gmail using STARTTLS
|
// Open a remote connection to gmail using STARTTLS
|
||||||
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
|
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
|
||||||
@@ -22,6 +26,6 @@ fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
use lettre::{
|
||||||
|
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||||
|
SmtpTransport, Transport,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
@@ -8,10 +11,11 @@ fn main() {
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new year")
|
.subject("Happy new year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy!"))
|
.body(String::from("Be happy!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to gmail
|
// Open a remote connection to gmail
|
||||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||||
@@ -22,6 +26,6 @@ fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(&email) {
|
match mailer.send(&email) {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
// This line is only to make it compile from lettre's examples folder,
|
// This line is only to make it compile from lettre's examples folder,
|
||||||
// since it uses Rust 2018 crate renaming to import tokio.
|
// since it uses Rust 2018 crate renaming to import tokio.
|
||||||
// Won't be needed in user's code.
|
// Won't be needed in user's code.
|
||||||
use tokio1_crate as tokio;
|
|
||||||
|
|
||||||
use lettre::{
|
use lettre::{
|
||||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
|
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||||
Tokio1Executor,
|
AsyncTransport, Message, Tokio1Executor,
|
||||||
};
|
};
|
||||||
|
use tokio1_crate as tokio;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -17,10 +16,11 @@ async fn main() {
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new async year")
|
.subject("Happy new async year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy with async!"))
|
.body(String::from("Be happy with async!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to gmail using STARTTLS
|
// Open a remote connection to gmail using STARTTLS
|
||||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||||
@@ -32,6 +32,6 @@ async fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(email).await {
|
match mailer.send(email).await {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
// This line is only to make it compile from lettre's examples folder,
|
// This line is only to make it compile from lettre's examples folder,
|
||||||
// since it uses Rust 2018 crate renaming to import tokio.
|
// since it uses Rust 2018 crate renaming to import tokio.
|
||||||
// Won't be needed in user's code.
|
// Won't be needed in user's code.
|
||||||
use tokio1_crate as tokio;
|
|
||||||
|
|
||||||
use lettre::{
|
use lettre::{
|
||||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
|
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||||
Tokio1Executor,
|
AsyncTransport, Message, Tokio1Executor,
|
||||||
};
|
};
|
||||||
|
use tokio1_crate as tokio;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@@ -17,10 +16,11 @@ async fn main() {
|
|||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||||
.subject("Happy new async year")
|
.subject("Happy new async year")
|
||||||
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(String::from("Be happy with async!"))
|
.body(String::from("Be happy with async!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
|
|
||||||
// Open a remote connection to gmail
|
// Open a remote connection to gmail
|
||||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||||
@@ -32,6 +32,6 @@ async fn main() {
|
|||||||
// Send the email
|
// Send the email
|
||||||
match mailer.send(email).await {
|
match mailer.send(email).await {
|
||||||
Ok(_) => println!("Email sent successfully!"),
|
Ok(_) => println!("Email sent successfully!"),
|
||||||
Err(e) => panic!("Could not send email: {:?}", e),
|
Err(e) => panic!("Could not send email: {e:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
use super::Address;
|
||||||
#[cfg(feature = "builder")]
|
#[cfg(feature = "builder")]
|
||||||
use crate::message::header::{self, Headers};
|
use crate::message::header::{self, Headers};
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
//! Representation of an email address
|
//! Representation of an email address
|
||||||
|
|
||||||
use idna::domain_to_ascii;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::Regex;
|
|
||||||
use std::{
|
use std::{
|
||||||
convert::{TryFrom, TryInto},
|
|
||||||
error::Error,
|
error::Error,
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
fmt::{Display, Formatter, Result as FmtResult},
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
@@ -12,6 +8,9 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use email_address::EmailAddress;
|
||||||
|
use idna::domain_to_ascii;
|
||||||
|
|
||||||
/// Represents an email address with a user and a domain name.
|
/// Represents an email address with a user and a domain name.
|
||||||
///
|
///
|
||||||
/// This type contains email in canonical form (_user@domain.tld_).
|
/// This type contains email in canonical form (_user@domain.tld_).
|
||||||
@@ -55,20 +54,6 @@ pub struct Address {
|
|||||||
at_start: usize,
|
at_start: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regex from the specs
|
|
||||||
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
|
|
||||||
// It will mark esoteric email addresses like quoted string as invalid
|
|
||||||
static USER_RE: Lazy<Regex> =
|
|
||||||
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
|
|
||||||
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
|
|
||||||
Regex::new(
|
|
||||||
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
|
|
||||||
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
|
|
||||||
|
|
||||||
impl Address {
|
impl Address {
|
||||||
/// Creates a new email address from a user and domain.
|
/// Creates a new email address from a user and domain.
|
||||||
///
|
///
|
||||||
@@ -126,7 +111,7 @@ impl Address {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
|
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
|
||||||
if USER_RE.is_match(user) {
|
if EmailAddress::is_valid_local_part(user) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(AddressError::InvalidUser)
|
Err(AddressError::InvalidUser)
|
||||||
@@ -142,16 +127,19 @@ impl Address {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
|
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
|
||||||
if DOMAIN_RE.is_match(domain) {
|
// Domain
|
||||||
|
if EmailAddress::is_valid_domain(domain) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(caps) = LITERAL_RE.captures(domain) {
|
// IP
|
||||||
if let Some(cap) = caps.get(1) {
|
let ip = domain
|
||||||
if cap.as_str().parse::<IpAddr>().is_ok() {
|
.strip_prefix('[')
|
||||||
return Ok(());
|
.and_then(|ip| ip.strip_suffix(']'))
|
||||||
}
|
.unwrap_or(domain);
|
||||||
}
|
|
||||||
|
if ip.parse::<IpAddr>().is_ok() {
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(AddressError::InvalidDomain)
|
Err(AddressError::InvalidDomain)
|
||||||
@@ -196,7 +184,7 @@ where
|
|||||||
let domain = domain.as_ref();
|
let domain = domain.as_ref();
|
||||||
Address::check_domain(domain)?;
|
Address::check_domain(domain)?;
|
||||||
|
|
||||||
let serialized = format!("{}@{}", user, domain);
|
let serialized = format!("{user}@{domain}");
|
||||||
Ok(Address {
|
Ok(Address {
|
||||||
serialized,
|
serialized,
|
||||||
at_start: user.len(),
|
at_start: user.len(),
|
||||||
@@ -238,7 +226,7 @@ fn check_address(val: &str) -> Result<usize, AddressError> {
|
|||||||
Ok(user.len())
|
Ok(user.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
/// Errors in email addresses parsing
|
/// Errors in email addresses parsing
|
||||||
pub enum AddressError {
|
pub enum AddressError {
|
||||||
/// Missing domain or user
|
/// Missing domain or user
|
||||||
@@ -269,7 +257,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_address() {
|
fn ascii_address() {
|
||||||
let addr_str = "something@example.com";
|
let addr_str = "something@example.com";
|
||||||
let addr = Address::from_str(addr_str).unwrap();
|
let addr = Address::from_str(addr_str).unwrap();
|
||||||
let addr2 = Address::new("something", "example.com").unwrap();
|
let addr2 = Address::new("something", "example.com").unwrap();
|
||||||
@@ -279,4 +267,36 @@ mod tests {
|
|||||||
assert_eq!(addr2.user(), "something");
|
assert_eq!(addr2.user(), "something");
|
||||||
assert_eq!(addr2.domain(), "example.com");
|
assert_eq!(addr2.domain(), "example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ascii_address_ipv4() {
|
||||||
|
let addr_str = "something@1.1.1.1";
|
||||||
|
let addr = Address::from_str(addr_str).unwrap();
|
||||||
|
let addr2 = Address::new("something", "1.1.1.1").unwrap();
|
||||||
|
assert_eq!(addr, addr2);
|
||||||
|
assert_eq!(addr.user(), "something");
|
||||||
|
assert_eq!(addr.domain(), "1.1.1.1");
|
||||||
|
assert_eq!(addr2.user(), "something");
|
||||||
|
assert_eq!(addr2.domain(), "1.1.1.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ascii_address_ipv6() {
|
||||||
|
let addr_str = "something@[2606:4700:4700::1111]";
|
||||||
|
let addr = Address::from_str(addr_str).unwrap();
|
||||||
|
let addr2 = Address::new("something", "[2606:4700:4700::1111]").unwrap();
|
||||||
|
assert_eq!(addr, addr2);
|
||||||
|
assert_eq!(addr.user(), "something");
|
||||||
|
assert_eq!(addr.domain(), "[2606:4700:4700::1111]");
|
||||||
|
assert_eq!(addr2.user(), "something");
|
||||||
|
assert_eq!(addr2.domain(), "[2606:4700:4700::1111]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_parts() {
|
||||||
|
assert!(Address::check_user("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err());
|
||||||
|
assert!(
|
||||||
|
Address::check_domain("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com").is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/base64.rs
Normal file
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)
|
||||||
|
}
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
use async_trait::async_trait;
|
|
||||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
|
||||||
use futures_util::future::BoxFuture;
|
|
||||||
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
@@ -12,6 +8,10 @@ use std::path::Path;
|
|||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||||
|
use futures_util::future::BoxFuture;
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
feature = "smtp-transport",
|
feature = "smtp-transport",
|
||||||
any(feature = "tokio1", feature = "async-std1")
|
any(feature = "tokio1", feature = "async-std1")
|
||||||
@@ -109,7 +109,6 @@ impl Executor for Tokio1Executor {
|
|||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
type Sleep = tokio1_crate::time::Sleep;
|
type Sleep = tokio1_crate::time::Sleep;
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
fn spawn<F>(fut: F) -> Self::Handle
|
fn spawn<F>(fut: F) -> Self::Handle
|
||||||
where
|
where
|
||||||
@@ -119,13 +118,11 @@ impl Executor for Tokio1Executor {
|
|||||||
tokio1_crate::spawn(fut)
|
tokio1_crate::spawn(fut)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
fn sleep(duration: Duration) -> Self::Sleep {
|
fn sleep(duration: Duration) -> Self::Sleep {
|
||||||
tokio1_crate::time::sleep(duration)
|
tokio1_crate::time::sleep(duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
async fn connect(
|
async fn connect(
|
||||||
hostname: &str,
|
hostname: &str,
|
||||||
@@ -146,6 +143,7 @@ impl Executor for Tokio1Executor {
|
|||||||
timeout,
|
timeout,
|
||||||
hello_name,
|
hello_name,
|
||||||
tls_parameters,
|
tls_parameters,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -165,13 +163,11 @@ impl Executor for Tokio1Executor {
|
|||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "file-transport-envelope")]
|
#[cfg(feature = "file-transport-envelope")]
|
||||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||||
tokio1_crate::fs::read(path).await
|
tokio1_crate::fs::read(path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "file-transport")]
|
#[cfg(feature = "file-transport")]
|
||||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||||
tokio1_crate::fs::write(path, contents).await
|
tokio1_crate::fs::write(path, contents).await
|
||||||
@@ -209,7 +205,6 @@ impl Executor for AsyncStd1Executor {
|
|||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
type Sleep = BoxFuture<'static, ()>;
|
type Sleep = BoxFuture<'static, ()>;
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
fn spawn<F>(fut: F) -> Self::Handle
|
fn spawn<F>(fut: F) -> Self::Handle
|
||||||
where
|
where
|
||||||
@@ -219,14 +214,12 @@ impl Executor for AsyncStd1Executor {
|
|||||||
async_std::task::spawn(fut)
|
async_std::task::spawn(fut)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
fn sleep(duration: Duration) -> Self::Sleep {
|
fn sleep(duration: Duration) -> Self::Sleep {
|
||||||
let fut = async move { async_std::task::sleep(duration).await };
|
let fut = async move { async_std::task::sleep(duration).await };
|
||||||
Box::pin(fut)
|
Box::pin(fut)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
#[cfg(feature = "smtp-transport")]
|
||||||
async fn connect(
|
async fn connect(
|
||||||
hostname: &str,
|
hostname: &str,
|
||||||
@@ -266,13 +259,11 @@ impl Executor for AsyncStd1Executor {
|
|||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "file-transport-envelope")]
|
#[cfg(feature = "file-transport-envelope")]
|
||||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||||
async_std::fs::read(path).await
|
async_std::fs::read(path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
#[cfg(feature = "file-transport")]
|
#[cfg(feature = "file-transport")]
|
||||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||||
async_std::fs::write(path, contents).await
|
async_std::fs::write(path, contents).await
|
||||||
@@ -288,15 +279,13 @@ impl SpawnHandle for async_std::task::JoinHandle<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod private {
|
mod private {
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub trait Sealed {}
|
pub trait Sealed {}
|
||||||
|
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
impl Sealed for Tokio1Executor {}
|
impl Sealed for super::Tokio1Executor {}
|
||||||
|
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
impl Sealed for AsyncStd1Executor {}
|
impl Sealed for super::AsyncStd1Executor {}
|
||||||
|
|
||||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||||
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
|
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
|
||||||
|
|||||||
76
src/lib.rs
76
src/lib.rs
@@ -6,7 +6,7 @@
|
|||||||
//! * Secure defaults
|
//! * Secure defaults
|
||||||
//! * Async support
|
//! * Async support
|
||||||
//!
|
//!
|
||||||
//! Lettre requires Rust 1.52.1 or newer.
|
//! Lettre requires Rust 1.60 or newer.
|
||||||
//!
|
//!
|
||||||
//! ## Features
|
//! ## Features
|
||||||
//!
|
//!
|
||||||
@@ -41,6 +41,15 @@
|
|||||||
//!
|
//!
|
||||||
//! NOTE: native-tls isn't supported with `async-std`
|
//! NOTE: native-tls isn't supported with `async-std`
|
||||||
//!
|
//!
|
||||||
|
//! #### SMTP over TLS via the boring crate (Boring TLS)
|
||||||
|
//!
|
||||||
|
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
|
||||||
|
//!
|
||||||
|
//! * **boring-tls**: TLS support for the synchronous version of the API
|
||||||
|
//! * **tokio1-boring-tls**: TLS support for the `tokio1` async version of the API
|
||||||
|
//!
|
||||||
|
//! NOTE: boring-tls isn't supported with `async-std`
|
||||||
|
//!
|
||||||
//! #### SMTP over TLS via the rustls crate
|
//! #### SMTP over TLS via the rustls crate
|
||||||
//!
|
//!
|
||||||
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
|
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
|
||||||
@@ -85,6 +94,7 @@
|
|||||||
//! * **serde**: Serialization/Deserialization of entities
|
//! * **serde**: Serialization/Deserialization of entities
|
||||||
//! * **tracing**: Logging using the `tracing` crate
|
//! * **tracing**: Logging using the `tracing` crate
|
||||||
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
|
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
|
||||||
|
//! * **dkim**: Add support for signing email with DKIM
|
||||||
//!
|
//!
|
||||||
//! [`SMTP`]: crate::transport::smtp
|
//! [`SMTP`]: crate::transport::smtp
|
||||||
//! [`sendmail`]: crate::transport::sendmail
|
//! [`sendmail`]: crate::transport::sendmail
|
||||||
@@ -97,8 +107,9 @@
|
|||||||
//! [Tokio 1.x]: https://docs.rs/tokio/1
|
//! [Tokio 1.x]: https://docs.rs/tokio/1
|
||||||
//! [async-std 1.x]: https://docs.rs/async-std/1
|
//! [async-std 1.x]: https://docs.rs/async-std/1
|
||||||
//! [mime 0.3]: https://docs.rs/mime/0.3
|
//! [mime 0.3]: https://docs.rs/mime/0.3
|
||||||
|
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
|
||||||
|
|
||||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.4")]
|
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.4")]
|
||||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -108,20 +119,42 @@
|
|||||||
trivial_numeric_casts,
|
trivial_numeric_casts,
|
||||||
unstable_features,
|
unstable_features,
|
||||||
unused_import_braces,
|
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_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
#[cfg(not(lettre_ignore_tls_mismatch))]
|
#[cfg(not(lettre_ignore_tls_mismatch))]
|
||||||
mod compiletime_checks {
|
mod compiletime_checks {
|
||||||
|
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]
|
||||||
|
compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
|
||||||
|
the executable will fail to link.");
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
feature = "tokio1",
|
feature = "tokio1",
|
||||||
feature = "native-tls",
|
feature = "native-tls",
|
||||||
not(feature = "tokio1-native-tls")
|
not(feature = "tokio1-native-tls")
|
||||||
))]
|
))]
|
||||||
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
|
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
|
||||||
If you'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 were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
feature = "tokio1",
|
feature = "tokio1",
|
||||||
@@ -129,8 +162,17 @@ If you're building a library which depends on lettre import it without default f
|
|||||||
not(feature = "tokio1-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.
|
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` hasn't been enabled by mistake.
|
If you'd like to use `native-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
|
||||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "tokio1",
|
||||||
|
feature = "boring-tls",
|
||||||
|
not(feature = "tokio1-boring-tls")
|
||||||
|
))]
|
||||||
|
compile_error!("Lettre is being built with the `tokio1` and the `boring-tls` features, but the `tokio1-boring-tls` feature hasn't been turned on.
|
||||||
|
If you'd like to use `boring-tls` make sure that the `rustls-tls` feature hasn't been enabled by mistake.
|
||||||
|
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
@@ -149,8 +191,8 @@ If you're building a library which depends on lettre import it without default f
|
|||||||
))]
|
))]
|
||||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
|
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
|
||||||
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
|
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
|
||||||
If you'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 were trying to opt into `rustls-tls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||||
If you're building a library which depends on lettre import lettre without default features and enable just the features you need.");
|
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
feature = "async-std1",
|
feature = "async-std1",
|
||||||
@@ -158,11 +200,13 @@ If you're building a library which depends on lettre import lettre without defau
|
|||||||
not(feature = "async-std1-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.
|
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 (you may need to import lettre without default features)
|
If you'd like to use `native-tls` make sure that the `rustls-tls` hasn't been enabled by mistake.
|
||||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod address;
|
pub mod address;
|
||||||
|
#[cfg(any(feature = "smtp-transport", feature = "dkim"))]
|
||||||
|
mod base64;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
mod executor;
|
mod executor;
|
||||||
@@ -171,6 +215,8 @@ mod executor;
|
|||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
||||||
|
use std::error::Error as StdError;
|
||||||
|
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
pub use self::executor::AsyncStd1Executor;
|
pub use self::executor::AsyncStd1Executor;
|
||||||
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
||||||
@@ -207,21 +253,17 @@ pub use crate::transport::sendmail::SendmailTransport;
|
|||||||
any(feature = "tokio1", feature = "async-std1")
|
any(feature = "tokio1", feature = "async-std1")
|
||||||
))]
|
))]
|
||||||
pub use crate::transport::smtp::AsyncSmtpTransport;
|
pub use crate::transport::smtp::AsyncSmtpTransport;
|
||||||
|
#[cfg(feature = "smtp-transport")]
|
||||||
|
pub use crate::transport::smtp::SmtpTransport;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use crate::transport::Transport;
|
pub use crate::transport::Transport;
|
||||||
use crate::{address::Envelope, error::Error};
|
use crate::{address::Envelope, error::Error};
|
||||||
|
|
||||||
#[cfg(feature = "smtp-transport")]
|
|
||||||
pub use crate::transport::smtp::SmtpTransport;
|
|
||||||
use std::error::Error as StdError;
|
|
||||||
|
|
||||||
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
|
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(feature = "builder")]
|
#[cfg(feature = "builder")]
|
||||||
mod test {
|
mod test {
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::message::{header, header::Headers, Mailbox, Mailboxes};
|
use crate::message::{header, header::Headers, Mailbox, Mailboxes};
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ impl Attachment {
|
|||||||
/// # use std::error::Error;
|
/// # use std::error::Error;
|
||||||
/// use std::fs;
|
/// use std::fs;
|
||||||
///
|
///
|
||||||
/// use lettre::message::{Attachment, header::ContentType};
|
/// use lettre::message::{header::ContentType, Attachment};
|
||||||
///
|
///
|
||||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
/// let filename = String::from("invoice.pdf");
|
/// let filename = String::from("invoice.pdf");
|
||||||
@@ -64,7 +64,7 @@ impl Attachment {
|
|||||||
/// # use std::error::Error;
|
/// # use std::error::Error;
|
||||||
/// use std::fs;
|
/// use std::fs;
|
||||||
///
|
///
|
||||||
/// use lettre::message::{Attachment, header::ContentType};
|
/// use lettre::message::{header::ContentType, Attachment};
|
||||||
///
|
///
|
||||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
/// let content_id = String::from("123");
|
/// let content_id = String::from("123");
|
||||||
@@ -96,7 +96,7 @@ impl Attachment {
|
|||||||
builder.header(header::ContentDisposition::attachment(&filename))
|
builder.header(header::ContentDisposition::attachment(&filename))
|
||||||
}
|
}
|
||||||
Disposition::Inline(content_id) => builder
|
Disposition::Inline(content_id) => builder
|
||||||
.header(header::ContentId::from(format!("<{}>", content_id)))
|
.header(header::ContentId::from(format!("<{content_id}>")))
|
||||||
.header(header::ContentDisposition::inline()),
|
.header(header::ContentDisposition::inline()),
|
||||||
};
|
};
|
||||||
builder = builder.header(content_type);
|
builder = builder.header(content_type);
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
use std::{
|
use std::{mem, ops::Deref};
|
||||||
io::{self, Write},
|
|
||||||
mem,
|
|
||||||
ops::Deref,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::message::header::ContentTransferEncoding;
|
use crate::message::header::ContentTransferEncoding;
|
||||||
|
|
||||||
@@ -41,7 +37,7 @@ impl Body {
|
|||||||
pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
|
pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
|
||||||
let mut buf: MaybeString = buf.into();
|
let mut buf: MaybeString = buf.into();
|
||||||
|
|
||||||
let encoding = buf.encoding();
|
let encoding = buf.encoding(false);
|
||||||
buf.encode_crlf();
|
buf.encode_crlf();
|
||||||
Self::new_impl(buf.into(), encoding)
|
Self::new_impl(buf.into(), encoding)
|
||||||
}
|
}
|
||||||
@@ -61,7 +57,22 @@ impl Body {
|
|||||||
) -> Result<Self, Vec<u8>> {
|
) -> Result<Self, Vec<u8>> {
|
||||||
let mut buf: MaybeString = buf.into();
|
let mut buf: MaybeString = buf.into();
|
||||||
|
|
||||||
if !buf.is_encoding_ok(encoding) {
|
let best_encoding = buf.encoding(true);
|
||||||
|
let ok = match (encoding, best_encoding) {
|
||||||
|
(ContentTransferEncoding::SevenBit, ContentTransferEncoding::SevenBit) => true,
|
||||||
|
(
|
||||||
|
ContentTransferEncoding::EightBit,
|
||||||
|
ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit,
|
||||||
|
) => true,
|
||||||
|
(ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit, _) => false,
|
||||||
|
(
|
||||||
|
ContentTransferEncoding::QuotedPrintable
|
||||||
|
| ContentTransferEncoding::Base64
|
||||||
|
| ContentTransferEncoding::Binary,
|
||||||
|
_,
|
||||||
|
) => true,
|
||||||
|
};
|
||||||
|
if !ok {
|
||||||
return Err(buf.into());
|
return Err(buf.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,36 +102,13 @@ impl Body {
|
|||||||
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
|
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
|
||||||
}
|
}
|
||||||
ContentTransferEncoding::Base64 => {
|
ContentTransferEncoding::Base64 => {
|
||||||
let base64_len = buf.len() * 4 / 3 + 4;
|
let len = email_encoding::body::base64::encoded_len(buf.len());
|
||||||
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
|
|
||||||
|
|
||||||
let mut out = Vec::with_capacity(base64_endings_len);
|
let mut out = String::with_capacity(len);
|
||||||
{
|
email_encoding::body::base64::encode(&buf, &mut out)
|
||||||
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH);
|
.expect("encode body as base64");
|
||||||
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
|
|
||||||
|
|
||||||
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails");
|
Self::dangerous_pre_encoded(out.into_bytes(), ContentTransferEncoding::Base64)
|
||||||
|
|
||||||
// modified Write::write_all to work around base64 crate bug
|
|
||||||
// TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
|
|
||||||
{
|
|
||||||
let mut buf: &[u8] = buf.as_ref();
|
|
||||||
while !buf.is_empty() {
|
|
||||||
match writer.write(buf) {
|
|
||||||
Ok(0) => {
|
|
||||||
// ignore 0 writes
|
|
||||||
}
|
|
||||||
Ok(n) => {
|
|
||||||
buf = &buf[n..];
|
|
||||||
}
|
|
||||||
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
|
|
||||||
Err(e) => panic!("base64 encoding never fails: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::dangerous_pre_encoded(out, ContentTransferEncoding::Base64)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,21 +141,20 @@ impl Body {
|
|||||||
impl MaybeString {
|
impl MaybeString {
|
||||||
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
|
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
|
||||||
///
|
///
|
||||||
/// If the `MaybeString` was created from a `String` composed only of US-ASCII
|
/// The `binary` encoding is never returned
|
||||||
/// characters, with no lines longer than 1000 characters, then 7bit
|
fn encoding(&self, supports_utf8: bool) -> ContentTransferEncoding {
|
||||||
/// encoding will be used, else quoted-printable will be chosen.
|
use email_encoding::body::Encoding;
|
||||||
///
|
|
||||||
/// If the `MaybeString` was instead created from a `Vec<u8>`, base64 encoding is always
|
let output = match self {
|
||||||
/// chosen.
|
Self::String(s) => Encoding::choose(s.as_str(), supports_utf8),
|
||||||
///
|
Self::Binary(b) => Encoding::choose(b.as_slice(), supports_utf8),
|
||||||
/// `8bit` and `binary` encodings are never returned, as they may not be
|
};
|
||||||
/// supported by all SMTP servers.
|
|
||||||
pub fn encoding(&self) -> ContentTransferEncoding {
|
match output {
|
||||||
match &self {
|
Encoding::SevenBit => ContentTransferEncoding::SevenBit,
|
||||||
Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit,
|
Encoding::EightBit => ContentTransferEncoding::EightBit,
|
||||||
// TODO: consider when base64 would be a better option because of output size
|
Encoding::QuotedPrintable => ContentTransferEncoding::QuotedPrintable,
|
||||||
Self::String(_) => ContentTransferEncoding::QuotedPrintable,
|
Encoding::Base64 => ContentTransferEncoding::Base64,
|
||||||
Self::Binary(_) => ContentTransferEncoding::Base64,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,18 +165,6 @@ impl MaybeString {
|
|||||||
Self::Binary(_) => {}
|
Self::Binary(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if using `encoding` to encode this `MaybeString`
|
|
||||||
/// would result into an invalid encoded body.
|
|
||||||
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool {
|
|
||||||
match encoding {
|
|
||||||
ContentTransferEncoding::SevenBit => is_7bit_encoded(self),
|
|
||||||
ContentTransferEncoding::EightBit => is_8bit_encoded(self),
|
|
||||||
ContentTransferEncoding::Binary
|
|
||||||
| ContentTransferEncoding::QuotedPrintable
|
|
||||||
| ContentTransferEncoding::Base64 => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for something that takes an encoded [`Body`].
|
/// A trait for something that takes an encoded [`Body`].
|
||||||
@@ -273,73 +248,6 @@ impl Deref for MaybeString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether it contains only US-ASCII characters,
|
|
||||||
/// and no lines are longer than 1000 characters including the `\n` character.
|
|
||||||
///
|
|
||||||
/// Most efficient content encoding available
|
|
||||||
fn is_7bit_encoded(buf: &[u8]) -> bool {
|
|
||||||
buf.is_ascii() && !contains_too_long_lines(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks that no lines are longer than 1000 characters,
|
|
||||||
/// including the `\n` character.
|
|
||||||
/// NOTE: 8bit isn't supported by all SMTP servers.
|
|
||||||
fn is_8bit_encoded(buf: &[u8]) -> bool {
|
|
||||||
!contains_too_long_lines(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if there are lines that are longer than 1000 characters,
|
|
||||||
/// including the `\n` character.
|
|
||||||
fn contains_too_long_lines(buf: &[u8]) -> bool {
|
|
||||||
buf.len() > 1000 && buf.split(|&b| b == b'\n').any(|line| line.len() > 999)
|
|
||||||
}
|
|
||||||
|
|
||||||
const LINE_SEPARATOR: &[u8] = b"\r\n";
|
|
||||||
const LINE_MAX_LENGTH: usize = 78 - LINE_SEPARATOR.len();
|
|
||||||
|
|
||||||
/// A `Write`r that inserts a line separator `\r\n` every `max_line_length` bytes.
|
|
||||||
struct LineWrappingWriter<'a, W> {
|
|
||||||
writer: &'a mut W,
|
|
||||||
current_line_length: usize,
|
|
||||||
max_line_length: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, W> LineWrappingWriter<'a, W> {
|
|
||||||
pub fn new(writer: &'a mut W, max_line_length: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
writer,
|
|
||||||
current_line_length: 0,
|
|
||||||
max_line_length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, W> Write for LineWrappingWriter<'a, W>
|
|
||||||
where
|
|
||||||
W: Write,
|
|
||||||
{
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
let remaining_line_len = self.max_line_length - self.current_line_length;
|
|
||||||
let write_len = std::cmp::min(buf.len(), remaining_line_len);
|
|
||||||
|
|
||||||
self.writer.write_all(&buf[..write_len])?;
|
|
||||||
|
|
||||||
if remaining_line_len == write_len {
|
|
||||||
self.writer.write_all(LINE_SEPARATOR)?;
|
|
||||||
|
|
||||||
self.current_line_length = 0;
|
|
||||||
} else {
|
|
||||||
self.current_line_length += write_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(write_len)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.writer.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// In place conversion to CRLF line endings
|
/// In place conversion to CRLF line endings
|
||||||
fn in_place_crlf_line_endings(string: &mut String) {
|
fn in_place_crlf_line_endings(string: &mut String) {
|
||||||
let indices = find_all_lf_char_indices(string);
|
let indices = find_all_lf_char_indices(string);
|
||||||
@@ -377,6 +285,8 @@ fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
|
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -509,13 +419,10 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quoted_printable_detect() {
|
fn quoted_printable_detect() {
|
||||||
let encoded = Body::new(String::from("Привет, мир!"));
|
let encoded = Body::new(String::from("Questo messaggio è corto"));
|
||||||
|
|
||||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||||
assert_eq!(
|
assert_eq!(encoded.as_ref(), b"Questo messaggio =C3=A8 corto");
|
||||||
encoded.as_ref(),
|
|
||||||
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -547,14 +454,17 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn quoted_printable_encode_line_wrap() {
|
fn quoted_printable_encode_line_wrap() {
|
||||||
let encoded = Body::new(String::from("Текст письма в уникоде"));
|
let encoded = Body::new(String::from(
|
||||||
|
"Se lo standard 📬 fosse stato più semplice avremmo finito molto prima.",
|
||||||
|
));
|
||||||
|
|
||||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||||
|
println!("{}", std::str::from_utf8(encoded.as_ref()).unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
encoded.as_ref(),
|
encoded.as_ref(),
|
||||||
concat!(
|
concat!(
|
||||||
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
|
"Se lo standard =F0=9F=93=AC fosse stato pi=C3=B9 semplice avremmo finito mo=\r\n",
|
||||||
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5"
|
"lto prima."
|
||||||
)
|
)
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
);
|
);
|
||||||
@@ -562,21 +472,25 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn base64_detect() {
|
fn base64_detect() {
|
||||||
let input = Body::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
let input = Body::new(vec![0; 80]);
|
||||||
let encoding = input.encoding();
|
let encoding = input.encoding();
|
||||||
assert_eq!(encoding, ContentTransferEncoding::Base64);
|
assert_eq!(encoding, ContentTransferEncoding::Base64);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn base64_encode_bytes() {
|
fn base64_encode_bytes() {
|
||||||
let encoded = Body::new_with_encoding(
|
let encoded =
|
||||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
Body::new_with_encoding(vec![0; 80], ContentTransferEncoding::Base64).unwrap();
|
||||||
ContentTransferEncoding::Base64,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||||
assert_eq!(encoded.as_ref(), b"AAECAwQFBgcICQ==");
|
assert_eq!(
|
||||||
|
encoded.as_ref(),
|
||||||
|
concat!(
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||||
|
)
|
||||||
|
.as_bytes()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
614
src/message/dkim.rs
Normal file
614
src/message/dkim.rs
Normal file
@@ -0,0 +1,614 @@
|
|||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
error::Error as StdError,
|
||||||
|
fmt::{self, Display},
|
||||||
|
iter::IntoIterator,
|
||||||
|
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::Keypair),
|
||||||
|
}
|
||||||
|
|
||||||
|
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::Keypair::from_bytes(
|
||||||
|
&crate::base64::decode(private_key).map_err(|err| {
|
||||||
|
DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err))
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(err)))?,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
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
|
||||||
|
/// selector: the name of the key publied in DNS
|
||||||
|
/// domain: the domain for which we sign the message
|
||||||
|
/// private_key: private key in PKCS1 string format
|
||||||
|
/// headers: a list of headers name to be included in the signature. Signing of more than one
|
||||||
|
/// header with same name is not supported
|
||||||
|
/// canonicalization: the canonicalization to be applied on the message
|
||||||
|
/// pub signing_algorithm: the signing algorithm to be used when signing
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DkimConfig {
|
||||||
|
selector: String,
|
||||||
|
domain: String,
|
||||||
|
private_key: DkimSigningKey,
|
||||||
|
headers: Vec<HeaderName>,
|
||||||
|
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-Signture 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",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Header, HeaderName};
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
|
|
||||||
/// `Content-Transfer-Encoding` of the body
|
/// `Content-Transfer-Encoding` of the body
|
||||||
@@ -35,8 +35,9 @@ impl Header for ContentTransferEncoding {
|
|||||||
Ok(s.parse()?)
|
Ok(s.parse()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display(&self) -> String {
|
fn display(&self) -> HeaderValue {
|
||||||
self.to_string()
|
let val = self.to_string();
|
||||||
|
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +75,10 @@ impl Default for ContentTransferEncoding {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::ContentTransferEncoding;
|
use super::ContentTransferEncoding;
|
||||||
use crate::message::header::{HeaderName, Headers};
|
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_content_transfer_encoding() {
|
fn format_content_transfer_encoding() {
|
||||||
@@ -94,20 +97,20 @@ mod test {
|
|||||||
fn parse_content_transfer_encoding() {
|
fn parse_content_transfer_encoding() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||||
"7bit".to_string(),
|
"7bit".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.get::<ContentTransferEncoding>(),
|
headers.get::<ContentTransferEncoding>(),
|
||||||
Some(ContentTransferEncoding::SevenBit)
|
Some(ContentTransferEncoding::SevenBit)
|
||||||
);
|
);
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||||
"base64".to_string(),
|
"base64".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.get::<ContentTransferEncoding>(),
|
headers.get::<ContentTransferEncoding>(),
|
||||||
|
|||||||
@@ -1,29 +1,57 @@
|
|||||||
use super::{Header, HeaderName};
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use email_encoding::headers::EmailWriter;
|
||||||
|
|
||||||
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
|
|
||||||
/// `Content-Disposition` of an attachment
|
/// `Content-Disposition` of an attachment
|
||||||
///
|
///
|
||||||
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
|
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct ContentDisposition(String);
|
pub struct ContentDisposition(HeaderValue);
|
||||||
|
|
||||||
impl ContentDisposition {
|
impl ContentDisposition {
|
||||||
/// An attachment which should be displayed inline into the message
|
/// An attachment which should be displayed inline into the message
|
||||||
pub fn inline() -> Self {
|
pub fn inline() -> Self {
|
||||||
Self("inline".into())
|
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
|
/// An attachment which should be displayed inline into the message, but that also
|
||||||
/// species the filename in case it were to be downloaded
|
/// species the filename in case it were to be downloaded
|
||||||
pub fn inline_with_name(file_name: &str) -> Self {
|
pub fn inline_with_name(file_name: &str) -> Self {
|
||||||
debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'");
|
Self::with_name("inline", file_name)
|
||||||
Self(format!("inline; filename=\"{}\"", file_name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An attachment which is separate from the body of the message, and can be downloaded separately
|
/// An attachment which is separate from the body of the message, and can be downloaded separately
|
||||||
pub fn attachment(file_name: &str) -> Self {
|
pub fn attachment(file_name: &str) -> Self {
|
||||||
debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'");
|
Self::with_name("attachment", file_name)
|
||||||
Self(format!("attachment; filename=\"{}\"", 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, false);
|
||||||
|
w.write_str(kind).expect("writing `kind` returned an error");
|
||||||
|
w.write_char(';').expect("writing `;` returned an error");
|
||||||
|
w.optional_breakpoint();
|
||||||
|
|
||||||
|
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,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,18 +61,28 @@ impl Header for ContentDisposition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||||
Ok(Self(s.into()))
|
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) -> String {
|
fn display(&self) -> HeaderValue {
|
||||||
self.0.clone()
|
self.0.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::ContentDisposition;
|
use super::ContentDisposition;
|
||||||
use crate::message::header::{HeaderName, Headers};
|
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_content_disposition() {
|
fn format_content_disposition() {
|
||||||
@@ -52,12 +90,12 @@ mod test {
|
|||||||
|
|
||||||
headers.set(ContentDisposition::inline());
|
headers.set(ContentDisposition::inline());
|
||||||
|
|
||||||
assert_eq!(format!("{}", headers), "Content-Disposition: inline\r\n");
|
assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");
|
||||||
|
|
||||||
headers.set(ContentDisposition::attachment("something.txt"));
|
headers.set(ContentDisposition::attachment("something.txt"));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", headers),
|
format!("{headers}"),
|
||||||
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
|
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -66,20 +104,20 @@ mod test {
|
|||||||
fn parse_content_disposition() {
|
fn parse_content_disposition() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||||
"inline".to_string(),
|
"inline".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.get::<ContentDisposition>(),
|
headers.get::<ContentDisposition>(),
|
||||||
Some(ContentDisposition::inline())
|
Some(ContentDisposition::inline())
|
||||||
);
|
);
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||||
"attachment; filename=\"something.txt\"".to_string(),
|
"attachment; filename=\"something.txt\"".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.get::<ContentDisposition>(),
|
headers.get::<ContentDisposition>(),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::{
|
|||||||
|
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
|
|
||||||
use super::{Header, HeaderName};
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
|
|
||||||
/// `Content-Type` of the body
|
/// `Content-Type` of the body
|
||||||
@@ -54,8 +54,8 @@ impl Header for ContentType {
|
|||||||
Ok(Self(s.parse()?))
|
Ok(Self(s.parse()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display(&self) -> String {
|
fn display(&self) -> HeaderValue {
|
||||||
self.0.to_string()
|
HeaderValue::new(Self::name(), self.0.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,11 +94,13 @@ impl Display for ContentTypeErr {
|
|||||||
// -- Serialization and Deserialization --
|
// -- Serialization and Deserialization --
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
mod serde {
|
mod serde {
|
||||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
|
||||||
use serde::ser::{Serialize, Serializer};
|
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::{
|
||||||
|
de::{self, Deserialize, Deserializer, Visitor},
|
||||||
|
ser::{Serialize, Serializer},
|
||||||
|
};
|
||||||
|
|
||||||
use super::ContentType;
|
use super::ContentType;
|
||||||
|
|
||||||
impl Serialize for ContentType {
|
impl Serialize for ContentType {
|
||||||
@@ -133,8 +135,7 @@ mod serde {
|
|||||||
match ContentType::parse(mime) {
|
match ContentType::parse(mime) {
|
||||||
Ok(content_type) => Ok(content_type),
|
Ok(content_type) => Ok(content_type),
|
||||||
Err(_) => Err(E::custom(format!(
|
Err(_) => Err(E::custom(format!(
|
||||||
"Couldn't parse the following MIME-Type: {}",
|
"Couldn't parse the following MIME-Type: {mime}"
|
||||||
mime
|
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,8 +148,10 @@ mod serde {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::ContentType;
|
use super::ContentType;
|
||||||
use crate::message::header::{HeaderName, Headers};
|
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_content_type() {
|
fn format_content_type() {
|
||||||
@@ -173,17 +176,17 @@ mod test {
|
|||||||
fn parse_content_type() {
|
fn parse_content_type() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Content-Type"),
|
HeaderName::new_from_ascii_str("Content-Type"),
|
||||||
"text/plain; charset=utf-8".to_string(),
|
"text/plain; charset=utf-8".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
|
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Content-Type"),
|
HeaderName::new_from_ascii_str("Content-Type"),
|
||||||
"text/html; charset=utf-8".to_string(),
|
"text/html; charset=utf-8".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
|
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ use std::time::SystemTime;
|
|||||||
|
|
||||||
use httpdate::HttpDate;
|
use httpdate::HttpDate;
|
||||||
|
|
||||||
use super::{Header, HeaderName};
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
|
|
||||||
/// Message `Date` header
|
/// Message `Date` header
|
||||||
///
|
///
|
||||||
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
|
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Date(HttpDate);
|
pub struct Date(HttpDate);
|
||||||
|
|
||||||
impl Date {
|
impl Date {
|
||||||
@@ -32,29 +32,29 @@ impl Header for Date {
|
|||||||
|
|
||||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||||
let mut s = String::from(s);
|
let mut s = String::from(s);
|
||||||
if s.ends_with(" -0000") {
|
if s.ends_with("+0000") {
|
||||||
// The httpdate crate expects the `Date` to end in ` GMT`, but email
|
// The httpdate crate expects the `Date` to end in ` GMT`, but email
|
||||||
// uses `-0000`, so we crudely fix this issue here.
|
// uses `+0000` to indicate UTC, so we crudely fix this issue here.
|
||||||
|
|
||||||
s.truncate(s.len() - "-0000".len());
|
s.truncate(s.len() - "+0000".len());
|
||||||
s.push_str("GMT");
|
s.push_str("GMT");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self(s.parse::<HttpDate>()?))
|
Ok(Self(s.parse::<HttpDate>()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display(&self) -> String {
|
fn display(&self) -> HeaderValue {
|
||||||
let mut s = self.0.to_string();
|
let mut val = self.0.to_string();
|
||||||
if s.ends_with(" GMT") {
|
if val.ends_with(" GMT") {
|
||||||
// The httpdate crate always appends ` GMT` to the end of the string,
|
// The httpdate crate always appends ` GMT` to the end of the string,
|
||||||
// but this is considered an obsolete date format for email
|
// but this is considered an obsolete date format for email
|
||||||
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
|
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
|
||||||
// so we replace `GMT` with `-0000`
|
// so we replace `GMT` with `+0000`
|
||||||
s.truncate(s.len() - "GMT".len());
|
val.truncate(val.len() - "GMT".len());
|
||||||
s.push_str("-0000");
|
val.push_str("+0000");
|
||||||
}
|
}
|
||||||
|
|
||||||
s
|
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +74,10 @@ impl From<Date> for SystemTime {
|
|||||||
mod test {
|
mod test {
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::Date;
|
use super::Date;
|
||||||
use crate::message::header::{HeaderName, Headers};
|
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_date() {
|
fn format_date() {
|
||||||
@@ -88,7 +90,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n".to_string()
|
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_owned()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tue, 15 Nov 1994 08:12:32 GMT
|
// Tue, 15 Nov 1994 08:12:32 GMT
|
||||||
@@ -98,7 +100,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
"Date: Tue, 15 Nov 1994 08:12:32 -0000\r\n"
|
"Date: Tue, 15 Nov 1994 08:12:32 +0000\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,10 +108,10 @@ mod test {
|
|||||||
fn parse_date() {
|
fn parse_date() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Date"),
|
HeaderName::new_from_ascii_str("Date"),
|
||||||
"Tue, 15 Nov 1994 08:12:31 -0000".to_string(),
|
"Tue, 15 Nov 1994 08:12:31 +0000".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.get::<Date>(),
|
headers.get::<Date>(),
|
||||||
@@ -118,10 +120,10 @@ mod test {
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Date"),
|
HeaderName::new_from_ascii_str("Date"),
|
||||||
"Tue, 15 Nov 1994 08:12:32 -0000".to_string(),
|
"Tue, 15 Nov 1994 08:12:32 +0000".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.get::<Date>(),
|
headers.get::<Date>(),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use super::{Header, HeaderName};
|
use email_encoding::headers::EmailWriter;
|
||||||
|
|
||||||
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::{
|
use crate::{
|
||||||
message::mailbox::{Mailbox, Mailboxes},
|
message::mailbox::{Mailbox, Mailboxes},
|
||||||
BoxError,
|
BoxError,
|
||||||
@@ -12,7 +14,7 @@ pub trait MailboxesHeader {
|
|||||||
macro_rules! mailbox_header {
|
macro_rules! mailbox_header {
|
||||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||||
$(#[$doc])*
|
$(#[$doc])*
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct $type_name(Mailbox);
|
pub struct $type_name(Mailbox);
|
||||||
|
|
||||||
impl Header for $type_name {
|
impl Header for $type_name {
|
||||||
@@ -25,8 +27,15 @@ macro_rules! mailbox_header {
|
|||||||
Ok(Self(mailbox))
|
Ok(Self(mailbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display(&self) -> String {
|
fn display(&self) -> HeaderValue {
|
||||||
self.0.to_string()
|
let mut encoded_value = String::new();
|
||||||
|
let line_len = $header_name.len() + ": ".len();
|
||||||
|
{
|
||||||
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
||||||
|
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +58,7 @@ macro_rules! mailbox_header {
|
|||||||
macro_rules! mailboxes_header {
|
macro_rules! mailboxes_header {
|
||||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||||
$(#[$doc])*
|
$(#[$doc])*
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct $type_name(pub(crate) Mailboxes);
|
pub struct $type_name(pub(crate) Mailboxes);
|
||||||
|
|
||||||
impl MailboxesHeader for $type_name {
|
impl MailboxesHeader for $type_name {
|
||||||
@@ -68,8 +77,15 @@ macro_rules! mailboxes_header {
|
|||||||
Ok(Self(mailbox))
|
Ok(Self(mailbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display(&self) -> String {
|
fn display(&self) -> HeaderValue {
|
||||||
self.0.to_string()
|
let mut encoded_value = String::new();
|
||||||
|
let line_len = $header_name.len() + ": ".len();
|
||||||
|
{
|
||||||
|
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false, false);
|
||||||
|
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
||||||
|
}
|
||||||
|
|
||||||
|
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +176,10 @@ mailboxes_header! {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::{From, Mailbox, Mailboxes};
|
use super::{From, Mailbox, Mailboxes};
|
||||||
use crate::message::header::{HeaderName, Headers};
|
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_single_without_name() {
|
fn format_single_without_name() {
|
||||||
@@ -175,12 +193,12 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_single_with_name() {
|
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();
|
let mut headers = Headers::new();
|
||||||
headers.set(From(from));
|
headers.set(From(from));
|
||||||
|
|
||||||
assert_eq!(headers.to_string(), "From: K. <kayo@example.com>\r\n");
|
assert_eq!(headers.to_string(), "From: Kayo <kayo@example.com>\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -201,7 +219,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_multi_with_name() {
|
fn format_multi_with_name() {
|
||||||
let from = vec![
|
let from = vec![
|
||||||
"K. <kayo@example.com>".parse().unwrap(),
|
"Kayo <kayo@example.com>".parse().unwrap(),
|
||||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -210,7 +228,7 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
|
"From: Kayo <kayo@example.com>, \"Pony P.\" <pony@domain.tld>\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,10 +250,10 @@ mod test {
|
|||||||
let from = vec!["kayo@example.com".parse().unwrap()].into();
|
let from = vec!["kayo@example.com".parse().unwrap()].into();
|
||||||
|
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("From"),
|
HeaderName::new_from_ascii_str("From"),
|
||||||
"kayo@example.com".to_string(),
|
"kayo@example.com".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||||
}
|
}
|
||||||
@@ -245,10 +263,10 @@ mod test {
|
|||||||
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
|
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
|
||||||
|
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("From"),
|
HeaderName::new_from_ascii_str("From"),
|
||||||
"K. <kayo@example.com>".to_string(),
|
"K. <kayo@example.com>".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||||
}
|
}
|
||||||
@@ -261,10 +279,10 @@ mod test {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("From"),
|
HeaderName::new_from_ascii_str("From"),
|
||||||
"kayo@example.com, pony@domain.tld".to_string(),
|
"kayo@example.com, pony@domain.tld".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||||
}
|
}
|
||||||
@@ -277,11 +295,38 @@ mod test {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("From"),
|
HeaderName::new_from_ascii_str("From"),
|
||||||
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(),
|
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multi_with_name_containing_comma() {
|
||||||
|
let from: Vec<Mailbox> = vec![
|
||||||
|
"Test, test <1@example.com>".parse().unwrap(),
|
||||||
|
"Test2, test2 <2@example.com>".parse().unwrap(),
|
||||||
|
];
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt::{self, Display, Formatter},
|
fmt::{self, Display, Formatter, Write},
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use email_encoding::headers::EmailWriter;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
content::*,
|
content::*,
|
||||||
content_disposition::ContentDisposition,
|
content_disposition::ContentDisposition,
|
||||||
@@ -34,13 +36,13 @@ pub trait Header: Clone {
|
|||||||
|
|
||||||
fn parse(s: &str) -> Result<Self, BoxError>;
|
fn parse(s: &str) -> Result<Self, BoxError>;
|
||||||
|
|
||||||
fn display(&self) -> String;
|
fn display(&self) -> HeaderValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A set of email headers
|
/// A set of email headers
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Headers {
|
pub struct Headers {
|
||||||
headers: Vec<(HeaderName, String)>,
|
headers: Vec<HeaderValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Headers {
|
impl Headers {
|
||||||
@@ -68,13 +70,14 @@ impl Headers {
|
|||||||
///
|
///
|
||||||
/// Returns `None` if `Header` isn't present in `Headers`.
|
/// Returns `None` if `Header` isn't present in `Headers`.
|
||||||
pub fn get<H: Header>(&self) -> Option<H> {
|
pub fn get<H: Header>(&self) -> Option<H> {
|
||||||
self.get_raw(&H::name()).and_then(|raw| H::parse(raw).ok())
|
self.get_raw(&H::name())
|
||||||
|
.and_then(|raw_value| H::parse(raw_value).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets `Header` into `Headers`, overriding `Header` if it
|
/// Sets `Header` into `Headers`, overriding `Header` if it
|
||||||
/// was already present in `Headers`
|
/// was already present in `Headers`
|
||||||
pub fn set<H: Header>(&mut self, header: H) {
|
pub fn set<H: Header>(&mut self, header: H) {
|
||||||
self.insert_raw(H::name(), header.display());
|
self.insert_raw(header.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove `Header` from `Headers`, returning it
|
/// Remove `Header` from `Headers`, returning it
|
||||||
@@ -82,7 +85,7 @@ impl Headers {
|
|||||||
/// Returns `None` if `Header` isn't in `Headers`.
|
/// Returns `None` if `Header` isn't in `Headers`.
|
||||||
pub fn remove<H: Header>(&mut self) -> Option<H> {
|
pub fn remove<H: Header>(&mut self) -> Option<H> {
|
||||||
self.remove_raw(&H::name())
|
self.remove_raw(&H::name())
|
||||||
.and_then(|(_name, raw)| H::parse(&raw).ok())
|
.and_then(|value| H::parse(&value.raw_value).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears `Headers`, removing all headers from it
|
/// Clears `Headers`, removing all headers from it
|
||||||
@@ -97,62 +100,46 @@ impl Headers {
|
|||||||
///
|
///
|
||||||
/// Returns `None` if `name` isn't present in `Headers`.
|
/// Returns `None` if `name` isn't present in `Headers`.
|
||||||
pub fn get_raw(&self, name: &str) -> Option<&str> {
|
pub fn get_raw(&self, name: &str) -> Option<&str> {
|
||||||
self.find_header(name).map(|(_name, value)| value)
|
self.find_header(name).map(|value| value.raw_value.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts a raw header into `Headers`, overriding `value` if it
|
/// Inserts a raw header into `Headers`, overriding `value` if it
|
||||||
/// was already present in `Headers`.
|
/// was already present in `Headers`.
|
||||||
pub fn insert_raw(&mut self, name: HeaderName, value: String) {
|
pub fn insert_raw(&mut self, value: HeaderValue) {
|
||||||
match self.find_header_mut(&name) {
|
match self.find_header_mut(&value.name) {
|
||||||
Some((_, current_value)) => {
|
Some(current_value) => {
|
||||||
*current_value = value;
|
*current_value = value;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.headers.push((name, value));
|
self.headers.push(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Appends a raw header into `Headers`
|
|
||||||
///
|
|
||||||
/// If a header with a name of `name` is already present,
|
|
||||||
/// appends `, ` + `value` to it's current value.
|
|
||||||
pub fn append_raw(&mut self, name: HeaderName, value: String) {
|
|
||||||
match self.find_header_mut(&name) {
|
|
||||||
Some((_name, prev_value)) => {
|
|
||||||
prev_value.push_str(", ");
|
|
||||||
prev_value.push_str(&value);
|
|
||||||
}
|
|
||||||
None => self.headers.push((name, value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a raw header from `Headers`, returning it
|
/// Remove a raw header from `Headers`, returning it
|
||||||
///
|
///
|
||||||
/// Returns `None` if `name` isn't present in `Headers`.
|
/// Returns `None` if `name` isn't present in `Headers`.
|
||||||
pub fn remove_raw(&mut self, name: &str) -> Option<(HeaderName, String)> {
|
pub fn remove_raw(&mut self, name: &str) -> Option<HeaderValue> {
|
||||||
self.find_header_index(name).map(|i| self.headers.remove(i))
|
self.find_header_index(name).map(|i| self.headers.remove(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_header(&self, name: &str) -> Option<(&HeaderName, &str)> {
|
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
|
||||||
self.headers
|
self.headers
|
||||||
.iter()
|
.iter()
|
||||||
.find(|&(name_, _value)| name.eq_ignore_ascii_case(name_))
|
.find(|value| name.eq_ignore_ascii_case(&value.name))
|
||||||
.map(|t| (&t.0, t.1.as_str()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_header_mut(&mut self, name: &str) -> Option<(&HeaderName, &mut String)> {
|
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
|
||||||
self.headers
|
self.headers
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|(name_, _value)| name.eq_ignore_ascii_case(name_))
|
.find(|value| name.eq_ignore_ascii_case(&value.name))
|
||||||
.map(|t| (&t.0, &mut t.1))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_header_index(&self, name: &str) -> Option<usize> {
|
fn find_header_index(&self, name: &str) -> Option<usize> {
|
||||||
self.headers
|
self.headers
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.find(|&(_i, (name_, _value))| name.eq_ignore_ascii_case(name_))
|
.find(|(_i, value)| name.eq_ignore_ascii_case(&value.name))
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,10 +147,10 @@ impl Headers {
|
|||||||
impl Display for Headers {
|
impl Display for Headers {
|
||||||
/// Formats `Headers`, ready to put them into an email
|
/// Formats `Headers`, ready to put them into an email
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
for (name, value) in &self.headers {
|
for value in &self.headers {
|
||||||
Display::fmt(name, f)?;
|
f.write_str(&value.name)?;
|
||||||
f.write_str(": ")?;
|
f.write_str(": ")?;
|
||||||
HeaderValueEncoder::encode(name, value, f)?;
|
f.write_str(&value.encoded_value)?;
|
||||||
f.write_str("\r\n")?;
|
f.write_str("\r\n")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,55 +277,86 @@ impl PartialEq<HeaderName> for &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ENCODING_START_PREFIX: &str = "=?utf-8?b?";
|
/// A safe for use header value
|
||||||
const ENCODING_END_SUFFIX: &str = "?=";
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
const MAX_LINE_LEN: usize = 76;
|
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
|
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
|
||||||
struct HeaderValueEncoder {
|
struct HeaderValueEncoder<'a> {
|
||||||
line_len: usize,
|
writer: EmailWriter<'a>,
|
||||||
encode_buf: String,
|
encode_buf: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HeaderValueEncoder {
|
impl<'a> HeaderValueEncoder<'a> {
|
||||||
fn encode(name: &str, value: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
|
||||||
let (words_iter, encoder) = Self::new(name, value);
|
let encoder = Self::new(name, f);
|
||||||
encoder.format(words_iter, f)
|
encoder.format(value.split_inclusive(' '))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new<'a>(name: &str, value: &'a str) -> (WordsPlusFillIterator<'a>, Self) {
|
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
|
||||||
(
|
let line_len = name.len() + ": ".len();
|
||||||
WordsPlusFillIterator { s: value },
|
let writer = EmailWriter::new(writer, line_len, 0, false, false);
|
||||||
Self {
|
|
||||||
line_len: name.len() + ": ".len(),
|
Self {
|
||||||
encode_buf: String::new(),
|
writer,
|
||||||
},
|
encode_buf: String::new(),
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(
|
fn format(mut self, words_iter: impl Iterator<Item = &'a str>) -> fmt::Result {
|
||||||
mut self,
|
|
||||||
words_iter: WordsPlusFillIterator<'_>,
|
|
||||||
f: &mut fmt::Formatter<'_>,
|
|
||||||
) -> fmt::Result {
|
|
||||||
/// Estimate if an encoded string of `len` would fix in an empty line
|
|
||||||
fn would_fit_new_line(len: usize) -> bool {
|
|
||||||
len < (MAX_LINE_LEN - " ".len())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Estimate how long a string of `len` would be after base64 encoding plus
|
|
||||||
/// adding the encoding prefix and suffix to it
|
|
||||||
fn base64_len(len: usize) -> usize {
|
|
||||||
ENCODING_START_PREFIX.len() + (len * 4 / 3 + 4) + ENCODING_END_SUFFIX.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Estimate how many more bytes we can fit in the current line
|
|
||||||
fn available_len_to_max_encode_len(len: usize) -> usize {
|
|
||||||
len.saturating_sub(
|
|
||||||
ENCODING_START_PREFIX.len() + (len * 3 / 4 + 4) + ENCODING_END_SUFFIX.len(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for next_word in words_iter {
|
for next_word in words_iter {
|
||||||
let allowed = allowed_str(next_word);
|
let allowed = allowed_str(next_word);
|
||||||
|
|
||||||
@@ -346,207 +364,54 @@ impl HeaderValueEncoder {
|
|||||||
// This word only contains allowed characters
|
// This word only contains allowed characters
|
||||||
|
|
||||||
// the next word is allowed, but we may have accumulated some words to encode
|
// the next word is allowed, but we may have accumulated some words to encode
|
||||||
self.flush_encode_buf(f, true)?;
|
self.flush_encode_buf()?;
|
||||||
|
|
||||||
if next_word.len() > self.remaining_line_len() {
|
self.writer.folding().write_str(next_word)?;
|
||||||
// not enough space left on this line to encode word
|
|
||||||
|
|
||||||
if self.something_written_to_this_line() && would_fit_new_line(next_word.len())
|
|
||||||
{
|
|
||||||
// word doesn't fit this line, but something had already been written to it,
|
|
||||||
// and word would fit the next line, so go to a new line
|
|
||||||
// so go to new line
|
|
||||||
self.new_line(f)?;
|
|
||||||
} else {
|
|
||||||
// word neither fits this line and the next one, cut it
|
|
||||||
// in the middle and make it fit
|
|
||||||
|
|
||||||
let mut next_word = next_word;
|
|
||||||
|
|
||||||
while !next_word.is_empty() {
|
|
||||||
if self.remaining_line_len() == 0 {
|
|
||||||
self.new_line(f)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let len = self.remaining_line_len().min(next_word.len());
|
|
||||||
let first_part = &next_word[..len];
|
|
||||||
next_word = &next_word[len..];
|
|
||||||
|
|
||||||
f.write_str(first_part)?;
|
|
||||||
self.line_len += first_part.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// word fits, write it!
|
|
||||||
f.write_str(next_word)?;
|
|
||||||
self.line_len += next_word.len();
|
|
||||||
} else {
|
} else {
|
||||||
// This word contains unallowed characters
|
// This word contains unallowed characters
|
||||||
|
self.encode_buf.push_str(next_word);
|
||||||
if self.remaining_line_len() >= base64_len(self.encode_buf.len() + next_word.len())
|
|
||||||
{
|
|
||||||
// next_word fits
|
|
||||||
self.encode_buf.push_str(next_word);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// next_word doesn't fit this line
|
|
||||||
|
|
||||||
if would_fit_new_line(base64_len(next_word.len())) {
|
|
||||||
// ...but it would fit the next one
|
|
||||||
|
|
||||||
self.flush_encode_buf(f, false)?;
|
|
||||||
self.new_line(f)?;
|
|
||||||
|
|
||||||
self.encode_buf.push_str(next_word);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...and also wouldn't fit the next one.
|
|
||||||
// chop it up into pieces
|
|
||||||
|
|
||||||
let mut next_word = next_word;
|
|
||||||
|
|
||||||
while !next_word.is_empty() {
|
|
||||||
let mut len = available_len_to_max_encode_len(self.remaining_line_len())
|
|
||||||
.min(next_word.len());
|
|
||||||
|
|
||||||
if len == 0 {
|
|
||||||
self.flush_encode_buf(f, false)?;
|
|
||||||
self.new_line(f)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// avoid slicing on a char boundary
|
|
||||||
while !next_word.is_char_boundary(len) {
|
|
||||||
len += 1;
|
|
||||||
}
|
|
||||||
let first_part = &next_word[..len];
|
|
||||||
next_word = &next_word[len..];
|
|
||||||
|
|
||||||
self.encode_buf.push_str(first_part);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.flush_encode_buf(f, false)?;
|
self.flush_encode_buf()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of bytes left for the current line
|
fn flush_encode_buf(&mut self) -> fmt::Result {
|
||||||
fn remaining_line_len(&self) -> usize {
|
|
||||||
MAX_LINE_LEN - self.line_len
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if something has been written to the current line
|
|
||||||
fn something_written_to_this_line(&self) -> bool {
|
|
||||||
self.line_len > 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush_encode_buf(
|
|
||||||
&mut self,
|
|
||||||
f: &mut fmt::Formatter<'_>,
|
|
||||||
switching_to_allowed: bool,
|
|
||||||
) -> fmt::Result {
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
if self.encode_buf.is_empty() {
|
if self.encode_buf.is_empty() {
|
||||||
// nothing to encode
|
// nothing to encode
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut write_after = None;
|
let prefix = self.encode_buf.trim_end_matches(' ');
|
||||||
|
email_encoding::headers::rfc2047::encode(prefix, &mut self.writer)?;
|
||||||
|
|
||||||
if switching_to_allowed {
|
// TODO: add a better API for doing this in email-encoding
|
||||||
// If the next word only contains allowed characters, and the string to encode
|
let spaces = self.encode_buf.len() - prefix.len();
|
||||||
// ends with a space, take the space out of the part to encode
|
for _ in 0..spaces {
|
||||||
|
self.writer.space();
|
||||||
let last_char = self.encode_buf.pop().expect("self.encode_buf isn't empty");
|
|
||||||
if is_space_like(last_char) {
|
|
||||||
write_after = Some(last_char);
|
|
||||||
} else {
|
|
||||||
self.encode_buf.push(last_char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.write_str(ENCODING_START_PREFIX)?;
|
|
||||||
let encoded = base64::display::Base64Display::with_config(
|
|
||||||
self.encode_buf.as_bytes(),
|
|
||||||
base64::STANDARD,
|
|
||||||
);
|
|
||||||
Display::fmt(&encoded, f)?;
|
|
||||||
f.write_str(ENCODING_END_SUFFIX)?;
|
|
||||||
|
|
||||||
self.line_len += ENCODING_START_PREFIX.len();
|
|
||||||
self.line_len += self.encode_buf.len() * 4 / 3 + 4;
|
|
||||||
self.line_len += ENCODING_END_SUFFIX.len();
|
|
||||||
|
|
||||||
if let Some(write_after) = write_after {
|
|
||||||
f.write_char(write_after)?;
|
|
||||||
self.line_len += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.encode_buf.clear();
|
self.encode_buf.clear();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_line(&mut self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.write_str("\r\n ")?;
|
|
||||||
self.line_len = 1;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator yielding a string split space by space, but including all space
|
|
||||||
/// characters between it and the next word
|
|
||||||
struct WordsPlusFillIterator<'a> {
|
|
||||||
s: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Iterator for WordsPlusFillIterator<'a> {
|
|
||||||
type Item = &'a str;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if self.s.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let next_word = self
|
|
||||||
.s
|
|
||||||
.char_indices()
|
|
||||||
.skip(1)
|
|
||||||
.skip_while(|&(_i, c)| !is_space_like(c))
|
|
||||||
.find(|&(_i, c)| !is_space_like(c))
|
|
||||||
.map(|(i, _)| i);
|
|
||||||
|
|
||||||
let word = &self.s[..next_word.unwrap_or_else(|| self.s.len())];
|
|
||||||
self.s = &self.s[word.len()..];
|
|
||||||
Some(word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn is_space_like(c: char) -> bool {
|
|
||||||
c == ',' || c == ' '
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn allowed_str(s: &str) -> bool {
|
fn allowed_str(s: &str) -> bool {
|
||||||
s.chars().all(allowed_char)
|
s.bytes().all(allowed_char)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn allowed_char(c: char) -> bool {
|
const fn allowed_char(c: u8) -> bool {
|
||||||
c >= 1 as char && c <= 9 as char
|
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
|
||||||
|| c == 11 as char
|
|
||||||
|| c == 12 as char
|
|
||||||
|| c >= 14 as char && c <= 127 as char
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{HeaderName, Headers};
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::{HeaderName, HeaderValue, Headers, To};
|
||||||
|
use crate::message::Mailboxes;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_headername() {
|
fn valid_headername() {
|
||||||
@@ -607,10 +472,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_ascii() {
|
fn format_ascii() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("To"),
|
HeaderName::new_from_ascii_str("To"),
|
||||||
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_string(),
|
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
@@ -621,16 +486,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_ascii_with_folding() {
|
fn format_ascii_with_folding() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("To"),
|
HeaderName::new_from_ascii_str("To"),
|
||||||
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_string(),
|
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith \r\n",
|
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith\r\n",
|
||||||
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand \r\n",
|
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand\r\n",
|
||||||
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
|
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -639,16 +504,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_ascii_with_folding_long_line() {
|
fn format_ascii_with_folding_long_line() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Subject"),
|
HeaderName::new_from_ascii_str("Subject"),
|
||||||
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
|
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: Hello! This is lettre, and this \r\n ",
|
"Subject: Hello! This is lettre, and this\r\n",
|
||||||
"IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n",
|
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||||
" guess that's it!\r\n"
|
" guess that's it!\r\n"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -658,15 +523,17 @@ mod tests {
|
|||||||
fn format_ascii_with_folding_very_long_line() {
|
fn format_ascii_with_folding_very_long_line() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(
|
||||||
|
HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Subject"),
|
HeaderName::new_from_ascii_str("Subject"),
|
||||||
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string()
|
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n",
|
"Subject: Hello!\r\n",
|
||||||
" ouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know\r\n",
|
" IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n",
|
||||||
|
" I don't know\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -674,28 +541,24 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_ascii_with_folding_giant_word() {
|
fn format_ascii_with_folding_giant_word() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Subject"),
|
HeaderName::new_from_ascii_str("Subject"),
|
||||||
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string()
|
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n",
|
||||||
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n",
|
|
||||||
" mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n",
|
|
||||||
" ghijklmnopqrstuvwxyz\r\n",
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_special() {
|
fn format_special() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("To"),
|
HeaderName::new_from_ascii_str("To"),
|
||||||
"Seán <sean@example.com>".to_string(),
|
"Seán <sean@example.com>".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
@@ -706,10 +569,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_special_emoji() {
|
fn format_special_emoji() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("To"),
|
HeaderName::new_from_ascii_str("To"),
|
||||||
"🌎 <world@example.com>".to_string(),
|
"🌎 <world@example.com>".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
@@ -720,19 +583,46 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_special_with_folding() {
|
fn format_special_with_folding() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
let to = To::from(Mailboxes::from_iter([
|
||||||
HeaderName::new_from_ascii_str("To"),
|
"🌍 <world@example.com>".parse().unwrap(),
|
||||||
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(),
|
"🦆 Everywhere <ducks@example.com>".parse().unwrap(),
|
||||||
);
|
"Иванов Иван Иванович <ivanov@example.com>".parse().unwrap(),
|
||||||
|
"Jānis Bērziņš <janis@example.com>".parse().unwrap(),
|
||||||
|
"Seán Ó Rudaí <sean@example.com>".parse().unwrap(),
|
||||||
|
]));
|
||||||
|
headers.set(to);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhiBFdmVyeXdo?=\r\n",
|
||||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
|
" =?utf-8?b?ZXJl?= <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LI=?=\r\n",
|
||||||
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
|
" =?utf-8?b?0LDQvSDQmNCy0LDQvdC+0LLQuNGH?= <ivanov@example.com>,\r\n",
|
||||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n"
|
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_special_with_folding_raw() {
|
||||||
|
let mut headers = Headers::new();
|
||||||
|
headers.insert_raw(HeaderValue::new(
|
||||||
|
HeaderName::new_from_ascii_str("To"),
|
||||||
|
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
|
||||||
|
));
|
||||||
|
|
||||||
|
// TODO: fix the fact that the encoder doesn't know that
|
||||||
|
// the space between the name and the address should be
|
||||||
|
// removed when wrapping.
|
||||||
|
assert_eq!(
|
||||||
|
headers.to_string(),
|
||||||
|
concat!(
|
||||||
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||||
|
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||||
|
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||||
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
|
||||||
|
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -741,23 +631,31 @@ mod tests {
|
|||||||
fn format_slice_on_char_boundary_bug() {
|
fn format_slice_on_char_boundary_bug() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(
|
||||||
|
HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Subject"),
|
HeaderName::new_from_ascii_str("Subject"),
|
||||||
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(),
|
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n"
|
concat!(
|
||||||
|
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n",
|
||||||
|
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
|
||||||
|
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
|
||||||
|
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
|
||||||
|
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
|
||||||
|
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+lsw==?=\r\n"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_bad_stuff() {
|
fn format_bad_stuff() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Subject"),
|
HeaderName::new_from_ascii_str("Subject"),
|
||||||
"Hello! \r\n This is \" bad \0. 👋".to_string(),
|
"Hello! \r\n This is \" bad \0. 👋".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
@@ -769,33 +667,40 @@ mod tests {
|
|||||||
fn format_everything() {
|
fn format_everything() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(
|
||||||
|
HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Subject"),
|
HeaderName::new_from_ascii_str("Subject"),
|
||||||
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
|
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
headers.insert_raw(
|
headers.insert_raw(
|
||||||
|
HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("To"),
|
HeaderName::new_from_ascii_str("To"),
|
||||||
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(),
|
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("From"),
|
HeaderName::new_from_ascii_str("From"),
|
||||||
"Someone <somewhere@example.com>".to_string(),
|
"Someone <somewhere@example.com>".to_owned(),
|
||||||
);
|
));
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||||
"quoted-printable".to_string(),
|
"quoted-printable".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
|
// TODO: fix the fact that the encoder doesn't know that
|
||||||
|
// the space between the name and the address should be
|
||||||
|
// removed when wrapping.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: Hello! This is lettre, and this \r\n",
|
"Subject: Hello! This is lettre, and this\r\n",
|
||||||
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n",
|
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||||
" guess that's it!\r\n",
|
" guess that's it!\r\n",
|
||||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
|
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||||
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
|
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
|
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>,\r\n",
|
||||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
||||||
"From: Someone <somewhere@example.com>\r\n",
|
"From: Someone <somewhere@example.com>\r\n",
|
||||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||||
)
|
)
|
||||||
@@ -805,16 +710,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn issue_653() {
|
fn issue_653() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Subject"),
|
HeaderName::new_from_ascii_str("Subject"),
|
||||||
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjgggggggggfっ".to_string(),
|
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjgggggggggfっ".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n",
|
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7?=\r\n",
|
||||||
" =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n"
|
" =?utf-8?b?Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
message::header::{Header, HeaderName},
|
message::header::{Header, HeaderName, HeaderValue},
|
||||||
BoxError,
|
BoxError,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
|
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub struct MimeVersion {
|
pub struct MimeVersion {
|
||||||
major: u8,
|
major: u8,
|
||||||
minor: u8,
|
minor: u8,
|
||||||
@@ -16,15 +16,18 @@ pub struct MimeVersion {
|
|||||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
|
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
|
||||||
|
|
||||||
impl MimeVersion {
|
impl MimeVersion {
|
||||||
|
/// Build a new `MimeVersion` header
|
||||||
pub const fn new(major: u8, minor: u8) -> Self {
|
pub const fn new(major: u8, minor: u8) -> Self {
|
||||||
MimeVersion { major, minor }
|
MimeVersion { major, minor }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the `major` value of this `MimeVersion` header.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn major(self) -> u8 {
|
pub const fn major(self) -> u8 {
|
||||||
self.major
|
self.major
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the `minor` value of this `MimeVersion` header.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn minor(self) -> u8 {
|
pub const fn minor(self) -> u8 {
|
||||||
self.minor
|
self.minor
|
||||||
@@ -50,8 +53,9 @@ impl Header for MimeVersion {
|
|||||||
Ok(MimeVersion::new(major, minor))
|
Ok(MimeVersion::new(major, minor))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display(&self) -> String {
|
fn display(&self) -> HeaderValue {
|
||||||
format!("{}.{}", self.major, self.minor)
|
let val = format!("{}.{}", self.major, self.minor);
|
||||||
|
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +67,10 @@ impl Default for MimeVersion {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::{MimeVersion, MIME_VERSION_1_0};
|
use super::{MimeVersion, MIME_VERSION_1_0};
|
||||||
use crate::message::header::{HeaderName, Headers};
|
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_mime_version() {
|
fn format_mime_version() {
|
||||||
@@ -83,17 +89,17 @@ mod test {
|
|||||||
fn parse_mime_version() {
|
fn parse_mime_version() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("MIME-Version"),
|
HeaderName::new_from_ascii_str("MIME-Version"),
|
||||||
"1.0".to_string(),
|
"1.0".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
|
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
|
||||||
|
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("MIME-Version"),
|
HeaderName::new_from_ascii_str("MIME-Version"),
|
||||||
"0.1".to_string(),
|
"0.1".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
|
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use super::{Header, HeaderName};
|
use super::{Header, HeaderName, HeaderValue};
|
||||||
use crate::BoxError;
|
use crate::BoxError;
|
||||||
|
|
||||||
macro_rules! text_header {
|
macro_rules! text_header {
|
||||||
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
|
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
|
||||||
$(#[$attr])*
|
$(#[$attr])*
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct $type_name(String);
|
pub struct $type_name(String);
|
||||||
|
|
||||||
impl Header for $type_name {
|
impl Header for $type_name {
|
||||||
@@ -16,8 +16,8 @@ macro_rules! text_header {
|
|||||||
Ok(Self(s.into()))
|
Ok(Self(s.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn display(&self) -> String {
|
fn display(&self) -> HeaderValue {
|
||||||
self.0.clone()
|
HeaderValue::new(Self::name(), self.0.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +85,10 @@ text_header! {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::Subject;
|
use super::Subject;
|
||||||
use crate::message::header::{HeaderName, Headers};
|
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_ascii() {
|
fn format_ascii() {
|
||||||
@@ -107,13 +109,24 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_utf8_word() {
|
||||||
|
let mut headers = Headers::new();
|
||||||
|
headers.set(Subject("Administratör".into()));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
headers.to_string(),
|
||||||
|
"Subject: =?utf-8?b?QWRtaW5pc3RyYXTDtnI=?=\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_ascii() {
|
fn parse_ascii() {
|
||||||
let mut headers = Headers::new();
|
let mut headers = Headers::new();
|
||||||
headers.insert_raw(
|
headers.insert_raw(HeaderValue::new(
|
||||||
HeaderName::new_from_ascii_str("Subject"),
|
HeaderName::new_from_ascii_str("Subject"),
|
||||||
"Sample subject".to_string(),
|
"Sample subject".to_owned(),
|
||||||
);
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.get::<Subject>(),
|
headers.get::<Subject>(),
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
use crate::message::{Mailbox, Mailboxes};
|
use std::fmt::{Formatter, Result as FmtResult};
|
||||||
|
|
||||||
use serde::{
|
use serde::{
|
||||||
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
|
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
|
||||||
ser::Serializer,
|
ser::Serializer,
|
||||||
Deserialize, Serialize,
|
Deserialize, Serialize,
|
||||||
};
|
};
|
||||||
use std::fmt::{Formatter, Result as FmtResult};
|
|
||||||
|
use crate::message::{Mailbox, Mailboxes};
|
||||||
|
|
||||||
impl Serialize for Mailbox {
|
impl Serialize for Mailbox {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
{
|
{
|
||||||
serializer.serialize_str(&self.to_string())
|
serializer.collect_str(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ impl Serialize for Mailboxes {
|
|||||||
where
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
{
|
{
|
||||||
serializer.serialize_str(&self.to_string())
|
serializer.collect_str(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,9 +154,11 @@ impl<'de> Deserialize<'de> for Mailboxes {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::from_str;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::address::Address;
|
use crate::address::Address;
|
||||||
use serde_json::from_str;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_address_string() {
|
fn parse_address_string() {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
use crate::address::{Address, AddressError};
|
|
||||||
use std::{
|
use std::{
|
||||||
convert::TryFrom,
|
|
||||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||||
|
mem,
|
||||||
slice::Iter,
|
slice::Iter,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use email_encoding::headers::EmailWriter;
|
||||||
|
|
||||||
|
use crate::address::{Address, AddressError};
|
||||||
|
|
||||||
/// Represents an email address with an optional name for the sender/recipient.
|
/// Represents an email address with an optional name for the sender/recipient.
|
||||||
///
|
///
|
||||||
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
|
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
|
||||||
@@ -63,6 +66,22 @@ impl Mailbox {
|
|||||||
pub fn new(name: Option<String>, email: Address) -> Self {
|
pub fn new(name: Option<String>, email: Address) -> Self {
|
||||||
Mailbox { name, email }
|
Mailbox { name, email }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
||||||
|
if let Some(name) = &self.name {
|
||||||
|
email_encoding::headers::quoted_string::encode(name, w)?;
|
||||||
|
w.optional_breakpoint();
|
||||||
|
w.write_char('<')?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.write_str(self.email.as_ref())?;
|
||||||
|
|
||||||
|
if self.name.is_some() {
|
||||||
|
w.write_char('>')?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Mailbox {
|
impl Display for Mailbox {
|
||||||
@@ -70,7 +89,7 @@ impl Display for Mailbox {
|
|||||||
if let Some(ref name) = self.name {
|
if let Some(ref name) = self.name {
|
||||||
let name = name.trim();
|
let name = name.trim();
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
f.write_str(name)?;
|
write_word(f, name)?;
|
||||||
f.write_str(" <")?;
|
f.write_str(" <")?;
|
||||||
self.email.fmt(f)?;
|
self.email.fmt(f)?;
|
||||||
return f.write_char('>');
|
return f.write_char('>');
|
||||||
@@ -126,6 +145,12 @@ impl FromStr for Mailbox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Address> for Mailbox {
|
||||||
|
fn from(value: Address) -> Self {
|
||||||
|
Self::new(None, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a sequence of [`Mailbox`] instances.
|
/// Represents a sequence of [`Mailbox`] instances.
|
||||||
///
|
///
|
||||||
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
|
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
|
||||||
@@ -250,6 +275,20 @@ impl Mailboxes {
|
|||||||
pub fn iter(&self) -> Iter<'_, Mailbox> {
|
pub fn iter(&self) -> Iter<'_, Mailbox> {
|
||||||
self.0.iter()
|
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.optional_breakpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox.encode(w)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Mailboxes {
|
impl Default for Mailboxes {
|
||||||
@@ -282,6 +321,18 @@ impl From<Mailboxes> for Vec<Mailbox> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromIterator<Mailbox> for Mailboxes {
|
||||||
|
fn from_iter<T: IntoIterator<Item = Mailbox>>(iter: T) -> Self {
|
||||||
|
Self(Vec::from_iter(iter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Extend<Mailbox> for Mailboxes {
|
||||||
|
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
|
||||||
|
self.0.extend(iter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoIterator for Mailboxes {
|
impl IntoIterator for Mailboxes {
|
||||||
type Item = Mailbox;
|
type Item = Mailbox;
|
||||||
type IntoIter = ::std::vec::IntoIter<Mailbox>;
|
type IntoIter = ::std::vec::IntoIter<Mailbox>;
|
||||||
@@ -291,14 +342,6 @@ impl IntoIterator for Mailboxes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Extend<Mailbox> for Mailboxes {
|
|
||||||
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
|
|
||||||
for elem in iter {
|
|
||||||
self.0.push(elem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Mailboxes {
|
impl Display for Mailboxes {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
let mut iter = self.iter();
|
let mut iter = self.iter();
|
||||||
@@ -319,19 +362,132 @@ impl Display for Mailboxes {
|
|||||||
impl FromStr for Mailboxes {
|
impl FromStr for Mailboxes {
|
||||||
type Err = AddressError;
|
type Err = AddressError;
|
||||||
|
|
||||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
fn from_str(mut src: &str) -> Result<Self, Self::Err> {
|
||||||
src.split(',')
|
let mut mailboxes = Vec::new();
|
||||||
.map(|m| m.trim().parse())
|
|
||||||
.collect::<Result<Vec<_>, _>>()
|
if !src.is_empty() {
|
||||||
.map(Mailboxes)
|
// n-1 elements
|
||||||
|
let mut skip = 0;
|
||||||
|
while let Some(i) = src[skip..].find(',') {
|
||||||
|
let left = &src[..skip + i];
|
||||||
|
|
||||||
|
match left.trim().parse() {
|
||||||
|
Ok(mailbox) => {
|
||||||
|
mailboxes.push(mailbox);
|
||||||
|
|
||||||
|
src = &src[left.len() + ",".len()..];
|
||||||
|
skip = 0;
|
||||||
|
}
|
||||||
|
Err(AddressError::MissingParts) => {
|
||||||
|
skip = left.len() + ",".len();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// last element
|
||||||
|
let mailbox = src.trim().parse()?;
|
||||||
|
mailboxes.push(mailbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 techically 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::Mailbox;
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::Mailbox;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mailbox_format_address_only() {
|
fn mailbox_format_address_only() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -350,7 +506,63 @@ mod test {
|
|||||||
"{}",
|
"{}",
|
||||||
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
|
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>"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +584,7 @@ mod test {
|
|||||||
"{}",
|
"{}",
|
||||||
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
|
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
|
||||||
),
|
),
|
||||||
"K. <kayo@example.com>"
|
"\"K.\" <kayo@example.com>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +626,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parse_address_from_tuple() {
|
fn parse_address_from_tuple() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
("K.".to_string(), "kayo@example.com".to_string()).try_into(),
|
("K.".to_owned(), "kayo@example.com".to_owned()).try_into(),
|
||||||
Ok(Mailbox::new(
|
Ok(Mailbox::new(
|
||||||
Some("K.".into()),
|
Some("K.".into()),
|
||||||
"kayo@example.com".parse().unwrap()
|
"kayo@example.com".parse().unwrap()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use std::io::Write;
|
use std::{io::Write, iter::repeat_with};
|
||||||
|
|
||||||
|
use mime::Mime;
|
||||||
|
|
||||||
use crate::message::{
|
use crate::message::{
|
||||||
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
|
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
|
||||||
EmailFormat, IntoBody,
|
EmailFormat, IntoBody,
|
||||||
};
|
};
|
||||||
use mime::Mime;
|
|
||||||
use std::iter::repeat_with;
|
|
||||||
|
|
||||||
/// MIME part variants
|
/// MIME part variants
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -190,9 +190,9 @@ impl MultiPartKind {
|
|||||||
},
|
},
|
||||||
boundary,
|
boundary,
|
||||||
match self {
|
match self {
|
||||||
Self::Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
|
Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
|
||||||
Self::Signed { protocol, micalg } =>
|
Self::Signed { protocol, micalg } =>
|
||||||
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
|
format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -398,6 +398,8 @@ impl EmailFormat for MultiPart {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::message::header;
|
use crate::message::header;
|
||||||
|
|
||||||
@@ -477,7 +479,7 @@ mod test {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8(part.formatted()).unwrap(),
|
String::from_utf8(part.formatted()).unwrap(),
|
||||||
concat!(
|
concat!(
|
||||||
"Content-Type: multipart/mixed; \r\n",
|
"Content-Type: multipart/mixed;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
@@ -524,8 +526,8 @@ mod test {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8(part.formatted()).unwrap(),
|
String::from_utf8(part.formatted()).unwrap(),
|
||||||
concat!(
|
concat!(
|
||||||
"Content-Type: multipart/encrypted; \r\n",
|
"Content-Type: multipart/encrypted;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
|
||||||
" protocol=\"application/pgp-encrypted\"\r\n",
|
" protocol=\"application/pgp-encrypted\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
@@ -580,8 +582,8 @@ mod test {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8(part.formatted()).unwrap(),
|
String::from_utf8(part.formatted()).unwrap(),
|
||||||
concat!(
|
concat!(
|
||||||
"Content-Type: multipart/signed; \r\n",
|
"Content-Type: multipart/signed;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
|
||||||
" protocol=\"application/pgp-signature\";",
|
" protocol=\"application/pgp-signature\";",
|
||||||
" micalg=\"pgp-sha256\"\r\n",
|
" micalg=\"pgp-sha256\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
@@ -622,7 +624,7 @@ mod test {
|
|||||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
|
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
|
||||||
|
|
||||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||||
concat!("Content-Type: multipart/alternative; \r\n",
|
concat!("Content-Type: multipart/alternative;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
@@ -660,11 +662,11 @@ mod test {
|
|||||||
.body(String::from("int main() { return 0; }")));
|
.body(String::from("int main() { return 0; }")));
|
||||||
|
|
||||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||||
concat!("Content-Type: multipart/mixed; \r\n",
|
concat!("Content-Type: multipart/mixed;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
"Content-Type: multipart/related; \r\n",
|
"Content-Type: multipart/related;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
//! The easiest way of creating a message, which uses a plain text body.
|
//! The easiest way of creating a message, which uses a plain text body.
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! use lettre::message::Message;
|
//! use lettre::message::{header::ContentType, Message};
|
||||||
//!
|
//!
|
||||||
//! # use std::error::Error;
|
//! # use std::error::Error;
|
||||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||||
//! .subject("Happy new year")
|
//! .subject("Happy new year")
|
||||||
|
//! .header(ContentType::TEXT_PLAIN)
|
||||||
//! .body(String::from("Be happy!"))?;
|
//! .body(String::from("Be happy!"))?;
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
//! To: Hei <hei@domain.tld>
|
//! To: Hei <hei@domain.tld>
|
||||||
//! Subject: Happy new year
|
//! Subject: Happy new year
|
||||||
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
||||||
|
//! Content-Type: text/plain; charset=utf-8
|
||||||
//! Content-Transfer-Encoding: 7bit
|
//! Content-Transfer-Encoding: 7bit
|
||||||
//!
|
//!
|
||||||
//! Be happy!
|
//! Be happy!
|
||||||
@@ -106,9 +108,10 @@
|
|||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! # use std::error::Error;
|
//! # use std::error::Error;
|
||||||
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
|
|
||||||
//! use std::fs;
|
//! use std::fs;
|
||||||
//!
|
//!
|
||||||
|
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
|
||||||
|
//!
|
||||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
//! let image = fs::read("docs/lettre.png")?;
|
//! let image = fs::read("docs/lettre.png")?;
|
||||||
//! // this image_body can be cloned and reused between emails.
|
//! // this image_body can be cloned and reused between emails.
|
||||||
@@ -195,15 +198,19 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//! </details>
|
//! </details>
|
||||||
|
|
||||||
use std::{convert::TryFrom, io::Write, iter, time::SystemTime};
|
use std::{io::Write, iter, time::SystemTime};
|
||||||
|
|
||||||
pub use attachment::Attachment;
|
pub use attachment::Attachment;
|
||||||
pub use body::{Body, IntoBody, MaybeString};
|
pub use body::{Body, IntoBody, MaybeString};
|
||||||
|
#[cfg(feature = "dkim")]
|
||||||
|
pub use dkim::*;
|
||||||
pub use mailbox::*;
|
pub use mailbox::*;
|
||||||
pub use mimebody::*;
|
pub use mimebody::*;
|
||||||
|
|
||||||
mod attachment;
|
mod attachment;
|
||||||
mod body;
|
mod body;
|
||||||
|
#[cfg(feature = "dkim")]
|
||||||
|
pub mod dkim;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
mod mailbox;
|
mod mailbox;
|
||||||
mod mimebody;
|
mod mimebody;
|
||||||
@@ -227,6 +234,7 @@ trait EmailFormat {
|
|||||||
pub struct MessageBuilder {
|
pub struct MessageBuilder {
|
||||||
headers: Headers,
|
headers: Headers,
|
||||||
envelope: Option<Envelope>,
|
envelope: Option<Envelope>,
|
||||||
|
drop_bcc: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBuilder {
|
impl MessageBuilder {
|
||||||
@@ -235,24 +243,26 @@ impl MessageBuilder {
|
|||||||
Self {
|
Self {
|
||||||
headers: Headers::new(),
|
headers: Headers::new(),
|
||||||
envelope: None,
|
envelope: None,
|
||||||
|
drop_bcc: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set custom header to message
|
/// Set or add mailbox to `From` header
|
||||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
///
|
||||||
self.headers.set(header);
|
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||||
self
|
///
|
||||||
|
/// Shortcut for `self.mailbox(header::From(mbox))`.
|
||||||
|
pub fn from(self, mbox: Mailbox) -> Self {
|
||||||
|
self.mailbox(header::From::from(Mailboxes::from(mbox)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add mailbox to header
|
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
|
||||||
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
|
///
|
||||||
match self.headers.get::<H>() {
|
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||||
Some(mut header_) => {
|
///
|
||||||
header_.join_mailboxes(header);
|
/// Shortcut for `self.header(header::Sender(mbox))`.
|
||||||
self.header(header_)
|
pub fn sender(self, mbox: Mailbox) -> Self {
|
||||||
}
|
self.header(header::Sender::from(mbox))
|
||||||
None => self.header(header),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add `Date` header to message
|
/// Add `Date` header to message
|
||||||
@@ -270,41 +280,6 @@ impl MessageBuilder {
|
|||||||
self.date(SystemTime::now())
|
self.date(SystemTime::now())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set `Subject` header to message
|
|
||||||
///
|
|
||||||
/// Shortcut for `self.header(header::Subject(subject.into()))`.
|
|
||||||
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
|
|
||||||
let s: String = subject.into();
|
|
||||||
self.header(header::Subject::from(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set `MIME-Version` header to 1.0
|
|
||||||
///
|
|
||||||
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
|
|
||||||
///
|
|
||||||
/// Not exposed as it is set by body methods
|
|
||||||
fn mime_1_0(self) -> Self {
|
|
||||||
self.header(header::MIME_VERSION_1_0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
|
|
||||||
///
|
|
||||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
|
||||||
///
|
|
||||||
/// Shortcut for `self.header(header::Sender(mbox))`.
|
|
||||||
pub fn sender(self, mbox: Mailbox) -> Self {
|
|
||||||
self.header(header::Sender::from(mbox))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set or add mailbox to `From` header
|
|
||||||
///
|
|
||||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
|
||||||
///
|
|
||||||
/// Shortcut for `self.mailbox(header::From(mbox))`.
|
|
||||||
pub fn from(self, mbox: Mailbox) -> Self {
|
|
||||||
self.mailbox(header::From::from(Mailboxes::from(mbox)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set or add mailbox to `ReplyTo` header
|
/// Set or add mailbox to `ReplyTo` header
|
||||||
///
|
///
|
||||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||||
@@ -347,6 +322,14 @@ impl MessageBuilder {
|
|||||||
self.header(header::References::from(id))
|
self.header(header::References::from(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set `Subject` header to message
|
||||||
|
///
|
||||||
|
/// Shortcut for `self.header(header::Subject(subject.into()))`.
|
||||||
|
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
|
||||||
|
let s: String = subject.into();
|
||||||
|
self.header(header::Subject::from(s))
|
||||||
|
}
|
||||||
|
|
||||||
/// Set [Message-ID
|
/// Set [Message-ID
|
||||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||||
///
|
///
|
||||||
@@ -362,9 +345,9 @@ impl MessageBuilder {
|
|||||||
let hostname = hostname::get()
|
let hostname = hostname::get()
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
.and_then(|s| s.into_string().map_err(|_| ()))
|
.and_then(|s| s.into_string().map_err(|_| ()))
|
||||||
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string());
|
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
|
||||||
#[cfg(not(feature = "hostname"))]
|
#[cfg(not(feature = "hostname"))]
|
||||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
|
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
|
||||||
|
|
||||||
self.header(header::MessageId::from(
|
self.header(header::MessageId::from(
|
||||||
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
||||||
@@ -375,17 +358,48 @@ impl MessageBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set [User-Agent
|
/// Set [User-Agent
|
||||||
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
|
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00)
|
||||||
pub fn user_agent(self, id: String) -> Self {
|
pub fn user_agent(self, id: String) -> Self {
|
||||||
self.header(header::UserAgent::from(id))
|
self.header(header::UserAgent::from(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set custom header to message
|
||||||
|
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||||
|
self.headers.set(header);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add mailbox to header
|
||||||
|
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
|
||||||
|
match self.headers.get::<H>() {
|
||||||
|
Some(mut header_) => {
|
||||||
|
header_.join_mailboxes(header);
|
||||||
|
self.header(header_)
|
||||||
|
}
|
||||||
|
None => self.header(header),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Force specific envelope (by default it is derived from headers)
|
/// Force specific envelope (by default it is derived from headers)
|
||||||
pub fn envelope(mut self, envelope: Envelope) -> Self {
|
pub fn envelope(mut self, envelope: Envelope) -> Self {
|
||||||
self.envelope = Some(envelope);
|
self.envelope = Some(envelope);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keep the `Bcc` header
|
||||||
|
///
|
||||||
|
/// By default the `Bcc` header is removed from the email after
|
||||||
|
/// using it to generate the message envelope. In some cases though,
|
||||||
|
/// like when saving the email as an `.eml`, or sending through
|
||||||
|
/// some transports (like the Gmail API) that don't take a separate
|
||||||
|
/// envelope value, it becomes necessary to keep the `Bcc` header.
|
||||||
|
///
|
||||||
|
/// Calling this method overrides the default behaviour.
|
||||||
|
pub fn keep_bcc(mut self) -> Self {
|
||||||
|
self.drop_bcc = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: High-level methods for attachments and embedded files
|
// TODO: High-level methods for attachments and embedded files
|
||||||
|
|
||||||
/// Create message from body
|
/// Create message from body
|
||||||
@@ -418,8 +432,10 @@ impl MessageBuilder {
|
|||||||
None => Envelope::try_from(&res.headers)?,
|
None => Envelope::try_from(&res.headers)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove `Bcc` headers now the envelope is set
|
if res.drop_bcc {
|
||||||
res.headers.remove::<header::Bcc>();
|
// Remove `Bcc` headers now the envelope is set
|
||||||
|
res.headers.remove::<header::Bcc>();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Message {
|
Ok(Message {
|
||||||
headers: res.headers,
|
headers: res.headers,
|
||||||
@@ -450,6 +466,15 @@ impl MessageBuilder {
|
|||||||
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
|
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
|
||||||
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
|
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set `MIME-Version` header to 1.0
|
||||||
|
///
|
||||||
|
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
|
||||||
|
///
|
||||||
|
/// Not exposed as it is set by body methods
|
||||||
|
fn mime_1_0(self) -> Self {
|
||||||
|
self.header(header::MIME_VERSION_1_0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Email message which can be formatted
|
/// Email message which can be formatted
|
||||||
@@ -489,6 +514,77 @@ impl Message {
|
|||||||
self.format(&mut out);
|
self.format(&mut out);
|
||||||
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(&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 {
|
impl EmailFormat for Message {
|
||||||
@@ -522,6 +618,8 @@ fn make_message_id() -> String {
|
|||||||
mod test {
|
mod test {
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
|
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -550,7 +648,7 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn email_message() {
|
fn email_message_no_bcc() {
|
||||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||||
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
|
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
|
||||||
|
|
||||||
@@ -574,9 +672,47 @@ mod test {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8(email.formatted()).unwrap(),
|
String::from_utf8(email.formatted()).unwrap(),
|
||||||
concat!(
|
concat!(
|
||||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\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",
|
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! Error and result type for file transport
|
//! Error and result type for file transport
|
||||||
|
|
||||||
use crate::BoxError;
|
|
||||||
use std::{error::Error as StdError, fmt};
|
use std::{error::Error as StdError, fmt};
|
||||||
|
|
||||||
|
use crate::BoxError;
|
||||||
|
|
||||||
/// The Errors that may occur when sending an email over SMTP
|
/// The Errors that may occur when sending an email over SMTP
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
inner: Box<Inner>,
|
inner: Box<Inner>,
|
||||||
@@ -70,7 +71,7 @@ impl fmt::Display for Error {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref e) = self.inner.source {
|
if let Some(ref e) = self.inner.source {
|
||||||
write!(f, ": {}", e)?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
//! #
|
//! #
|
||||||
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
|
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
//! use lettre::{FileTransport, Message, Transport};
|
|
||||||
//! use std::env::temp_dir;
|
//! use std::env::temp_dir;
|
||||||
//!
|
//!
|
||||||
|
//! use lettre::{FileTransport, Message, Transport};
|
||||||
|
//!
|
||||||
//! // Write to the local temp directory
|
//! // Write to the local temp directory
|
||||||
//! let sender = FileTransport::new(temp_dir());
|
//! let sender = FileTransport::new(temp_dir());
|
||||||
//! let email = Message::builder()
|
//! let email = Message::builder()
|
||||||
@@ -41,9 +42,10 @@
|
|||||||
//! #
|
//! #
|
||||||
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
|
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
|
||||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
//! use lettre::{FileTransport, Message, Transport};
|
|
||||||
//! use std::env::temp_dir;
|
//! use std::env::temp_dir;
|
||||||
//!
|
//!
|
||||||
|
//! use lettre::{FileTransport, Message, Transport};
|
||||||
|
//!
|
||||||
//! // Write to the local temp directory
|
//! // Write to the local temp directory
|
||||||
//! let sender = FileTransport::with_envelope(temp_dir());
|
//! let sender = FileTransport::with_envelope(temp_dir());
|
||||||
//! let email = Message::builder()
|
//! let email = Message::builder()
|
||||||
@@ -70,7 +72,8 @@
|
|||||||
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
|
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
|
||||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||||
//! use std::env::temp_dir;
|
//! use std::env::temp_dir;
|
||||||
//! use lettre::{AsyncTransport, Tokio1Executor, Message, AsyncFileTransport};
|
//!
|
||||||
|
//! use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
||||||
//!
|
//!
|
||||||
//! // Write to the local temp directory
|
//! // Write to the local temp directory
|
||||||
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||||
@@ -95,7 +98,8 @@
|
|||||||
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
|
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
|
||||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||||
//! use std::env::temp_dir;
|
//! use std::env::temp_dir;
|
||||||
//! use lettre::{AsyncTransport, AsyncStd1Executor, Message, AsyncFileTransport};
|
//!
|
||||||
|
//! use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||||
//!
|
//!
|
||||||
//! // Write to the local temp directory
|
//! // Write to the local temp directory
|
||||||
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||||
@@ -132,20 +136,22 @@
|
|||||||
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
|
//! {"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 = "tokio1"))]
|
|
||||||
use crate::{AsyncTransport, Executor};
|
|
||||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
|
||||||
use async_trait::async_trait;
|
|
||||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str,
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||||
|
use async_trait::async_trait;
|
||||||
use uuid::Uuid;
|
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;
|
mod error;
|
||||||
|
|
||||||
type Id = String;
|
type Id = String;
|
||||||
@@ -202,18 +208,18 @@ impl FileTransport {
|
|||||||
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
let eml_file = self.path.join(format!("{}.eml", email_id));
|
let eml_file = self.path.join(format!("{email_id}.eml"));
|
||||||
let eml = fs::read(eml_file).map_err(error::io)?;
|
let eml = fs::read(eml_file).map_err(error::io)?;
|
||||||
|
|
||||||
let json_file = self.path.join(format!("{}.json", email_id));
|
let json_file = self.path.join(format!("{email_id}.json"));
|
||||||
let json = fs::read(&json_file).map_err(error::io)?;
|
let json = fs::read(json_file).map_err(error::io)?;
|
||||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||||
|
|
||||||
Ok((envelope, eml))
|
Ok((envelope, eml))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
|
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
|
||||||
self.path.join(format!("{}.{}", email_id, extension))
|
self.path.join(format!("{email_id}.{extension}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,10 +255,10 @@ where
|
|||||||
/// Reads the envelope and the raw message content.
|
/// Reads the envelope and the raw message content.
|
||||||
#[cfg(feature = "file-transport-envelope")]
|
#[cfg(feature = "file-transport-envelope")]
|
||||||
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||||
let eml_file = self.inner.path.join(format!("{}.eml", email_id));
|
let eml_file = self.inner.path.join(format!("{email_id}.eml"));
|
||||||
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
|
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
|
||||||
|
|
||||||
let json_file = self.inner.path.join(format!("{}.json", email_id));
|
let json_file = self.inner.path.join(format!("{email_id}.json"));
|
||||||
let json = E::fs_read(&json_file).await.map_err(error::io)?;
|
let json = E::fs_read(&json_file).await.map_err(error::io)?;
|
||||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||||
|
|
||||||
@@ -270,6 +276,8 @@ impl Transport for FileTransport {
|
|||||||
let email_id = Uuid::new_v4();
|
let email_id = Uuid::new_v4();
|
||||||
|
|
||||||
let file = self.path(&email_id, "eml");
|
let file = self.path(&email_id, "eml");
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(?file, "writing email to");
|
||||||
fs::write(file, email).map_err(error::io)?;
|
fs::write(file, email).map_err(error::io)?;
|
||||||
|
|
||||||
#[cfg(feature = "file-transport-envelope")]
|
#[cfg(feature = "file-transport-envelope")]
|
||||||
@@ -300,6 +308,8 @@ where
|
|||||||
let email_id = Uuid::new_v4();
|
let email_id = Uuid::new_v4();
|
||||||
|
|
||||||
let file = self.inner.path(&email_id, "eml");
|
let file = self.inner.path(&email_id, "eml");
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(?file, "writing email to");
|
||||||
E::fs_write(&file, email).await.map_err(error::io)?;
|
E::fs_write(&file, email).await.map_err(error::io)?;
|
||||||
|
|
||||||
#[cfg(feature = "file-transport-envelope")]
|
#[cfg(feature = "file-transport-envelope")]
|
||||||
|
|||||||
@@ -56,8 +56,7 @@
|
|||||||
//! #
|
//! #
|
||||||
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
|
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
|
||||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||||
//! use lettre::transport::smtp::authentication::Credentials;
|
//! use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||||
//! use lettre::{Message, SmtpTransport, Transport};
|
|
||||||
//!
|
//!
|
||||||
//! let email = Message::builder()
|
//! let email = Message::builder()
|
||||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||||
@@ -66,7 +65,7 @@
|
|||||||
//! .subject("Happy new year")
|
//! .subject("Happy new year")
|
||||||
//! .body(String::from("Be happy!"))?;
|
//! .body(String::from("Be happy!"))?;
|
||||||
//!
|
//!
|
||||||
//! let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
//! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||||
//!
|
//!
|
||||||
//! // Open a remote connection to the SMTP relay server
|
//! // Open a remote connection to the SMTP relay server
|
||||||
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
|
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
|
||||||
@@ -128,6 +127,9 @@ pub trait Transport {
|
|||||||
#[cfg(feature = "builder")]
|
#[cfg(feature = "builder")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||||
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::trace!("starting to send an email");
|
||||||
|
|
||||||
let raw = message.formatted();
|
let raw = message.formatted();
|
||||||
self.send_raw(message.envelope(), &raw)
|
self.send_raw(message.envelope(), &raw)
|
||||||
}
|
}
|
||||||
@@ -150,6 +152,9 @@ pub trait AsyncTransport {
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||||
// TODO take &Message
|
// TODO take &Message
|
||||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::trace!("starting to send an email");
|
||||||
|
|
||||||
let raw = message.formatted();
|
let raw = message.formatted();
|
||||||
let envelope = message.envelope();
|
let envelope = message.envelope();
|
||||||
self.send_raw(envelope, &raw).await
|
self.send_raw(envelope, &raw).await
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! Error and result type for sendmail transport
|
//! Error and result type for sendmail transport
|
||||||
|
|
||||||
use crate::BoxError;
|
|
||||||
use std::{error::Error as StdError, fmt};
|
use std::{error::Error as StdError, fmt};
|
||||||
|
|
||||||
|
use crate::BoxError;
|
||||||
|
|
||||||
/// The Errors that may occur when sending an email over sendmail
|
/// The Errors that may occur when sending an email over sendmail
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
inner: Box<Inner>,
|
inner: Box<Inner>,
|
||||||
@@ -67,7 +68,7 @@ impl fmt::Display for Error {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref e) = self.inner.source {
|
if let Some(ref e) = self.inner.source {
|
||||||
write!(f, ": {}", e)?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -33,7 +33,9 @@
|
|||||||
//! #
|
//! #
|
||||||
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
|
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
|
||||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||||
//! use lettre::{Message, AsyncTransport, Tokio1Executor, AsyncSendmailTransport, SendmailTransport};
|
//! use lettre::{
|
||||||
|
//! AsyncSendmailTransport, AsyncTransport, Message, SendmailTransport, Tokio1Executor,
|
||||||
|
//! };
|
||||||
//!
|
//!
|
||||||
//! let email = Message::builder()
|
//! let email = Message::builder()
|
||||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||||
@@ -72,6 +74,17 @@
|
|||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::{
|
||||||
|
ffi::OsString,
|
||||||
|
io::Write,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
pub use self::error::Error;
|
pub use self::error::Error;
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
use crate::AsyncStd1Executor;
|
use crate::AsyncStd1Executor;
|
||||||
@@ -80,15 +93,6 @@ use crate::Tokio1Executor;
|
|||||||
use crate::{address::Envelope, Transport};
|
use crate::{address::Envelope, Transport};
|
||||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||||
use crate::{AsyncTransport, Executor};
|
use crate::{AsyncTransport, Executor};
|
||||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
|
||||||
use async_trait::async_trait;
|
|
||||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::{
|
|
||||||
ffi::OsString,
|
|
||||||
io::Write,
|
|
||||||
process::{Command, Stdio},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
@@ -228,6 +232,9 @@ impl Transport for SendmailTransport {
|
|||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(command = ?self.command, "sending email with");
|
||||||
|
|
||||||
// Spawn the sendmail command
|
// Spawn the sendmail command
|
||||||
let mut process = self.command(envelope).spawn().map_err(error::client)?;
|
let mut process = self.command(envelope).spawn().map_err(error::client)?;
|
||||||
|
|
||||||
@@ -257,6 +264,9 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
|
|||||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||||
use async_std::io::prelude::WriteExt;
|
use async_std::io::prelude::WriteExt;
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(command = ?self.inner.command, "sending email with");
|
||||||
|
|
||||||
let mut command = self.async_std_command(envelope);
|
let mut command = self.async_std_command(envelope);
|
||||||
|
|
||||||
// Spawn the sendmail command
|
// Spawn the sendmail command
|
||||||
@@ -289,6 +299,9 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
|
|||||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||||
use tokio1_crate::io::AsyncWriteExt;
|
use tokio1_crate::io::AsyncWriteExt;
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(command = ?self.inner.command, "sending email with");
|
||||||
|
|
||||||
let mut command = self.tokio1_command(envelope);
|
let mut command = self.tokio1_command(envelope);
|
||||||
|
|
||||||
// Spawn the sendmail command
|
// Spawn the sendmail command
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
#[cfg(feature = "pool")]
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{self, Debug},
|
fmt::{self, Debug},
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,6 +199,9 @@ where
|
|||||||
{
|
{
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
#[cfg(feature = "pool")]
|
||||||
|
inner: Arc::clone(&self.inner),
|
||||||
|
#[cfg(not(feature = "pool"))]
|
||||||
inner: self.inner.clone(),
|
inner: self.inner.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! Provides limited SASL authentication mechanisms
|
//! Provides limited SASL authentication mechanisms
|
||||||
|
|
||||||
use crate::transport::smtp::error::{self, Error};
|
|
||||||
use std::fmt::{self, Debug, Display, Formatter};
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
|
|
||||||
|
use crate::transport::smtp::error::{self, Error};
|
||||||
|
|
||||||
/// Accepted authentication mechanisms
|
/// Accepted authentication mechanisms
|
||||||
///
|
///
|
||||||
/// Trying LOGIN last as it is deprecated.
|
/// Trying LOGIN last as it is deprecated.
|
||||||
@@ -97,12 +98,12 @@ impl Mechanism {
|
|||||||
let decoded_challenge = challenge
|
let decoded_challenge = challenge
|
||||||
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
|
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
|
||||||
|
|
||||||
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
if ["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
||||||
return Ok(credentials.authentication_identity.to_string());
|
return Ok(credentials.authentication_identity.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if vec!["Password", "Password:"].contains(&decoded_challenge) {
|
if ["Password", "Password:"].contains(&decoded_challenge) {
|
||||||
return Ok(credentials.secret.to_string());
|
return Ok(credentials.secret.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(error::client("Unrecognized challenge"))
|
Err(error::client("Unrecognized challenge"))
|
||||||
@@ -126,7 +127,7 @@ mod test {
|
|||||||
fn test_plain() {
|
fn test_plain() {
|
||||||
let mechanism = Mechanism::Plain;
|
let mechanism = Mechanism::Plain;
|
||||||
|
|
||||||
let credentials = Credentials::new("username".to_string(), "password".to_string());
|
let credentials = Credentials::new("username".to_owned(), "password".to_owned());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mechanism.response(&credentials, None).unwrap(),
|
mechanism.response(&credentials, None).unwrap(),
|
||||||
@@ -139,7 +140,7 @@ mod test {
|
|||||||
fn test_login() {
|
fn test_login() {
|
||||||
let mechanism = Mechanism::Login;
|
let mechanism = Mechanism::Login;
|
||||||
|
|
||||||
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
|
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
mechanism.response(&credentials, Some("Username")).unwrap(),
|
mechanism.response(&credentials, Some("Username")).unwrap(),
|
||||||
@@ -157,8 +158,8 @@ mod test {
|
|||||||
let mechanism = Mechanism::Xoauth2;
|
let mechanism = Mechanism::Xoauth2;
|
||||||
|
|
||||||
let credentials = Credentials::new(
|
let credentials = Credentials::new(
|
||||||
"username".to_string(),
|
"username".to_owned(),
|
||||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
|
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -171,7 +172,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_from_user_pass_for_credentials() {
|
fn test_from_user_pass_for_credentials() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Credentials::new("alice".to_string(), "wonderland".to_string()),
|
Credentials::new("alice".to_owned(), "wonderland".to_owned()),
|
||||||
Credentials::from(("alice", "wonderland"))
|
Credentials::from(("alice", "wonderland"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
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 super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||||
use crate::{
|
use crate::{
|
||||||
transport::smtp::{
|
transport::smtp::{
|
||||||
authentication::{Credentials, Mechanism},
|
authentication::{Credentials, Mechanism},
|
||||||
commands::*,
|
commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
|
||||||
error,
|
error,
|
||||||
error::Error,
|
error::Error,
|
||||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||||
@@ -10,11 +18,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
Envelope,
|
Envelope,
|
||||||
};
|
};
|
||||||
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
||||||
use std::{fmt::Display, time::Duration};
|
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
|
||||||
use super::escape_crlf;
|
|
||||||
|
|
||||||
macro_rules! try_smtp (
|
macro_rules! try_smtp (
|
||||||
($err: expr, $client: ident) => ({
|
($err: expr, $client: ident) => ({
|
||||||
@@ -40,10 +43,23 @@ pub struct AsyncSmtpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncSmtpConnection {
|
impl AsyncSmtpConnection {
|
||||||
|
/// Get information about the server
|
||||||
pub fn server_info(&self) -> &ServerInfo {
|
pub fn server_info(&self) -> &ServerInfo {
|
||||||
&self.server_info
|
&self.server_info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connects with existing async stream
|
||||||
|
///
|
||||||
|
/// Sends EHLO and parses server information
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
pub async fn connect_with_transport(
|
||||||
|
stream: Box<dyn AsyncTokioStream>,
|
||||||
|
hello_name: &ClientId,
|
||||||
|
) -> Result<AsyncSmtpConnection, Error> {
|
||||||
|
let stream = AsyncNetworkStream::use_existing_tokio1(stream);
|
||||||
|
Self::connect_impl(stream, hello_name).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Connects to the configured server
|
/// Connects to the configured server
|
||||||
///
|
///
|
||||||
/// Sends EHLO and parses server information
|
/// Sends EHLO and parses server information
|
||||||
@@ -53,8 +69,11 @@ impl AsyncSmtpConnection {
|
|||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
hello_name: &ClientId,
|
hello_name: &ClientId,
|
||||||
tls_parameters: Option<TlsParameters>,
|
tls_parameters: Option<TlsParameters>,
|
||||||
|
local_address: Option<IpAddr>,
|
||||||
) -> Result<AsyncSmtpConnection, Error> {
|
) -> Result<AsyncSmtpConnection, Error> {
|
||||||
let stream = AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters).await?;
|
let stream =
|
||||||
|
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
|
||||||
|
.await?;
|
||||||
Self::connect_impl(stream, hello_name).await
|
Self::connect_impl(stream, hello_name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +208,7 @@ impl AsyncSmtpConnection {
|
|||||||
self.panic = true;
|
self.panic = true;
|
||||||
let _ = self.command(Quit).await;
|
let _ = self.command(Quit).await;
|
||||||
}
|
}
|
||||||
|
let _ = self.stream.close().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the underlying stream
|
/// Sets the underlying stream
|
||||||
@@ -295,7 +315,10 @@ impl AsyncSmtpConnection {
|
|||||||
return if response.is_positive() {
|
return if response.is_positive() {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
} else {
|
} else {
|
||||||
Err(error::code(response.code()))
|
Err(error::code(
|
||||||
|
response.code(),
|
||||||
|
Some(response.message().collect()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(nom::Err::Failure(e)) => {
|
Err(nom::Err::Failure(e)) => {
|
||||||
@@ -310,4 +333,10 @@ impl AsyncSmtpConnection {
|
|||||||
|
|
||||||
Err(error::response("incomplete response"))
|
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,63 +1,85 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io, mem,
|
fmt, io, mem,
|
||||||
net::SocketAddr,
|
net::{IpAddr, SocketAddr},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[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::{
|
use futures_io::{
|
||||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
|
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
|
||||||
Result as IoResult,
|
Result as IoResult,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "tokio1")]
|
|
||||||
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
|
|
||||||
|
|
||||||
#[cfg(feature = "async-std1")]
|
|
||||||
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
|
|
||||||
#[cfg(feature = "tokio1")]
|
|
||||||
use tokio1_crate::net::{TcpStream as Tokio1TcpStream, ToSocketAddrs as Tokio1ToSocketAddrs};
|
|
||||||
|
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
|
||||||
use async_native_tls::TlsStream as AsyncStd1TlsStream;
|
|
||||||
#[cfg(feature = "tokio1-native-tls")]
|
|
||||||
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
|
|
||||||
|
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
#[cfg(feature = "async-std1-rustls-tls")]
|
||||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
use tokio1_boring::SslStream as Tokio1SslStream;
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
use tokio1_crate::io::{AsyncRead, AsyncWrite, ReadBuf as Tokio1ReadBuf};
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
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 = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
||||||
|
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
feature = "tokio1-native-tls",
|
feature = "tokio1-native-tls",
|
||||||
feature = "tokio1-rustls-tls",
|
feature = "tokio1-rustls-tls",
|
||||||
|
feature = "tokio1-boring-tls",
|
||||||
feature = "async-std1-native-tls",
|
feature = "async-std1-native-tls",
|
||||||
feature = "async-std1-rustls-tls"
|
feature = "async-std1-rustls-tls"
|
||||||
))]
|
))]
|
||||||
use super::InnerTlsParameters;
|
use super::InnerTlsParameters;
|
||||||
use super::TlsParameters;
|
use super::TlsParameters;
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
use crate::transport::smtp::client::net::resolved_address_filter;
|
||||||
use crate::transport::smtp::{error, Error};
|
use crate::transport::smtp::{error, Error};
|
||||||
|
|
||||||
/// A network stream
|
/// A network stream
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct AsyncNetworkStream {
|
pub struct AsyncNetworkStream {
|
||||||
inner: InnerAsyncNetworkStream,
|
inner: InnerAsyncNetworkStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
pub trait AsyncTokioStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + fmt::Debug {
|
||||||
|
fn peer_addr(&self) -> io::Result<SocketAddr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
impl AsyncTokioStream for Tokio1TcpStream {
|
||||||
|
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||||
|
self.peer_addr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents the different types of underlying network streams
|
/// Represents the different types of underlying network streams
|
||||||
// usually only one TLS backend at a time is going to be enabled,
|
// usually only one TLS backend at a time is going to be enabled,
|
||||||
// so clippy::large_enum_variant doesn't make sense here
|
// so clippy::large_enum_variant doesn't make sense here
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug)]
|
||||||
enum InnerAsyncNetworkStream {
|
enum InnerAsyncNetworkStream {
|
||||||
/// Plain Tokio 1.x TCP stream
|
/// Plain Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
Tokio1Tcp(Tokio1TcpStream),
|
Tokio1Tcp(Box<dyn AsyncTokioStream>),
|
||||||
/// Encrypted Tokio 1.x TCP stream
|
/// Encrypted Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "tokio1-native-tls")]
|
#[cfg(feature = "tokio1-native-tls")]
|
||||||
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>),
|
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
|
||||||
/// Encrypted Tokio 1.x TCP stream
|
/// Encrypted Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>),
|
Tokio1RustlsTls(Tokio1RustlsTlsStream<Box<dyn AsyncTokioStream>>),
|
||||||
|
/// Encrypted Tokio 1.x TCP stream
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
|
||||||
/// Plain Tokio 1.x TCP stream
|
/// Plain Tokio 1.x TCP stream
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
AsyncStd1Tcp(AsyncStd1TcpStream),
|
AsyncStd1Tcp(AsyncStd1TcpStream),
|
||||||
@@ -91,6 +113,8 @@ impl AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
InnerAsyncNetworkStream::Tokio1BoringTls(ref s) => s.get_ref().peer_addr(),
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-native-tls")]
|
||||||
@@ -107,50 +131,71 @@ impl AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
|
||||||
|
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tokio1")]
|
#[cfg(feature = "tokio1")]
|
||||||
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
|
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
|
||||||
server: T,
|
server: T,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
tls_parameters: Option<TlsParameters>,
|
tls_parameters: Option<TlsParameters>,
|
||||||
|
local_addr: Option<IpAddr>,
|
||||||
) -> Result<AsyncNetworkStream, Error> {
|
) -> Result<AsyncNetworkStream, Error> {
|
||||||
async fn try_connect_timeout<T: Tokio1ToSocketAddrs>(
|
async fn try_connect<T: Tokio1ToSocketAddrs>(
|
||||||
server: T,
|
server: T,
|
||||||
timeout: Duration,
|
timeout: Option<Duration>,
|
||||||
|
local_addr: Option<IpAddr>,
|
||||||
) -> Result<Tokio1TcpStream, Error> {
|
) -> Result<Tokio1TcpStream, Error> {
|
||||||
let addrs = tokio1_crate::net::lookup_host(server)
|
let addrs = tokio1_crate::net::lookup_host(server)
|
||||||
.await
|
.await
|
||||||
.map_err(error::connection)?;
|
.map_err(error::connection)?
|
||||||
|
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
|
||||||
|
|
||||||
let mut last_err = None;
|
let mut last_err = None;
|
||||||
|
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
let connect_future = Tokio1TcpStream::connect(&addr);
|
let socket = match addr.ip() {
|
||||||
match tokio1_crate::time::timeout(timeout, connect_future).await {
|
IpAddr::V4(_) => Tokio1TcpSocket::new_v4(),
|
||||||
Ok(Ok(stream)) => return Ok(stream),
|
IpAddr::V6(_) => Tokio1TcpSocket::new_v6(),
|
||||||
Ok(Err(err)) => last_err = Some(err),
|
}
|
||||||
Err(_) => {
|
.map_err(error::connection)?;
|
||||||
last_err = Some(io::Error::new(
|
if let Some(local_addr) = local_addr {
|
||||||
io::ErrorKind::TimedOut,
|
socket
|
||||||
"connection timed out",
|
.bind(SocketAddr::new(local_addr, 0))
|
||||||
))
|
.map_err(error::connection)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connect_future = socket.connect(addr);
|
||||||
|
if let Some(timeout) = timeout {
|
||||||
|
match tokio1_crate::time::timeout(timeout, connect_future).await {
|
||||||
|
Ok(Ok(stream)) => return Ok(stream),
|
||||||
|
Ok(Err(err)) => last_err = Some(err),
|
||||||
|
Err(_) => {
|
||||||
|
last_err = Some(io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
"connection timed out",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match connect_future.await {
|
||||||
|
Ok(stream) => return Ok(stream),
|
||||||
|
Err(err) => last_err = Some(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(match last_err {
|
Err(match last_err {
|
||||||
Some(last_err) => error::connection(last_err),
|
Some(last_err) => error::connection(last_err),
|
||||||
None => error::connection("could not resolve to any address"),
|
None => error::connection("could not resolve to any supported address"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let tcp_stream = match timeout {
|
let tcp_stream = try_connect(server, timeout, local_addr).await?;
|
||||||
Some(t) => try_connect_timeout(server, t).await?,
|
let mut stream =
|
||||||
None => Tokio1TcpStream::connect(server)
|
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
|
||||||
.await
|
|
||||||
.map_err(error::connection)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
|
|
||||||
if let Some(tls_parameters) = tls_parameters {
|
if let Some(tls_parameters) = tls_parameters {
|
||||||
stream.upgrade_tls(tls_parameters).await?;
|
stream.upgrade_tls(tls_parameters).await?;
|
||||||
}
|
}
|
||||||
@@ -163,6 +208,9 @@ impl AsyncNetworkStream {
|
|||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
tls_parameters: Option<TlsParameters>,
|
tls_parameters: Option<TlsParameters>,
|
||||||
) -> Result<AsyncNetworkStream, Error> {
|
) -> Result<AsyncNetworkStream, Error> {
|
||||||
|
// Unfortunately there doesn't currently seem to be a way to set the local address
|
||||||
|
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
|
||||||
|
// connected which is a blocking operation.
|
||||||
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
|
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
|
||||||
server: T,
|
server: T,
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
@@ -209,14 +257,22 @@ impl AsyncNetworkStream {
|
|||||||
match &self.inner {
|
match &self.inner {
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
feature = "tokio1",
|
feature = "tokio1",
|
||||||
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))
|
not(any(
|
||||||
|
feature = "tokio1-native-tls",
|
||||||
|
feature = "tokio1-rustls-tls",
|
||||||
|
feature = "tokio1-boring-tls"
|
||||||
|
))
|
||||||
))]
|
))]
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||||
let _ = tls_parameters;
|
let _ = tls_parameters;
|
||||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
|
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
#[cfg(any(
|
||||||
|
feature = "tokio1-native-tls",
|
||||||
|
feature = "tokio1-rustls-tls",
|
||||||
|
feature = "tokio1-boring-tls"
|
||||||
|
))]
|
||||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||||
// get owned TcpStream
|
// get owned TcpStream
|
||||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||||
@@ -258,12 +314,16 @@ impl AsyncNetworkStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
#[cfg(any(
|
||||||
|
feature = "tokio1-native-tls",
|
||||||
|
feature = "tokio1-rustls-tls",
|
||||||
|
feature = "tokio1-boring-tls"
|
||||||
|
))]
|
||||||
async fn upgrade_tokio1_tls(
|
async fn upgrade_tokio1_tls(
|
||||||
tcp_stream: Tokio1TcpStream,
|
tcp_stream: Box<dyn AsyncTokioStream>,
|
||||||
tls_parameters: TlsParameters,
|
tls_parameters: TlsParameters,
|
||||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||||
let domain = tls_parameters.domain().to_string();
|
let domain = tls_parameters.domain().to_owned();
|
||||||
|
|
||||||
match tls_parameters.connector {
|
match tls_parameters.connector {
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
@@ -290,8 +350,6 @@ impl AsyncNetworkStream {
|
|||||||
|
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
return {
|
return {
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
use rustls::ServerName;
|
use rustls::ServerName;
|
||||||
use tokio1_rustls::TlsConnector;
|
use tokio1_rustls::TlsConnector;
|
||||||
|
|
||||||
@@ -306,11 +364,31 @@ impl AsyncNetworkStream {
|
|||||||
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerTlsParameters::BoringTls(connector) => {
|
||||||
|
#[cfg(not(feature = "tokio1-boring-tls"))]
|
||||||
|
panic!("built without the tokio1-boring-tls feature");
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
return {
|
||||||
|
let mut config = connector.configure().map_err(error::connection)?;
|
||||||
|
config.set_verify_hostname(tls_parameters.accept_invalid_hostnames);
|
||||||
|
|
||||||
|
let stream = tokio1_boring::connect(config, &domain, tcp_stream)
|
||||||
|
.await
|
||||||
|
.map_err(error::connection)?;
|
||||||
|
Ok(InnerAsyncNetworkStream::Tokio1BoringTls(stream))
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
#[cfg(any(
|
||||||
|
feature = "async-std1-native-tls",
|
||||||
|
feature = "async-std1-rustls-tls",
|
||||||
|
feature = "async-std1-boring-tls"
|
||||||
|
))]
|
||||||
async fn upgrade_asyncstd1_tls(
|
async fn upgrade_asyncstd1_tls(
|
||||||
tcp_stream: AsyncStd1TcpStream,
|
tcp_stream: AsyncStd1TcpStream,
|
||||||
mut tls_parameters: TlsParameters,
|
mut tls_parameters: TlsParameters,
|
||||||
@@ -345,8 +423,6 @@ impl AsyncNetworkStream {
|
|||||||
|
|
||||||
#[cfg(feature = "async-std1-rustls-tls")]
|
#[cfg(feature = "async-std1-rustls-tls")]
|
||||||
return {
|
return {
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
use futures_rustls::TlsConnector;
|
use futures_rustls::TlsConnector;
|
||||||
use rustls::ServerName;
|
use rustls::ServerName;
|
||||||
|
|
||||||
@@ -361,6 +437,10 @@ impl AsyncNetworkStream {
|
|||||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerTlsParameters::BoringTls(connector) => {
|
||||||
|
panic!("boring-tls isn't supported with async-std yet.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +452,8 @@ impl AsyncNetworkStream {
|
|||||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
|
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
|
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
|
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-native-tls")]
|
||||||
@@ -381,6 +463,57 @@ impl AsyncNetworkStream {
|
|||||||
InnerAsyncNetworkStream::None => false,
|
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()
|
||||||
|
.clone()
|
||||||
|
.0),
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
||||||
|
.ssl()
|
||||||
|
.peer_certificate()
|
||||||
|
.unwrap()
|
||||||
|
.to_der()
|
||||||
|
.map_err(error::tls)?),
|
||||||
|
#[cfg(feature = "async-std1")]
|
||||||
|
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()
|
||||||
|
.clone()
|
||||||
|
.0),
|
||||||
|
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FuturesAsyncRead for AsyncNetworkStream {
|
impl FuturesAsyncRead for AsyncNetworkStream {
|
||||||
@@ -417,6 +550,15 @@ impl FuturesAsyncRead for AsyncNetworkStream {
|
|||||||
Poll::Pending => Poll::Pending,
|
Poll::Pending => Poll::Pending,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => {
|
||||||
|
let mut b = Tokio1ReadBuf::new(buf);
|
||||||
|
match Pin::new(s).poll_read(cx, &mut b) {
|
||||||
|
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||||
|
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-native-tls")]
|
||||||
@@ -448,6 +590,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
|||||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-native-tls")]
|
||||||
@@ -473,6 +617,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
|||||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-native-tls")]
|
||||||
@@ -494,6 +640,8 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
|
|||||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||||
#[cfg(feature = "tokio1-rustls-tls")]
|
#[cfg(feature = "tokio1-rustls-tls")]
|
||||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||||
|
#[cfg(feature = "tokio1-boring-tls")]
|
||||||
|
InnerAsyncNetworkStream::Tokio1BoringTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||||
#[cfg(feature = "async-std1")]
|
#[cfg(feature = "async-std1")]
|
||||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
|
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
|
||||||
#[cfg(feature = "async-std1-native-tls")]
|
#[cfg(feature = "async-std1-native-tls")]
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
io::{self, BufRead, BufReader, Write},
|
io::{self, BufRead, BufReader, Write},
|
||||||
net::ToSocketAddrs,
|
net::{IpAddr, ToSocketAddrs},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
use super::escape_crlf;
|
||||||
use super::{ClientCodec, NetworkStream, TlsParameters};
|
use super::{ClientCodec, NetworkStream, TlsParameters};
|
||||||
use crate::{
|
use crate::{
|
||||||
address::Envelope,
|
address::Envelope,
|
||||||
transport::smtp::{
|
transport::smtp::{
|
||||||
authentication::{Credentials, Mechanism},
|
authentication::{Credentials, Mechanism},
|
||||||
commands::*,
|
commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
|
||||||
error,
|
error,
|
||||||
error::Error,
|
error::Error,
|
||||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||||
@@ -18,9 +20,6 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "tracing")]
|
|
||||||
use super::escape_crlf;
|
|
||||||
|
|
||||||
macro_rules! try_smtp (
|
macro_rules! try_smtp (
|
||||||
($err: expr, $client: ident) => ({
|
($err: expr, $client: ident) => ({
|
||||||
match $err {
|
match $err {
|
||||||
@@ -45,6 +44,7 @@ pub struct SmtpConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SmtpConnection {
|
impl SmtpConnection {
|
||||||
|
/// Get information about the server
|
||||||
pub fn server_info(&self) -> &ServerInfo {
|
pub fn server_info(&self) -> &ServerInfo {
|
||||||
&self.server_info
|
&self.server_info
|
||||||
}
|
}
|
||||||
@@ -59,8 +59,9 @@ impl SmtpConnection {
|
|||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
hello_name: &ClientId,
|
hello_name: &ClientId,
|
||||||
tls_parameters: Option<&TlsParameters>,
|
tls_parameters: Option<&TlsParameters>,
|
||||||
|
local_address: Option<IpAddr>,
|
||||||
) -> Result<SmtpConnection, Error> {
|
) -> Result<SmtpConnection, Error> {
|
||||||
let stream = NetworkStream::connect(server, timeout, tls_parameters)?;
|
let stream = NetworkStream::connect(server, timeout, tls_parameters, local_address)?;
|
||||||
let stream = BufReader::new(stream);
|
let stream = BufReader::new(stream);
|
||||||
let mut conn = SmtpConnection {
|
let mut conn = SmtpConnection {
|
||||||
stream,
|
stream,
|
||||||
@@ -142,7 +143,7 @@ impl SmtpConnection {
|
|||||||
hello_name: &ClientId,
|
hello_name: &ClientId,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if self.server_info.supports_feature(Extension::StartTls) {
|
if self.server_info.supports_feature(Extension::StartTls) {
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
{
|
{
|
||||||
try_smtp!(self.command(Starttls), self);
|
try_smtp!(self.command(Starttls), self);
|
||||||
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
||||||
@@ -152,7 +153,11 @@ impl SmtpConnection {
|
|||||||
try_smtp!(self.ehlo(hello_name), self);
|
try_smtp!(self.ehlo(hello_name), self);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
|
#[cfg(not(any(
|
||||||
|
feature = "native-tls",
|
||||||
|
feature = "rustls-tls",
|
||||||
|
feature = "boring-tls"
|
||||||
|
)))]
|
||||||
// This should never happen as `Tls` can only be created
|
// This should never happen as `Tls` can only be created
|
||||||
// when a TLS library is enabled
|
// when a TLS library is enabled
|
||||||
unreachable!("TLS support required but not supported");
|
unreachable!("TLS support required but not supported");
|
||||||
@@ -178,6 +183,7 @@ impl SmtpConnection {
|
|||||||
self.panic = true;
|
self.panic = true;
|
||||||
let _ = self.command(Quit);
|
let _ = self.command(Quit);
|
||||||
}
|
}
|
||||||
|
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the underlying stream
|
/// Sets the underlying stream
|
||||||
@@ -237,11 +243,12 @@ impl SmtpConnection {
|
|||||||
|
|
||||||
/// Sends the message content
|
/// Sends the message content
|
||||||
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
||||||
let mut out_buf: Vec<u8> = vec![];
|
|
||||||
let mut codec = ClientCodec::new();
|
let mut codec = ClientCodec::new();
|
||||||
|
let mut out_buf = Vec::with_capacity(message.len());
|
||||||
codec.encode(message, &mut out_buf);
|
codec.encode(message, &mut out_buf);
|
||||||
self.write(out_buf.as_slice())?;
|
self.write(out_buf.as_slice())?;
|
||||||
self.write(b"\r\n.\r\n")?;
|
self.write(b"\r\n.\r\n")?;
|
||||||
|
|
||||||
self.read_response()
|
self.read_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +283,10 @@ impl SmtpConnection {
|
|||||||
return if response.is_positive() {
|
return if response.is_positive() {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
} else {
|
} else {
|
||||||
Err(error::code(response.code()))
|
Err(error::code(
|
||||||
|
response.code(),
|
||||||
|
Some(response.message().collect()),
|
||||||
|
))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Err(nom::Err::Failure(e)) => {
|
Err(nom::Err::Failure(e)) => {
|
||||||
@@ -291,4 +301,10 @@ impl SmtpConnection {
|
|||||||
|
|
||||||
Err(error::response("incomplete response"))
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,14 @@
|
|||||||
//!
|
//!
|
||||||
//! # #[cfg(feature = "smtp-transport")]
|
//! # #[cfg(feature = "smtp-transport")]
|
||||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
//! # 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 hello = ClientId::Domain("my_hostname".to_owned());
|
||||||
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None)?;
|
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?;
|
||||||
//! client.command(
|
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
|
||||||
//! Mail::new(Some("user@example.com".parse()?), vec![])
|
//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
|
||||||
//! )?;
|
|
||||||
//! client.command(
|
|
||||||
//! Rcpt::new("user@example.org".parse()?, vec![])
|
|
||||||
//! )?;
|
|
||||||
//! client.command(Data)?;
|
//! client.command(Data)?;
|
||||||
//! client.message("Test email".as_bytes())?;
|
//! client.message("Test email".as_bytes())?;
|
||||||
//! client.command(Quit)?;
|
//! client.command(Quit)?;
|
||||||
@@ -28,15 +26,19 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
pub(crate) use self::async_connection::AsyncSmtpConnection;
|
pub use self::async_connection::AsyncSmtpConnection;
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
pub(crate) use self::async_net::AsyncNetworkStream;
|
pub use self::async_net::AsyncNetworkStream;
|
||||||
|
#[cfg(feature = "tokio1")]
|
||||||
|
pub use self::async_net::AsyncTokioStream;
|
||||||
use self::net::NetworkStream;
|
use self::net::NetworkStream;
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
pub(super) use self::tls::InnerTlsParameters;
|
pub(super) use self::tls::InnerTlsParameters;
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
|
pub use self::tls::TlsVersion;
|
||||||
pub use self::{
|
pub use self::{
|
||||||
connection::SmtpConnection,
|
connection::SmtpConnection,
|
||||||
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
|
tls::{Certificate, CertificateStore, Tls, TlsParameters, TlsParametersBuilder},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
@@ -48,60 +50,57 @@ mod net;
|
|||||||
mod tls;
|
mod tls;
|
||||||
|
|
||||||
/// The codec used for transparency
|
/// The codec used for transparency
|
||||||
#[derive(Default, Clone, Copy, Debug)]
|
#[derive(Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
||||||
struct ClientCodec {
|
struct ClientCodec {
|
||||||
escape_count: u8,
|
status: CodecStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientCodec {
|
impl ClientCodec {
|
||||||
/// Creates a new client codec
|
/// Creates a new client codec
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
ClientCodec::default()
|
Self {
|
||||||
|
status: CodecStatus::StartOfNewLine,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds transparency
|
/// Adds transparency
|
||||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
|
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
|
||||||
match frame.len() {
|
for &b in frame {
|
||||||
0 => {
|
buf.push(b);
|
||||||
match self.escape_count {
|
match (b, self.status) {
|
||||||
0 => buf.extend_from_slice(b"\r\n.\r\n"),
|
(b'\r', _) => {
|
||||||
1 => buf.extend_from_slice(b"\n.\r\n"),
|
self.status = CodecStatus::StartingNewLine;
|
||||||
2 => buf.extend_from_slice(b".\r\n"),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
}
|
||||||
self.escape_count = 0;
|
(b'\n', CodecStatus::StartingNewLine) => {
|
||||||
}
|
self.status = CodecStatus::StartOfNewLine;
|
||||||
_ => {
|
|
||||||
let mut start = 0;
|
|
||||||
for (idx, byte) in frame.iter().enumerate() {
|
|
||||||
match self.escape_count {
|
|
||||||
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
|
|
||||||
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
|
|
||||||
2 => {
|
|
||||||
self.escape_count = if *byte == b'.' {
|
|
||||||
3
|
|
||||||
} else if *byte == b'\r' {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
if self.escape_count == 3 {
|
|
||||||
self.escape_count = 0;
|
|
||||||
buf.extend_from_slice(&frame[start..idx]);
|
|
||||||
buf.extend_from_slice(b".");
|
|
||||||
start = idx;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
buf.extend_from_slice(&frame[start..]);
|
(_, CodecStatus::StartingNewLine) => {
|
||||||
|
self.status = CodecStatus::MiddleOfLine;
|
||||||
|
}
|
||||||
|
(b'.', CodecStatus::StartOfNewLine) => {
|
||||||
|
self.status = CodecStatus::MiddleOfLine;
|
||||||
|
buf.push(b'.');
|
||||||
|
}
|
||||||
|
(_, CodecStatus::StartOfNewLine) => {
|
||||||
|
self.status = CodecStatus::MiddleOfLine;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
|
enum CodecStatus {
|
||||||
|
/// We are past the first character of the current line
|
||||||
|
MiddleOfLine,
|
||||||
|
/// We just read a `\r` character
|
||||||
|
StartingNewLine,
|
||||||
|
/// We are at the start of a new line
|
||||||
|
StartOfNewLine,
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||||
/// Used for debug displays
|
/// Used for debug displays
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
@@ -115,9 +114,10 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_codec() {
|
fn test_codec() {
|
||||||
|
let mut buf = Vec::new();
|
||||||
let mut codec = ClientCodec::new();
|
let mut codec = ClientCodec::new();
|
||||||
let mut buf: Vec<u8> = vec![];
|
|
||||||
|
|
||||||
|
codec.encode(b".\r\n", &mut buf);
|
||||||
codec.encode(b"test\r\n", &mut buf);
|
codec.encode(b"test\r\n", &mut buf);
|
||||||
codec.encode(b"test\r\n\r\n", &mut buf);
|
codec.encode(b"test\r\n\r\n", &mut buf);
|
||||||
codec.encode(b".\r\n", &mut buf);
|
codec.encode(b".\r\n", &mut buf);
|
||||||
@@ -128,9 +128,13 @@ mod test {
|
|||||||
codec.encode(b"test\n", &mut buf);
|
codec.encode(b"test\n", &mut buf);
|
||||||
codec.encode(b".test\n", &mut buf);
|
codec.encode(b".test\n", &mut buf);
|
||||||
codec.encode(b"test", &mut buf);
|
codec.encode(b"test", &mut buf);
|
||||||
|
codec.encode(b"test", &mut buf);
|
||||||
|
codec.encode(b"test\r\n", &mut buf);
|
||||||
|
codec.encode(b".test\r\n", &mut buf);
|
||||||
|
codec.encode(b"test.\r\n", &mut buf);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8(buf).unwrap(),
|
String::from_utf8(buf).unwrap(),
|
||||||
"test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
|
"..\r\ntest\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntesttesttest\r\n..test\r\ntest.\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
use std::convert::TryFrom;
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
mem,
|
mem,
|
||||||
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
use boring::ssl::SslStream;
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
use native_tls::TlsStream;
|
use native_tls::TlsStream;
|
||||||
|
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
use rustls::{ClientConnection, ServerName, StreamOwned};
|
use rustls::{ClientConnection, ServerName, StreamOwned};
|
||||||
|
use socket2::{Domain, Protocol, Type};
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
use super::InnerTlsParameters;
|
use super::InnerTlsParameters;
|
||||||
use super::TlsParameters;
|
use super::TlsParameters;
|
||||||
use crate::transport::smtp::{error, Error};
|
use crate::transport::smtp::{error, Error};
|
||||||
@@ -36,6 +38,8 @@ enum InnerNetworkStream {
|
|||||||
/// Encrypted TCP stream
|
/// Encrypted TCP stream
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
|
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
BoringTls(SslStream<TcpStream>),
|
||||||
/// Can't be built
|
/// Can't be built
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
@@ -57,6 +61,8 @@ impl NetworkStream {
|
|||||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
|
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
|
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(ref s) => s.get_ref().peer_addr(),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(SocketAddr::V4(SocketAddrV4::new(
|
Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||||
@@ -75,6 +81,8 @@ impl NetworkStream {
|
|||||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
|
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
|
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(ref s) => s.get_ref().shutdown(how),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -86,19 +94,39 @@ impl NetworkStream {
|
|||||||
server: T,
|
server: T,
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
tls_parameters: Option<&TlsParameters>,
|
tls_parameters: Option<&TlsParameters>,
|
||||||
|
local_addr: Option<IpAddr>,
|
||||||
) -> Result<NetworkStream, Error> {
|
) -> Result<NetworkStream, Error> {
|
||||||
fn try_connect_timeout<T: ToSocketAddrs>(
|
fn try_connect<T: ToSocketAddrs>(
|
||||||
server: T,
|
server: T,
|
||||||
timeout: Duration,
|
timeout: Option<Duration>,
|
||||||
|
local_addr: Option<IpAddr>,
|
||||||
) -> Result<TcpStream, Error> {
|
) -> Result<TcpStream, Error> {
|
||||||
let addrs = server.to_socket_addrs().map_err(error::connection)?;
|
let addrs = server
|
||||||
|
.to_socket_addrs()
|
||||||
|
.map_err(error::connection)?
|
||||||
|
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
|
||||||
|
|
||||||
let mut last_err = None;
|
let mut last_err = None;
|
||||||
|
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
match TcpStream::connect_timeout(&addr, timeout) {
|
let socket = socket2::Socket::new(
|
||||||
Ok(stream) => return Ok(stream),
|
Domain::for_address(addr),
|
||||||
Err(err) => last_err = Some(err),
|
Type::STREAM,
|
||||||
|
Some(Protocol::TCP),
|
||||||
|
)
|
||||||
|
.map_err(error::connection)?;
|
||||||
|
bind_local_address(&socket, &addr, local_addr)?;
|
||||||
|
|
||||||
|
if let Some(timeout) = timeout {
|
||||||
|
match socket.connect_timeout(&addr.into(), timeout) {
|
||||||
|
Ok(_) => return Ok(socket.into()),
|
||||||
|
Err(err) => last_err = Some(err),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match socket.connect(&addr.into()) {
|
||||||
|
Ok(_) => return Ok(socket.into()),
|
||||||
|
Err(err) => last_err = Some(err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,11 +136,7 @@ impl NetworkStream {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let tcp_stream = match timeout {
|
let tcp_stream = try_connect(server, timeout, local_addr)?;
|
||||||
Some(t) => try_connect_timeout(server, t)?,
|
|
||||||
None => TcpStream::connect(server).map_err(error::connection)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
|
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
|
||||||
if let Some(tls_parameters) = tls_parameters {
|
if let Some(tls_parameters) = tls_parameters {
|
||||||
stream.upgrade_tls(tls_parameters)?;
|
stream.upgrade_tls(tls_parameters)?;
|
||||||
@@ -122,13 +146,17 @@ impl NetworkStream {
|
|||||||
|
|
||||||
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
|
#[cfg(not(any(
|
||||||
|
feature = "native-tls",
|
||||||
|
feature = "rustls-tls",
|
||||||
|
feature = "boring-tls"
|
||||||
|
)))]
|
||||||
InnerNetworkStream::Tcp(_) => {
|
InnerNetworkStream::Tcp(_) => {
|
||||||
let _ = tls_parameters;
|
let _ = tls_parameters;
|
||||||
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
|
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
InnerNetworkStream::Tcp(_) => {
|
InnerNetworkStream::Tcp(_) => {
|
||||||
// get owned TcpStream
|
// get owned TcpStream
|
||||||
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
||||||
@@ -144,7 +172,7 @@ impl NetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
fn upgrade_tls_impl(
|
fn upgrade_tls_impl(
|
||||||
tcp_stream: TcpStream,
|
tcp_stream: TcpStream,
|
||||||
tls_parameters: &TlsParameters,
|
tls_parameters: &TlsParameters,
|
||||||
@@ -161,11 +189,21 @@ impl NetworkStream {
|
|||||||
InnerTlsParameters::RustlsTls(connector) => {
|
InnerTlsParameters::RustlsTls(connector) => {
|
||||||
let domain = ServerName::try_from(tls_parameters.domain())
|
let domain = ServerName::try_from(tls_parameters.domain())
|
||||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||||
let connection =
|
let connection = ClientConnection::new(Arc::clone(connector), domain)
|
||||||
ClientConnection::new(connector.clone(), domain).map_err(error::connection)?;
|
.map_err(error::connection)?;
|
||||||
let stream = StreamOwned::new(connection, tcp_stream);
|
let stream = StreamOwned::new(connection, tcp_stream);
|
||||||
InnerNetworkStream::RustlsTls(stream)
|
InnerNetworkStream::RustlsTls(stream)
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerTlsParameters::BoringTls(connector) => {
|
||||||
|
let stream = connector
|
||||||
|
.configure()
|
||||||
|
.map_err(error::connection)?
|
||||||
|
.verify_hostname(tls_parameters.accept_invalid_hostnames)
|
||||||
|
.connect(tls_parameters.domain(), tcp_stream)
|
||||||
|
.map_err(error::connection)?;
|
||||||
|
InnerNetworkStream::BoringTls(stream)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +214,8 @@ impl NetworkStream {
|
|||||||
InnerNetworkStream::NativeTls(_) => true,
|
InnerNetworkStream::NativeTls(_) => true,
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
InnerNetworkStream::RustlsTls(_) => true,
|
InnerNetworkStream::RustlsTls(_) => true,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(_) => true,
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
false
|
false
|
||||||
@@ -183,6 +223,37 @@ impl NetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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()
|
||||||
|
.clone()
|
||||||
|
.0),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
||||||
|
.ssl()
|
||||||
|
.peer_certificate()
|
||||||
|
.unwrap()
|
||||||
|
.to_der()
|
||||||
|
.map_err(error::tls)?),
|
||||||
|
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||||
match self.inner {
|
match self.inner {
|
||||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||||
@@ -194,6 +265,10 @@ impl NetworkStream {
|
|||||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||||
stream.get_ref().set_read_timeout(duration)
|
stream.get_ref().set_read_timeout(duration)
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(ref mut stream) => {
|
||||||
|
stream.get_ref().set_read_timeout(duration)
|
||||||
|
}
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -214,7 +289,10 @@ impl NetworkStream {
|
|||||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||||
stream.get_ref().set_write_timeout(duration)
|
stream.get_ref().set_write_timeout(duration)
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(ref mut stream) => {
|
||||||
|
stream.get_ref().set_write_timeout(duration)
|
||||||
|
}
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -231,6 +309,8 @@ impl Read for NetworkStream {
|
|||||||
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
|
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
|
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(ref mut s) => s.read(buf),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(0)
|
Ok(0)
|
||||||
@@ -247,6 +327,8 @@ impl Write for NetworkStream {
|
|||||||
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
|
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
|
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(ref mut s) => s.write(buf),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(0)
|
Ok(0)
|
||||||
@@ -261,6 +343,8 @@ impl Write for NetworkStream {
|
|||||||
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
|
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
|
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
InnerNetworkStream::BoringTls(ref mut s) => s.flush(),
|
||||||
InnerNetworkStream::None => {
|
InnerNetworkStream::None => {
|
||||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -268,3 +352,47 @@ impl Write for NetworkStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If the local address is set, binds the socket to this address.
|
||||||
|
/// If local address is not set, then destination address is required to determine to 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,21 +1,58 @@
|
|||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
use std::fmt::{self, Debug};
|
||||||
use crate::transport::smtp::{error, Error};
|
#[cfg(feature = "rustls-tls")]
|
||||||
|
use std::{sync::Arc, time::SystemTime};
|
||||||
|
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
use boring::{
|
||||||
|
ssl::{SslConnector, SslVersion},
|
||||||
|
x509::store::X509StoreBuilder,
|
||||||
|
};
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
use native_tls::{Protocol, TlsConnector};
|
use native_tls::{Protocol, TlsConnector};
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
use rustls::{
|
use rustls::{
|
||||||
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
|
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
|
||||||
ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName,
|
ClientConfig, Error as TlsError, RootCertStore, ServerName,
|
||||||
};
|
};
|
||||||
use std::fmt::{self, Debug};
|
|
||||||
#[cfg(feature = "rustls-tls")]
|
|
||||||
use std::{sync::Arc, time::SystemTime};
|
|
||||||
|
|
||||||
/// Accepted protocols by default.
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
use crate::transport::smtp::{error, Error};
|
||||||
// This is also rustls' default behavior
|
|
||||||
#[cfg(feature = "native-tls")]
|
/// TLS protocol versions.
|
||||||
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
|
#[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, altough not supported by all SMTP servers.
|
||||||
|
///
|
||||||
|
/// Altough it is technically supported by all TLS backends,
|
||||||
|
/// trying to set it for `native-tls` will give a runtime error.
|
||||||
|
Tlsv13,
|
||||||
|
}
|
||||||
|
|
||||||
/// How to apply TLS to a client connection
|
/// How to apply TLS to a client connection
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -24,16 +61,25 @@ pub enum Tls {
|
|||||||
/// Insecure connection only (for testing purposes)
|
/// Insecure connection only (for testing purposes)
|
||||||
None,
|
None,
|
||||||
/// Start with insecure connection and use `STARTTLS` when available
|
/// Start with insecure connection and use `STARTTLS` when available
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
Opportunistic(TlsParameters),
|
Opportunistic(TlsParameters),
|
||||||
/// Start with insecure connection and require `STARTTLS`
|
/// Start with insecure connection and require `STARTTLS`
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
Required(TlsParameters),
|
Required(TlsParameters),
|
||||||
/// Use TLS wrapped connection
|
/// Use TLS wrapped connection
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
Wrapper(TlsParameters),
|
Wrapper(TlsParameters),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,31 +87,65 @@ impl Debug for Tls {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
Self::None => f.pad("None"),
|
Self::None => f.pad("None"),
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
Self::Opportunistic(_) => f.pad("Opportunistic"),
|
Self::Opportunistic(_) => f.pad("Opportunistic"),
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
Self::Required(_) => f.pad("Required"),
|
Self::Required(_) => f.pad("Required"),
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||||
Self::Wrapper(_) => f.pad("Wrapper"),
|
Self::Wrapper(_) => f.pad("Wrapper"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Source for the base set of root certificates to trust.
|
||||||
|
#[allow(missing_copy_implementations)]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
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,
|
||||||
|
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
|
||||||
|
///
|
||||||
|
/// This option is only available in the rustls backend.
|
||||||
|
#[cfg(feature = "webpki-roots")]
|
||||||
|
WebpkiRoots,
|
||||||
|
/// Don't use any system certificates.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CertificateStore {
|
||||||
|
fn default() -> Self {
|
||||||
|
CertificateStore::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parameters to use for secure clients
|
/// Parameters to use for secure clients
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TlsParameters {
|
pub struct TlsParameters {
|
||||||
pub(crate) connector: InnerTlsParameters,
|
pub(crate) connector: InnerTlsParameters,
|
||||||
/// The domain name which is expected in the TLS certificate from the server
|
/// The domain name which is expected in the TLS certificate from the server
|
||||||
pub(super) domain: String,
|
pub(super) domain: String,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
pub(super) accept_invalid_hostnames: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for `TlsParameters`
|
/// Builder for `TlsParameters`
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TlsParametersBuilder {
|
pub struct TlsParametersBuilder {
|
||||||
domain: String,
|
domain: String,
|
||||||
|
cert_store: CertificateStore,
|
||||||
root_certs: Vec<Certificate>,
|
root_certs: Vec<Certificate>,
|
||||||
accept_invalid_hostnames: bool,
|
accept_invalid_hostnames: bool,
|
||||||
accept_invalid_certs: bool,
|
accept_invalid_certs: bool,
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
|
min_tls_version: TlsVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TlsParametersBuilder {
|
impl TlsParametersBuilder {
|
||||||
@@ -73,12 +153,21 @@ impl TlsParametersBuilder {
|
|||||||
pub fn new(domain: String) -> Self {
|
pub fn new(domain: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
domain,
|
domain,
|
||||||
|
cert_store: CertificateStore::Default,
|
||||||
root_certs: Vec::new(),
|
root_certs: Vec::new(),
|
||||||
accept_invalid_hostnames: false,
|
accept_invalid_hostnames: false,
|
||||||
accept_invalid_certs: false,
|
accept_invalid_certs: false,
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
|
min_tls_version: TlsVersion::Tlsv12,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the source for the base set of root certificates to trust.
|
||||||
|
pub fn certificate_store(mut self, cert_store: CertificateStore) -> Self {
|
||||||
|
self.cert_store = cert_store;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a custom root certificate
|
/// Add a custom root certificate
|
||||||
///
|
///
|
||||||
/// Can be used to safely connect to a server using a self signed certificate, for example.
|
/// Can be used to safely connect to a server using a self signed certificate, for example.
|
||||||
@@ -100,13 +189,22 @@ impl TlsParametersBuilder {
|
|||||||
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
||||||
///
|
///
|
||||||
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
|
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(any(feature = "native-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "boring-tls"))))]
|
||||||
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
|
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
|
||||||
self.accept_invalid_hostnames = accept_invalid_hostnames;
|
self.accept_invalid_hostnames = accept_invalid_hostnames;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controls which minimum TLS version is allowed
|
||||||
|
///
|
||||||
|
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
|
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
|
||||||
|
self.min_tls_version = min_tls_version;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Controls whether invalid certificates are accepted
|
/// Controls whether invalid certificates are accepted
|
||||||
///
|
///
|
||||||
/// Defaults to `false`.
|
/// Defaults to `false`.
|
||||||
@@ -128,16 +226,20 @@ impl TlsParametersBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
/// Creates a new `TlsParameters` using native-tls, boring-tls or rustls
|
||||||
/// depending on which one is available
|
/// depending on which one is available
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
pub fn build(self) -> Result<TlsParameters, Error> {
|
pub fn build(self) -> Result<TlsParameters, Error> {
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
return self.build_rustls();
|
return self.build_rustls();
|
||||||
|
#[cfg(all(not(feature = "rustls-tls"), feature = "native-tls"))]
|
||||||
#[cfg(not(feature = "rustls-tls"))]
|
|
||||||
return self.build_native();
|
return self.build_native();
|
||||||
|
#[cfg(all(not(feature = "rustls-tls"), feature = "boring-tls"))]
|
||||||
|
return self.build_boring();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new `TlsParameters` using native-tls with the provided configuration
|
/// Creates a new `TlsParameters` using native-tls with the provided configuration
|
||||||
@@ -146,17 +248,93 @@ impl TlsParametersBuilder {
|
|||||||
pub fn build_native(self) -> Result<TlsParameters, Error> {
|
pub fn build_native(self) -> Result<TlsParameters, Error> {
|
||||||
let mut tls_builder = TlsConnector::builder();
|
let mut tls_builder = TlsConnector::builder();
|
||||||
|
|
||||||
|
match self.cert_store {
|
||||||
|
CertificateStore::Default => {}
|
||||||
|
CertificateStore::None => {
|
||||||
|
tls_builder.disable_built_in_roots(true);
|
||||||
|
}
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
other => {
|
||||||
|
return Err(error::tls(format!(
|
||||||
|
"{other:?} is not supported in native tls"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
for cert in self.root_certs {
|
for cert in self.root_certs {
|
||||||
tls_builder.add_root_certificate(cert.native_tls);
|
tls_builder.add_root_certificate(cert.native_tls);
|
||||||
}
|
}
|
||||||
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
|
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
|
||||||
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
|
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
|
||||||
|
|
||||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
|
let min_tls_version = match self.min_tls_version {
|
||||||
|
TlsVersion::Tlsv10 => Protocol::Tlsv10,
|
||||||
|
TlsVersion::Tlsv11 => Protocol::Tlsv11,
|
||||||
|
TlsVersion::Tlsv12 => Protocol::Tlsv12,
|
||||||
|
TlsVersion::Tlsv13 => {
|
||||||
|
return Err(error::tls(
|
||||||
|
"min tls version Tlsv13 not supported in native tls",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tls_builder.min_protocol_version(Some(min_tls_version));
|
||||||
let connector = tls_builder.build().map_err(error::tls)?;
|
let connector = tls_builder.build().map_err(error::tls)?;
|
||||||
Ok(TlsParameters {
|
Ok(TlsParameters {
|
||||||
connector: InnerTlsParameters::NativeTls(connector),
|
connector: InnerTlsParameters::NativeTls(connector),
|
||||||
domain: self.domain,
|
domain: self.domain,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `TlsParameters` using boring-tls with the provided configuration
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||||
|
pub fn build_boring(self) -> Result<TlsParameters, Error> {
|
||||||
|
use boring::ssl::{SslMethod, SslVerifyMode};
|
||||||
|
|
||||||
|
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
|
||||||
|
|
||||||
|
if self.accept_invalid_certs {
|
||||||
|
tls_builder.set_verify(SslVerifyMode::NONE);
|
||||||
|
} else {
|
||||||
|
match self.cert_store {
|
||||||
|
CertificateStore::Default => {}
|
||||||
|
CertificateStore::None => {
|
||||||
|
// Replace the default store with an empty store.
|
||||||
|
tls_builder
|
||||||
|
.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
|
||||||
|
}
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
other => {
|
||||||
|
return Err(error::tls(format!(
|
||||||
|
"{other:?} is not supported in boring tls"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cert_store = tls_builder.cert_store_mut();
|
||||||
|
|
||||||
|
for cert in self.root_certs {
|
||||||
|
cert_store.add_cert(cert.boring_tls).map_err(error::tls)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_tls_version = match self.min_tls_version {
|
||||||
|
TlsVersion::Tlsv10 => SslVersion::TLS1,
|
||||||
|
TlsVersion::Tlsv11 => SslVersion::TLS1_1,
|
||||||
|
TlsVersion::Tlsv12 => SslVersion::TLS1_2,
|
||||||
|
TlsVersion::Tlsv13 => SslVersion::TLS1_3,
|
||||||
|
};
|
||||||
|
|
||||||
|
tls_builder
|
||||||
|
.set_min_proto_version(Some(min_tls_version))
|
||||||
|
.map_err(error::tls)?;
|
||||||
|
let connector = tls_builder.build();
|
||||||
|
Ok(TlsParameters {
|
||||||
|
connector: InnerTlsParameters::BoringTls(connector),
|
||||||
|
domain: self.domain,
|
||||||
|
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,26 +343,81 @@ impl TlsParametersBuilder {
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||||
let tls = ClientConfig::builder();
|
let tls = ClientConfig::builder();
|
||||||
let tls = tls.with_safe_defaults();
|
|
||||||
|
let just_version3 = &[&rustls::version::TLS13];
|
||||||
|
let supported_versions = match self.min_tls_version {
|
||||||
|
TlsVersion::Tlsv10 => {
|
||||||
|
return Err(error::tls("min tls version Tlsv10 not supported in rustls"))
|
||||||
|
}
|
||||||
|
TlsVersion::Tlsv11 => {
|
||||||
|
return Err(error::tls("min tls version Tlsv11 not supported in rustls"))
|
||||||
|
}
|
||||||
|
TlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
|
||||||
|
TlsVersion::Tlsv13 => just_version3,
|
||||||
|
};
|
||||||
|
|
||||||
|
let tls = tls
|
||||||
|
.with_safe_default_cipher_suites()
|
||||||
|
.with_safe_default_kx_groups()
|
||||||
|
.with_protocol_versions(supported_versions)
|
||||||
|
.map_err(error::tls)?;
|
||||||
|
|
||||||
let tls = if self.accept_invalid_certs {
|
let tls = if self.accept_invalid_certs {
|
||||||
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
|
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
|
||||||
} else {
|
} else {
|
||||||
let mut root_cert_store = RootCertStore::empty();
|
let mut root_cert_store = RootCertStore::empty();
|
||||||
|
|
||||||
|
#[cfg(feature = "rustls-native-certs")]
|
||||||
|
fn load_native_roots(store: &mut RootCertStore) -> Result<(), Error> {
|
||||||
|
let native_certs = rustls_native_certs::load_native_certs().map_err(error::tls)?;
|
||||||
|
let mut valid_count = 0;
|
||||||
|
let mut invalid_count = 0;
|
||||||
|
for cert in native_certs {
|
||||||
|
match store.add(&rustls::Certificate(cert.0)) {
|
||||||
|
Ok(_) => valid_count += 1,
|
||||||
|
Err(err) => {
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!("certificate parsing failed: {:?}", err);
|
||||||
|
invalid_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "tracing")]
|
||||||
|
tracing::debug!(
|
||||||
|
"loaded platform certs with {valid_count} valid and {invalid_count} invalid certs"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "webpki-roots")]
|
||||||
|
fn load_webpki_roots(store: &mut RootCertStore) {
|
||||||
|
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
|
||||||
|
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
|
||||||
|
ta.subject,
|
||||||
|
ta.spki,
|
||||||
|
ta.name_constraints,
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.cert_store {
|
||||||
|
CertificateStore::Default => {
|
||||||
|
#[cfg(feature = "rustls-native-certs")]
|
||||||
|
load_native_roots(&mut root_cert_store)?;
|
||||||
|
#[cfg(all(not(feature = "rustls-native-certs"), feature = "webpki-roots"))]
|
||||||
|
load_webpki_roots(&mut root_cert_store);
|
||||||
|
}
|
||||||
|
#[cfg(feature = "webpki-roots")]
|
||||||
|
CertificateStore::WebpkiRoots => {
|
||||||
|
load_webpki_roots(&mut root_cert_store);
|
||||||
|
}
|
||||||
|
CertificateStore::None => {}
|
||||||
|
}
|
||||||
for cert in self.root_certs {
|
for cert in self.root_certs {
|
||||||
for rustls_cert in cert.rustls {
|
for rustls_cert in cert.rustls {
|
||||||
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
|
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root_cert_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
|
|
||||||
|ta| {
|
|
||||||
OwnedTrustAnchor::from_subject_spki_name_constraints(
|
|
||||||
ta.subject,
|
|
||||||
ta.spki,
|
|
||||||
ta.name_constraints,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
|
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
|
||||||
root_cert_store,
|
root_cert_store,
|
||||||
@@ -196,27 +429,36 @@ impl TlsParametersBuilder {
|
|||||||
Ok(TlsParameters {
|
Ok(TlsParameters {
|
||||||
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
||||||
domain: self.domain,
|
domain: self.domain,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
accept_invalid_hostnames: self.accept_invalid_hostnames,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub enum InnerTlsParameters {
|
pub enum InnerTlsParameters {
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
NativeTls(TlsConnector),
|
NativeTls(TlsConnector),
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
RustlsTls(Arc<ClientConfig>),
|
RustlsTls(Arc<ClientConfig>),
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
BoringTls(SslConnector),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TlsParameters {
|
impl TlsParameters {
|
||||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
/// Creates a new `TlsParameters` using native-tls or rustls
|
||||||
/// depending on which one is available
|
/// depending on which one is available
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
pub fn new(domain: String) -> Result<Self, Error> {
|
pub fn new(domain: String) -> Result<Self, Error> {
|
||||||
TlsParametersBuilder::new(domain).build()
|
TlsParametersBuilder::new(domain).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new `TlsParameters` builder
|
||||||
pub fn builder(domain: String) -> TlsParametersBuilder {
|
pub fn builder(domain: String) -> TlsParametersBuilder {
|
||||||
TlsParametersBuilder::new(domain)
|
TlsParametersBuilder::new(domain)
|
||||||
}
|
}
|
||||||
@@ -235,6 +477,13 @@ impl TlsParameters {
|
|||||||
TlsParametersBuilder::new(domain).build_rustls()
|
TlsParametersBuilder::new(domain).build_rustls()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new `TlsParameters` using boring
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||||
|
pub fn new_boring(domain: String) -> Result<Self, Error> {
|
||||||
|
TlsParametersBuilder::new(domain).build_boring()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn domain(&self) -> &str {
|
pub fn domain(&self) -> &str {
|
||||||
&self.domain
|
&self.domain
|
||||||
}
|
}
|
||||||
@@ -248,20 +497,27 @@ pub struct Certificate {
|
|||||||
native_tls: native_tls::Certificate,
|
native_tls: native_tls::Certificate,
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
rustls: Vec<rustls::Certificate>,
|
rustls: Vec<rustls::Certificate>,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
boring_tls: boring::x509::X509,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
impl Certificate {
|
impl Certificate {
|
||||||
/// Create a `Certificate` from a DER encoded certificate
|
/// Create a `Certificate` from a DER encoded certificate
|
||||||
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
|
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
|
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
let boring_tls_cert = boring::x509::X509::from_der(&der).map_err(error::tls)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
native_tls: native_tls_cert,
|
native_tls: native_tls_cert,
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
rustls: vec![rustls::Certificate(der)],
|
rustls: vec![rustls::Certificate(der)],
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
boring_tls: boring_tls_cert,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +526,9 @@ impl Certificate {
|
|||||||
#[cfg(feature = "native-tls")]
|
#[cfg(feature = "native-tls")]
|
||||||
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
|
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
let boring_tls_cert = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
|
||||||
|
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
let rustls_cert = {
|
let rustls_cert = {
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
@@ -287,6 +546,8 @@ impl Certificate {
|
|||||||
native_tls: native_tls_cert,
|
native_tls: native_tls_cert,
|
||||||
#[cfg(feature = "rustls-tls")]
|
#[cfg(feature = "rustls-tls")]
|
||||||
rustls: rustls_cert,
|
rustls: rustls_cert,
|
||||||
|
#[cfg(feature = "boring-tls")]
|
||||||
|
boring_tls: boring_tls_cert,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
//! SMTP commands
|
//! SMTP commands
|
||||||
|
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
address::Address,
|
address::Address,
|
||||||
transport::smtp::{
|
transport::smtp::{
|
||||||
@@ -9,10 +11,9 @@ use crate::{
|
|||||||
response::Response,
|
response::Response,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::fmt::{self, Display, Formatter};
|
|
||||||
|
|
||||||
/// EHLO command
|
/// EHLO command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Ehlo {
|
pub struct Ehlo {
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
@@ -32,7 +33,7 @@ impl Ehlo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// STARTTLS command
|
/// STARTTLS command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Starttls;
|
pub struct Starttls;
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ impl Display for Starttls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// MAIL command
|
/// MAIL command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Mail {
|
pub struct Mail {
|
||||||
sender: Option<Address>,
|
sender: Option<Address>,
|
||||||
@@ -58,7 +59,7 @@ impl Display for Mail {
|
|||||||
self.sender.as_ref().map_or("", |s| s.as_ref())
|
self.sender.as_ref().map_or("", |s| s.as_ref())
|
||||||
)?;
|
)?;
|
||||||
for parameter in &self.parameters {
|
for parameter in &self.parameters {
|
||||||
write!(f, " {}", parameter)?;
|
write!(f, " {parameter}")?;
|
||||||
}
|
}
|
||||||
f.write_str("\r\n")
|
f.write_str("\r\n")
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,7 @@ impl Mail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// RCPT command
|
/// RCPT command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Rcpt {
|
pub struct Rcpt {
|
||||||
recipient: Address,
|
recipient: Address,
|
||||||
@@ -83,7 +84,7 @@ impl Display for Rcpt {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||||
for parameter in &self.parameters {
|
for parameter in &self.parameters {
|
||||||
write!(f, " {}", parameter)?;
|
write!(f, " {parameter}")?;
|
||||||
}
|
}
|
||||||
f.write_str("\r\n")
|
f.write_str("\r\n")
|
||||||
}
|
}
|
||||||
@@ -100,7 +101,7 @@ impl Rcpt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// DATA command
|
/// DATA command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Data;
|
pub struct Data;
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ impl Display for Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// QUIT command
|
/// QUIT command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Quit;
|
pub struct Quit;
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ impl Display for Quit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// NOOP command
|
/// NOOP command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Noop;
|
pub struct Noop;
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ impl Display for Noop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// HELP command
|
/// HELP command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Help {
|
pub struct Help {
|
||||||
argument: Option<String>,
|
argument: Option<String>,
|
||||||
@@ -143,7 +144,7 @@ impl Display for Help {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str("HELP")?;
|
f.write_str("HELP")?;
|
||||||
if let Some(argument) = &self.argument {
|
if let Some(argument) = &self.argument {
|
||||||
write!(f, " {}", argument)?;
|
write!(f, " {argument}")?;
|
||||||
}
|
}
|
||||||
f.write_str("\r\n")
|
f.write_str("\r\n")
|
||||||
}
|
}
|
||||||
@@ -157,7 +158,7 @@ impl Help {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// VRFY command
|
/// VRFY command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Vrfy {
|
pub struct Vrfy {
|
||||||
argument: String,
|
argument: String,
|
||||||
@@ -177,7 +178,7 @@ impl Vrfy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// EXPN command
|
/// EXPN command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Expn {
|
pub struct Expn {
|
||||||
argument: String,
|
argument: String,
|
||||||
@@ -197,7 +198,7 @@ impl Expn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// RSET command
|
/// RSET command
|
||||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Rset;
|
pub struct Rset;
|
||||||
|
|
||||||
@@ -208,7 +209,7 @@ impl Display for Rset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// AUTH command
|
/// AUTH command
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Auth {
|
pub struct Auth {
|
||||||
mechanism: Mechanism,
|
mechanism: Mechanism,
|
||||||
@@ -219,7 +220,7 @@ pub struct Auth {
|
|||||||
|
|
||||||
impl Display for Auth {
|
impl Display for Auth {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
let encoded_response = self.response.as_ref().map(base64::encode);
|
let encoded_response = self.response.as_ref().map(crate::base64::encode);
|
||||||
|
|
||||||
if self.mechanism.supports_initial_response() {
|
if self.mechanism.supports_initial_response() {
|
||||||
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
||||||
@@ -270,7 +271,7 @@ impl Auth {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
||||||
|
|
||||||
let decoded_base64 = base64::decode(&encoded_challenge).map_err(error::response)?;
|
let decoded_base64 = crate::base64::decode(encoded_challenge).map_err(error::response)?;
|
||||||
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
|
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
||||||
@@ -288,21 +289,22 @@ impl Auth {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::transport::smtp::extension::MailBodyParameter;
|
use crate::transport::smtp::extension::MailBodyParameter;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
let id = ClientId::Domain("localhost".to_string());
|
let id = ClientId::Domain("localhost".to_owned());
|
||||||
let email = Address::from_str("test@example.com").unwrap();
|
let email = Address::from_str("test@example.com").unwrap();
|
||||||
let mail_parameter = MailParameter::Other {
|
let mail_parameter = MailParameter::Other {
|
||||||
keyword: "TEST".to_string(),
|
keyword: "TEST".to_owned(),
|
||||||
value: Some("value".to_string()),
|
value: Some("value".to_owned()),
|
||||||
};
|
};
|
||||||
let rcpt_parameter = RcptParameter::Other {
|
let rcpt_parameter = RcptParameter::Other {
|
||||||
keyword: "TEST".to_string(),
|
keyword: "TEST".to_owned(),
|
||||||
value: Some("value".to_string()),
|
value: Some("value".to_owned()),
|
||||||
};
|
};
|
||||||
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
|
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -339,24 +341,18 @@ mod test {
|
|||||||
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
|
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
|
||||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(format!("{}", Quit), "QUIT\r\n");
|
assert_eq!(format!("{Quit}"), "QUIT\r\n");
|
||||||
assert_eq!(format!("{}", Data), "DATA\r\n");
|
assert_eq!(format!("{Data}"), "DATA\r\n");
|
||||||
assert_eq!(format!("{}", Noop), "NOOP\r\n");
|
assert_eq!(format!("{Noop}"), "NOOP\r\n");
|
||||||
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", Help::new(Some("test".to_string()))),
|
format!("{}", Help::new(Some("test".to_owned()))),
|
||||||
"HELP test\r\n"
|
"HELP test\r\n"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
|
||||||
format!("{}", Vrfy::new("test".to_string())),
|
assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\r\n");
|
||||||
"VRFY test\r\n"
|
assert_eq!(format!("{Rset}"), "RSET\r\n");
|
||||||
);
|
let credentials = Credentials::new("user".to_owned(), "password".to_owned());
|
||||||
assert_eq!(
|
|
||||||
format!("{}", Expn::new("test".to_string())),
|
|
||||||
"EXPN test\r\n"
|
|
||||||
);
|
|
||||||
assert_eq!(format!("{}", Rset), "RSET\r\n");
|
|
||||||
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
//! Error and result type for SMTP clients
|
//! Error and result type for SMTP clients
|
||||||
|
|
||||||
|
use std::{error::Error as StdError, fmt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
transport::smtp::response::{Code, Severity},
|
transport::smtp::response::{Code, Severity},
|
||||||
BoxError,
|
BoxError,
|
||||||
};
|
};
|
||||||
use std::{error::Error as StdError, fmt};
|
|
||||||
|
|
||||||
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
|
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
|
||||||
|
|
||||||
@@ -67,8 +68,11 @@ impl Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the error is from TLS
|
/// Returns true if the error is from TLS
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
pub fn is_tls(&self) -> bool {
|
pub fn is_tls(&self) -> bool {
|
||||||
matches!(self.inner.kind, Kind::Tls)
|
matches!(self.inner.kind, Kind::Tls)
|
||||||
}
|
}
|
||||||
@@ -101,8 +105,11 @@ pub(crate) enum Kind {
|
|||||||
/// Underlying network i/o error
|
/// Underlying network i/o error
|
||||||
Network,
|
Network,
|
||||||
/// TLS error
|
/// TLS error
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
Tls,
|
Tls,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,18 +134,18 @@ impl fmt::Display for Error {
|
|||||||
Kind::Client => f.write_str("internal client error")?,
|
Kind::Client => f.write_str("internal client error")?,
|
||||||
Kind::Network => f.write_str("network error")?,
|
Kind::Network => f.write_str("network error")?,
|
||||||
Kind::Connection => f.write_str("Connection error")?,
|
Kind::Connection => f.write_str("Connection error")?,
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
Kind::Tls => f.write_str("tls error")?,
|
Kind::Tls => f.write_str("tls error")?,
|
||||||
Kind::Transient(ref code) => {
|
Kind::Transient(ref code) => {
|
||||||
write!(f, "transient error ({})", code)?;
|
write!(f, "transient error ({code})")?;
|
||||||
}
|
}
|
||||||
Kind::Permanent(ref code) => {
|
Kind::Permanent(ref code) => {
|
||||||
write!(f, "permanent error ({})", code)?;
|
write!(f, "permanent error ({code})")?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref e) = self.inner.source {
|
if let Some(ref e) = self.inner.source {
|
||||||
write!(f, ": {}", e)?;
|
write!(f, ": {e}")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -154,10 +161,10 @@ impl StdError for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn code(c: Code) -> Error {
|
pub(crate) fn code(c: Code, s: Option<String>) -> Error {
|
||||||
match c.severity {
|
match c.severity {
|
||||||
Severity::TransientNegativeCompletion => Error::new::<Error>(Kind::Transient(c), None),
|
Severity::TransientNegativeCompletion => Error::new(Kind::Transient(c), s),
|
||||||
Severity::PermanentNegativeCompletion => Error::new::<Error>(Kind::Permanent(c), None),
|
Severity::PermanentNegativeCompletion => Error::new(Kind::Permanent(c), s),
|
||||||
_ => client("Unknown error code"),
|
_ => client("Unknown error code"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,7 +185,7 @@ pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
|
|||||||
Error::new(Kind::Connection, Some(e))
|
Error::new(Kind::Connection, Some(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
|
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
|
||||||
Error::new(Kind::Tls, Some(e))
|
Error::new(Kind::Tls, Some(e))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
//! ESMTP features
|
//! ESMTP features
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
fmt::{self, Display, Formatter},
|
||||||
|
net::{Ipv4Addr, Ipv6Addr},
|
||||||
|
result::Result,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::{
|
||||||
authentication::Mechanism,
|
authentication::Mechanism,
|
||||||
error::{self, Error},
|
error::{self, Error},
|
||||||
response::Response,
|
response::Response,
|
||||||
util::XText,
|
util::XText,
|
||||||
};
|
};
|
||||||
use std::{
|
|
||||||
collections::HashSet,
|
|
||||||
fmt::{self, Display, Formatter},
|
|
||||||
net::{Ipv4Addr, Ipv6Addr},
|
|
||||||
result::Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Client identifier, the parameter to `EHLO`
|
/// Client identifier, the parameter to `EHLO`
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
@@ -54,8 +55,8 @@ impl Display for ClientId {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
Self::Domain(ref value) => f.write_str(value),
|
Self::Domain(ref value) => f.write_str(value),
|
||||||
Self::Ipv4(ref value) => write!(f, "[{}]", value),
|
Self::Ipv4(ref value) => write!(f, "[{value}]"),
|
||||||
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value),
|
Self::Ipv6(ref value) => write!(f, "[IPv6:{value}]"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ impl Display for Extension {
|
|||||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||||
Extension::StartTls => f.write_str("STARTTLS"),
|
Extension::StartTls => f.write_str("STARTTLS"),
|
||||||
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
|
Extension::Authentication(ref mechanism) => write!(f, "AUTH {mechanism}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +119,7 @@ pub struct ServerInfo {
|
|||||||
impl Display for ServerInfo {
|
impl Display for ServerInfo {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
let features = if self.features.is_empty() {
|
let features = if self.features.is_empty() {
|
||||||
"no supported features".to_string()
|
"no supported features".to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("{:?}", self.features)
|
format!("{:?}", self.features)
|
||||||
};
|
};
|
||||||
@@ -173,7 +174,7 @@ impl ServerInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(ServerInfo {
|
Ok(ServerInfo {
|
||||||
name: name.to_string(),
|
name: name.to_owned(),
|
||||||
features,
|
features,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -227,8 +228,8 @@ pub enum MailParameter {
|
|||||||
impl Display for MailParameter {
|
impl Display for MailParameter {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
|
MailParameter::Body(ref value) => write!(f, "BODY={value}"),
|
||||||
MailParameter::Size(size) => write!(f, "SIZE={}", size),
|
MailParameter::Size(size) => write!(f, "SIZE={size}"),
|
||||||
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||||
MailParameter::Other {
|
MailParameter::Other {
|
||||||
ref keyword,
|
ref keyword,
|
||||||
@@ -292,31 +293,32 @@ impl Display for RcptParameter {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::{
|
||||||
authentication::Mechanism,
|
authentication::Mechanism,
|
||||||
response::{Category, Code, Detail, Response, Severity},
|
response::{Category, Code, Detail, Response, Severity},
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_clientid_fmt() {
|
fn test_clientid_fmt() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", ClientId::Domain("test".to_string())),
|
format!("{}", ClientId::Domain("test".to_owned())),
|
||||||
"test".to_string()
|
"test".to_owned()
|
||||||
);
|
);
|
||||||
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string());
|
assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extension_fmt() {
|
fn test_extension_fmt() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", Extension::EightBitMime),
|
format!("{}", Extension::EightBitMime),
|
||||||
"8BITMIME".to_string()
|
"8BITMIME".to_owned()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{}", Extension::Authentication(Mechanism::Plain)),
|
format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||||
"AUTH PLAIN".to_string()
|
"AUTH PLAIN".to_owned()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,11 +331,11 @@ mod test {
|
|||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
ServerInfo {
|
ServerInfo {
|
||||||
name: "name".to_string(),
|
name: "name".to_owned(),
|
||||||
features: eightbitmime,
|
features: eightbitmime,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"name with {EightBitMime}".to_string()
|
"name with {EightBitMime}".to_owned()
|
||||||
);
|
);
|
||||||
|
|
||||||
let empty = HashSet::new();
|
let empty = HashSet::new();
|
||||||
@@ -342,11 +344,11 @@ mod test {
|
|||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
ServerInfo {
|
ServerInfo {
|
||||||
name: "name".to_string(),
|
name: "name".to_owned(),
|
||||||
features: empty,
|
features: empty,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"name with no supported features".to_string()
|
"name with no supported features".to_owned()
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut plain = HashSet::new();
|
let mut plain = HashSet::new();
|
||||||
@@ -356,11 +358,11 @@ mod test {
|
|||||||
format!(
|
format!(
|
||||||
"{}",
|
"{}",
|
||||||
ServerInfo {
|
ServerInfo {
|
||||||
name: "name".to_string(),
|
name: "name".to_owned(),
|
||||||
features: plain,
|
features: plain,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
"name with {Authentication(Plain)}".to_string()
|
"name with {Authentication(Plain)}".to_owned()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,18 +374,14 @@ mod test {
|
|||||||
Category::Unspecified4,
|
Category::Unspecified4,
|
||||||
Detail::One,
|
Detail::One,
|
||||||
),
|
),
|
||||||
vec![
|
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
|
||||||
"me".to_string(),
|
|
||||||
"8BITMIME".to_string(),
|
|
||||||
"SIZE 42".to_string(),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut features = HashSet::new();
|
let mut features = HashSet::new();
|
||||||
assert!(features.insert(Extension::EightBitMime));
|
assert!(features.insert(Extension::EightBitMime));
|
||||||
|
|
||||||
let server_info = ServerInfo {
|
let server_info = ServerInfo {
|
||||||
name: "me".to_string(),
|
name: "me".to_owned(),
|
||||||
features,
|
features,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -399,10 +397,10 @@ mod test {
|
|||||||
Detail::One,
|
Detail::One,
|
||||||
),
|
),
|
||||||
vec![
|
vec![
|
||||||
"me".to_string(),
|
"me".to_owned(),
|
||||||
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
|
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
|
||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_owned(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_owned(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -412,7 +410,7 @@ mod test {
|
|||||||
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
|
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
|
||||||
|
|
||||||
let server_info2 = ServerInfo {
|
let server_info2 = ServerInfo {
|
||||||
name: "me".to_string(),
|
name: "me".to_owned(),
|
||||||
features: features2,
|
features: features2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
//! use lettre::{Message, Transport, SmtpTransport};
|
//! use lettre::{Message, SmtpTransport, Transport};
|
||||||
//!
|
//!
|
||||||
//! let email = Message::builder()
|
//! let email = Message::builder()
|
||||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||||
@@ -43,8 +43,7 @@
|
|||||||
//! .body(String::from("Be happy!"))?;
|
//! .body(String::from("Be happy!"))?;
|
||||||
//!
|
//!
|
||||||
//! // Create TLS transport on port 465
|
//! // Create TLS transport on port 465
|
||||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
//! let sender = SmtpTransport::relay("smtp.example.com")?.build();
|
||||||
//! .build();
|
|
||||||
//! // Send the email via remote relay
|
//! // Send the email via remote relay
|
||||||
//! let result = sender.send(&email);
|
//! let result = sender.send(&email);
|
||||||
//! assert!(result.is_ok());
|
//! assert!(result.is_ok());
|
||||||
@@ -59,7 +58,13 @@
|
|||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
//! # 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()
|
//! let email = Message::builder()
|
||||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||||
@@ -71,11 +76,14 @@
|
|||||||
//! // Create TLS transport on port 587 with STARTTLS
|
//! // Create TLS transport on port 587 with STARTTLS
|
||||||
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
|
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
|
||||||
//! // Add credentials for authentication
|
//! // Add credentials for authentication
|
||||||
//! .credentials(Credentials::new("username".to_string(), "password".to_string()))
|
//! .credentials(Credentials::new(
|
||||||
|
//! "username".to_owned(),
|
||||||
|
//! "password".to_owned(),
|
||||||
|
//! ))
|
||||||
//! // Configure expected authentication mechanism
|
//! // Configure expected authentication mechanism
|
||||||
//! .authentication(vec![Mechanism::Plain])
|
//! .authentication(vec![Mechanism::Plain])
|
||||||
//! // Connection pool settings
|
//! // Connection pool settings
|
||||||
//! .pool_config( PoolConfig::new().max_size(20))
|
//! .pool_config(PoolConfig::new().max_size(20))
|
||||||
//! .build();
|
//! .build();
|
||||||
//!
|
//!
|
||||||
//! // Send the email via remote relay
|
//! // Send the email via remote relay
|
||||||
@@ -90,7 +98,10 @@
|
|||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
//! # 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()
|
//! let email = Message::builder()
|
||||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||||
@@ -100,8 +111,9 @@
|
|||||||
//! .body(String::from("Be happy!"))?;
|
//! .body(String::from("Be happy!"))?;
|
||||||
//!
|
//!
|
||||||
//! // Custom TLS configuration
|
//! // Custom TLS configuration
|
||||||
//! let tls = TlsParameters::builder("smtp.example.com".to_string())
|
//! let tls = TlsParameters::builder("smtp.example.com".to_owned())
|
||||||
//! .dangerous_accept_invalid_certs(true).build()?;
|
//! .dangerous_accept_invalid_certs(true)
|
||||||
|
//! .build()?;
|
||||||
//!
|
//!
|
||||||
//! // Create TLS transport on port 465
|
//! // Create TLS transport on port 465
|
||||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||||
@@ -116,6 +128,10 @@
|
|||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use client::Tls;
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
|
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
|
||||||
#[cfg(feature = "pool")]
|
#[cfg(feature = "pool")]
|
||||||
@@ -124,7 +140,7 @@ pub use self::{
|
|||||||
error::Error,
|
error::Error,
|
||||||
transport::{SmtpTransport, SmtpTransportBuilder},
|
transport::{SmtpTransport, SmtpTransportBuilder},
|
||||||
};
|
};
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
use crate::transport::smtp::client::TlsParameters;
|
use crate::transport::smtp::client::TlsParameters;
|
||||||
use crate::transport::smtp::{
|
use crate::transport::smtp::{
|
||||||
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||||
@@ -132,8 +148,6 @@ use crate::transport::smtp::{
|
|||||||
extension::ClientId,
|
extension::ClientId,
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use client::Tls;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
mod async_transport;
|
mod async_transport;
|
||||||
@@ -186,7 +200,7 @@ struct SmtpInfo {
|
|||||||
impl Default for SmtpInfo {
|
impl Default for SmtpInfo {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
server: "localhost".to_string(),
|
server: "localhost".to_owned(),
|
||||||
port: SMTP_PORT,
|
port: SMTP_PORT,
|
||||||
hello_name: ClientId::default(),
|
hello_name: ClientId::default(),
|
||||||
credentials: None,
|
credentials: None,
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
use std::fmt::{self, Debug};
|
use std::{
|
||||||
use std::mem;
|
fmt::{self, Debug},
|
||||||
use std::ops::{Deref, DerefMut};
|
mem,
|
||||||
use std::sync::Arc;
|
ops::{Deref, DerefMut},
|
||||||
use std::time::{Duration, Instant};
|
sync::Arc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use futures_util::lock::Mutex;
|
use futures_util::{
|
||||||
use futures_util::stream::{self, StreamExt};
|
lock::Mutex,
|
||||||
|
stream::{self, StreamExt},
|
||||||
|
};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
use crate::executor::SpawnHandle;
|
use super::{
|
||||||
use crate::transport::smtp::async_transport::AsyncSmtpClient;
|
super::{client::AsyncSmtpConnection, Error},
|
||||||
use crate::Executor;
|
PoolConfig,
|
||||||
|
};
|
||||||
use super::super::client::AsyncSmtpConnection;
|
use crate::{executor::SpawnHandle, transport::smtp::async_transport::AsyncSmtpClient, Executor};
|
||||||
use super::super::Error;
|
|
||||||
use super::PoolConfig;
|
|
||||||
|
|
||||||
pub struct Pool<E: Executor> {
|
pub struct Pool<E: Executor> {
|
||||||
config: PoolConfig,
|
config: PoolConfig,
|
||||||
@@ -156,14 +158,14 @@ impl<E: Executor> Pool<E> {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("reusing a pooled connection");
|
tracing::debug!("reusing a pooled connection");
|
||||||
|
|
||||||
return Ok(PooledConnection::wrap(conn, self.clone()));
|
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("creating a new connection");
|
tracing::debug!("creating a new connection");
|
||||||
|
|
||||||
let conn = self.client.connection().await?;
|
let conn = self.client.connection().await?;
|
||||||
return Ok(PooledConnection::wrap(conn, self.clone()));
|
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +203,7 @@ impl<E: Executor> Debug for Pool<E> {
|
|||||||
&match self.connections.try_lock() {
|
&match self.connections.try_lock() {
|
||||||
Some(connections) => format!("{} connections", connections.len()),
|
Some(connections) => format!("{} connections", connections.len()),
|
||||||
|
|
||||||
None => "LOCKED".to_string(),
|
None => "LOCKED".to_owned(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.field("client", &self.client)
|
.field("client", &self.client)
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
use std::fmt::{self, Debug};
|
use std::{
|
||||||
use std::ops::{Deref, DerefMut};
|
fmt::{self, Debug},
|
||||||
use std::sync::{Arc, Mutex, TryLockError};
|
mem,
|
||||||
use std::time::{Duration, Instant};
|
ops::{Deref, DerefMut},
|
||||||
use std::{mem, thread};
|
sync::{Arc, Mutex, TryLockError},
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
super::{client::SmtpConnection, Error},
|
||||||
|
PoolConfig,
|
||||||
|
};
|
||||||
use crate::transport::smtp::transport::SmtpClient;
|
use crate::transport::smtp::transport::SmtpClient;
|
||||||
|
|
||||||
use super::super::client::SmtpConnection;
|
|
||||||
use super::super::Error;
|
|
||||||
use super::PoolConfig;
|
|
||||||
|
|
||||||
pub struct Pool {
|
pub struct Pool {
|
||||||
config: PoolConfig,
|
config: PoolConfig,
|
||||||
connections: Mutex<Vec<ParkedConnection>>,
|
connections: Mutex<Vec<ParkedConnection>>,
|
||||||
@@ -138,14 +141,14 @@ impl Pool {
|
|||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("reusing a pooled connection");
|
tracing::debug!("reusing a pooled connection");
|
||||||
|
|
||||||
return Ok(PooledConnection::wrap(conn, self.clone()));
|
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
#[cfg(feature = "tracing")]
|
#[cfg(feature = "tracing")]
|
||||||
tracing::debug!("creating a new connection");
|
tracing::debug!("creating a new connection");
|
||||||
|
|
||||||
let conn = self.client.connection()?;
|
let conn = self.client.connection()?;
|
||||||
return Ok(PooledConnection::wrap(conn, self.clone()));
|
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,8 +186,8 @@ impl Debug for Pool {
|
|||||||
&match self.connections.try_lock() {
|
&match self.connections.try_lock() {
|
||||||
Ok(connections) => format!("{} connections", connections.len()),
|
Ok(connections) => format!("{} connections", connections.len()),
|
||||||
|
|
||||||
Err(TryLockError::WouldBlock) => "LOCKED".to_string(),
|
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
|
||||||
Err(TryLockError::Poisoned(_)) => "POISONED".to_string(),
|
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.field("client", &self.client)
|
.field("client", &self.client)
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
//! SMTP response, containing a mandatory return code and an optional text
|
//! SMTP response, containing a mandatory return code and an optional text
|
||||||
//! message
|
//! message
|
||||||
|
|
||||||
use crate::transport::smtp::{error, Error};
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result},
|
||||||
|
result,
|
||||||
|
str::FromStr,
|
||||||
|
string::ToString,
|
||||||
|
};
|
||||||
|
|
||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
bytes::streaming::{tag, take_until},
|
bytes::streaming::{tag, take_until},
|
||||||
@@ -10,12 +16,8 @@ use nom::{
|
|||||||
sequence::{preceded, tuple},
|
sequence::{preceded, tuple},
|
||||||
IResult,
|
IResult,
|
||||||
};
|
};
|
||||||
use std::{
|
|
||||||
fmt::{Display, Formatter, Result},
|
use crate::transport::smtp::{error, Error};
|
||||||
result,
|
|
||||||
str::FromStr,
|
|
||||||
string::ToString,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// First digit indicates severity
|
/// First digit indicates severity
|
||||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
@@ -149,7 +151,7 @@ impl FromStr for Response {
|
|||||||
fn from_str(s: &str) -> result::Result<Response, Error> {
|
fn from_str(s: &str) -> result::Result<Response, Error> {
|
||||||
parse_response(s)
|
parse_response(s)
|
||||||
.map(|(_, r)| r)
|
.map(|(_, r)| r)
|
||||||
.map_err(|e| error::response(e.to_string()))
|
.map_err(|e| error::response(e.to_owned()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,10 +329,10 @@ mod test {
|
|||||||
detail: Detail::Zero,
|
detail: Detail::Zero,
|
||||||
},
|
},
|
||||||
message: vec![
|
message: vec![
|
||||||
"me".to_string(),
|
"me".to_owned(),
|
||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_owned(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_owned(),
|
||||||
"AUTH PLAIN CRAM-MD5".to_string(),
|
"AUTH PLAIN CRAM-MD5".to_owned(),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -350,11 +352,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::Zero,
|
detail: Detail::Zero,
|
||||||
},
|
},
|
||||||
vec![
|
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||||
"me".to_string(),
|
|
||||||
"8BITMIME".to_string(),
|
|
||||||
"SIZE 42".to_string(),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
.is_positive());
|
.is_positive());
|
||||||
assert!(!Response::new(
|
assert!(!Response::new(
|
||||||
@@ -363,11 +361,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::Zero,
|
detail: Detail::Zero,
|
||||||
},
|
},
|
||||||
vec![
|
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||||
"me".to_string(),
|
|
||||||
"8BITMIME".to_string(),
|
|
||||||
"SIZE 42".to_string(),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
.is_positive());
|
.is_positive());
|
||||||
}
|
}
|
||||||
@@ -380,11 +374,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![
|
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||||
"me".to_string(),
|
|
||||||
"8BITMIME".to_string(),
|
|
||||||
"SIZE 42".to_string(),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
.has_code(451));
|
.has_code(451));
|
||||||
assert!(!Response::new(
|
assert!(!Response::new(
|
||||||
@@ -393,11 +383,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![
|
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||||
"me".to_string(),
|
|
||||||
"8BITMIME".to_string(),
|
|
||||||
"SIZE 42".to_string(),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
.has_code(251));
|
.has_code(251));
|
||||||
}
|
}
|
||||||
@@ -411,11 +397,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![
|
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||||
"me".to_string(),
|
|
||||||
"8BITMIME".to_string(),
|
|
||||||
"SIZE 42".to_string(),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
.first_word(),
|
.first_word(),
|
||||||
Some("me")
|
Some("me")
|
||||||
@@ -428,9 +410,9 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![
|
vec![
|
||||||
"me mo".to_string(),
|
"me mo".to_owned(),
|
||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_owned(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_owned(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.first_word(),
|
.first_word(),
|
||||||
@@ -455,7 +437,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![" ".to_string()],
|
vec![" ".to_owned()],
|
||||||
)
|
)
|
||||||
.first_word(),
|
.first_word(),
|
||||||
None
|
None
|
||||||
@@ -467,7 +449,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![" ".to_string()],
|
vec![" ".to_owned()],
|
||||||
)
|
)
|
||||||
.first_word(),
|
.first_word(),
|
||||||
None
|
None
|
||||||
@@ -479,7 +461,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec!["".to_string()],
|
vec!["".to_owned()],
|
||||||
)
|
)
|
||||||
.first_word(),
|
.first_word(),
|
||||||
None
|
None
|
||||||
@@ -492,7 +474,7 @@ mod test {
|
|||||||
let res = parse_response(raw_response);
|
let res = parse_response(raw_response);
|
||||||
match res {
|
match res {
|
||||||
Err(nom::Err::Incomplete(_)) => {}
|
Err(nom::Err::Incomplete(_)) => {}
|
||||||
_ => panic!("Expected incomplete response, got {:?}", res),
|
_ => panic!("Expected incomplete response, got {res:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,11 +487,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![
|
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned(),],
|
||||||
"me".to_string(),
|
|
||||||
"8BITMIME".to_string(),
|
|
||||||
"SIZE 42".to_string(),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
.first_line(),
|
.first_line(),
|
||||||
Some("me")
|
Some("me")
|
||||||
@@ -522,9 +500,9 @@ mod test {
|
|||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![
|
vec![
|
||||||
"me mo".to_string(),
|
"me mo".to_owned(),
|
||||||
"8BITMIME".to_string(),
|
"8BITMIME".to_owned(),
|
||||||
"SIZE 42".to_string(),
|
"SIZE 42".to_owned(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.first_line(),
|
.first_line(),
|
||||||
@@ -549,7 +527,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![" ".to_string()],
|
vec![" ".to_owned()],
|
||||||
)
|
)
|
||||||
.first_line(),
|
.first_line(),
|
||||||
Some(" ")
|
Some(" ")
|
||||||
@@ -561,7 +539,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec![" ".to_string()],
|
vec![" ".to_owned()],
|
||||||
)
|
)
|
||||||
.first_line(),
|
.first_line(),
|
||||||
Some(" ")
|
Some(" ")
|
||||||
@@ -573,7 +551,7 @@ mod test {
|
|||||||
category: Category::MailSystem,
|
category: Category::MailSystem,
|
||||||
detail: Detail::One,
|
detail: Detail::One,
|
||||||
},
|
},
|
||||||
vec!["".to_string()],
|
vec!["".to_owned()],
|
||||||
)
|
)
|
||||||
.first_line(),
|
.first_line(),
|
||||||
Some("")
|
Some("")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use super::pool::sync_impl::Pool;
|
|||||||
#[cfg(feature = "pool")]
|
#[cfg(feature = "pool")]
|
||||||
use super::PoolConfig;
|
use super::PoolConfig;
|
||||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||||
use crate::{address::Envelope, Transport};
|
use crate::{address::Envelope, Transport};
|
||||||
|
|
||||||
@@ -45,8 +45,11 @@ impl SmtpTransport {
|
|||||||
///
|
///
|
||||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||||
/// to validate TLS certificates.
|
/// to validate TLS certificates.
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||||
|
|
||||||
@@ -66,8 +69,11 @@ impl SmtpTransport {
|
|||||||
///
|
///
|
||||||
/// An error is returned if the connection can't be upgraded. No credentials
|
/// An error is returned if the connection can't be upgraded. No credentials
|
||||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||||
|
|
||||||
@@ -166,8 +172,11 @@ impl SmtpTransportBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set the TLS settings to use
|
/// Set the TLS settings to use
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
#[cfg_attr(
|
||||||
|
docsrs,
|
||||||
|
doc(cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls")))
|
||||||
|
)]
|
||||||
pub fn tls(mut self, tls: Tls) -> Self {
|
pub fn tls(mut self, tls: Tls) -> Self {
|
||||||
self.info.tls = tls;
|
self.info.tls = tls;
|
||||||
self
|
self
|
||||||
@@ -210,7 +219,7 @@ impl SmtpClient {
|
|||||||
pub fn connection(&self) -> Result<SmtpConnection, Error> {
|
pub fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||||
#[allow(clippy::match_single_binding)]
|
#[allow(clippy::match_single_binding)]
|
||||||
let tls_parameters = match self.info.tls {
|
let tls_parameters = match self.info.tls {
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
@@ -221,9 +230,10 @@ impl SmtpClient {
|
|||||||
self.info.timeout,
|
self.info.timeout,
|
||||||
&self.info.hello_name,
|
&self.info.hello_name,
|
||||||
tls_parameters,
|
tls_parameters,
|
||||||
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
#[cfg(any(feature = "native-tls", feature = "rustls-tls", feature = "boring-tls"))]
|
||||||
match self.info.tls {
|
match self.info.tls {
|
||||||
Tls::Opportunistic(ref tls_parameters) => {
|
Tls::Opportunistic(ref tls_parameters) => {
|
||||||
if conn.can_starttls() {
|
if conn.can_starttls() {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ mod tests {
|
|||||||
]
|
]
|
||||||
.iter()
|
.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
|
//! The stub transport logs message envelopes as well as contents. It can be useful for testing
|
||||||
//! testing purposes.
|
//! purposes.
|
||||||
//!
|
//!
|
||||||
//! #### Stub Transport
|
//! # Stub Transport
|
||||||
//!
|
//!
|
||||||
//! The stub transport returns provided result and drops the content. It can be useful for
|
//! The stub transport logs message envelopes as well as contents. It can be useful for testing
|
||||||
//! testing purposes.
|
//! purposes.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
//!
|
//!
|
||||||
//! ```rust
|
//! ```rust
|
||||||
//! # #[cfg(feature = "builder")]
|
//! # #[cfg(feature = "builder")]
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
//! use lettre::{transport::stub::StubTransport, Message, Transport};
|
//! use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||||
//!
|
//!
|
||||||
//! # use std::error::Error;
|
//! # use std::error::Error;
|
||||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
//! # fn try_main() -> Result<(), Box<dyn Error>> {
|
||||||
//! let email = Message::builder()
|
//! let email = Message::builder()
|
||||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||||
@@ -23,18 +25,36 @@
|
|||||||
//! let mut sender = StubTransport::new_ok();
|
//! let mut sender = StubTransport::new_ok();
|
||||||
//! let result = sender.send(&email);
|
//! let result = sender.send(&email);
|
||||||
//! assert!(result.is_ok());
|
//! assert!(result.is_ok());
|
||||||
|
//! assert_eq!(
|
||||||
|
//! sender.messages(),
|
||||||
|
//! vec![(
|
||||||
|
//! email.envelope().clone(),
|
||||||
|
//! String::from_utf8(email.formatted()).unwrap()
|
||||||
|
//! )],
|
||||||
|
//! );
|
||||||
//! # Ok(())
|
//! # Ok(())
|
||||||
//! # }
|
//! # }
|
||||||
|
//! # try_main().unwrap();
|
||||||
//! # }
|
//! # }
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
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"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
use crate::AsyncTransport;
|
use crate::AsyncTransport;
|
||||||
use crate::{address::Envelope, Transport};
|
use crate::{address::Envelope, Transport};
|
||||||
#[cfg(any(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)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct Error;
|
pub struct Error;
|
||||||
|
|
||||||
@@ -46,47 +66,113 @@ impl fmt::Display for Error {
|
|||||||
|
|
||||||
impl StdError for Error {}
|
impl StdError for Error {}
|
||||||
|
|
||||||
/// This transport logs the message envelope and returns the given response
|
/// This transport logs messages and always returns the given response
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct StubTransport {
|
pub struct StubTransport {
|
||||||
response: Result<(), Error>,
|
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 {
|
impl StubTransport {
|
||||||
/// Creates a new transport that always returns the given Result
|
/// Creates a new transport that always returns the given Result
|
||||||
pub fn new(response: Result<(), Error>) -> StubTransport {
|
pub fn new(response: Result<(), Error>) -> Self {
|
||||||
StubTransport { response }
|
Self {
|
||||||
|
response,
|
||||||
|
message_log: Arc::new(StdMutex::new(vec![])),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new transport that always returns a success response
|
/// Creates a new transport that always returns a success response
|
||||||
pub fn new_ok() -> StubTransport {
|
pub fn new_ok() -> Self {
|
||||||
StubTransport { response: Ok(()) }
|
Self {
|
||||||
|
response: Ok(()),
|
||||||
|
message_log: Arc::new(StdMutex::new(vec![])),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new transport that always returns an error
|
/// Creates a new transport that always returns an error
|
||||||
pub fn new_error() -> StubTransport {
|
pub fn new_error() -> Self {
|
||||||
StubTransport {
|
Self {
|
||||||
response: Err(Error),
|
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 {
|
impl Transport for StubTransport {
|
||||||
type Ok = ();
|
type Ok = ();
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||||
|
self.message_log
|
||||||
|
.lock()
|
||||||
|
.expect("Couldn't acquire lock to write message log")
|
||||||
|
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
|
||||||
self.response
|
self.response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AsyncTransport for StubTransport {
|
impl AsyncTransport for AsyncStubTransport {
|
||||||
type Ok = ();
|
type Ok = ();
|
||||||
type Error = Error;
|
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
|
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"
|
||||||
4
testdata/email_with_png.eml
vendored
4
testdata/email_with_png.eml
vendored
@@ -1,10 +1,10 @@
|
|||||||
Date: Tue, 15 Nov 1994 08:12:31 -0000
|
Date: Tue, 15 Nov 1994 08:12:31 +0000
|
||||||
From: NoBody <nobody@domain.tld>
|
From: NoBody <nobody@domain.tld>
|
||||||
Reply-To: Yuin <yuin@domain.tld>
|
Reply-To: Yuin <yuin@domain.tld>
|
||||||
To: Hei <hei@domain.tld>
|
To: Hei <hei@domain.tld>
|
||||||
Subject: Happy new year
|
Subject: Happy new year
|
||||||
MIME-Version: 1.0
|
MIME-Version: 1.0
|
||||||
Content-Type: multipart/related;
|
Content-Type: multipart/related;
|
||||||
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
|
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
|
||||||
|
|
||||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ fn default_date() -> std::time::SystemTime {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(all(feature = "file-transport", feature = "builder"))]
|
#[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||||
mod sync {
|
mod sync {
|
||||||
use crate::default_date;
|
|
||||||
use lettre::{FileTransport, Message, Transport};
|
|
||||||
use std::{
|
use std::{
|
||||||
env::temp_dir,
|
env::temp_dir,
|
||||||
fs::{read_to_string, remove_file},
|
fs::{read_to_string, remove_file},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use lettre::{FileTransport, Message, Transport};
|
||||||
|
|
||||||
|
use crate::default_date;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn file_transport() {
|
fn file_transport() {
|
||||||
let sender = FileTransport::new(temp_dir());
|
let sender = FileTransport::new(temp_dir());
|
||||||
@@ -31,7 +33,7 @@ mod sync {
|
|||||||
let result = sender.send(&email);
|
let result = sender.send(&email);
|
||||||
let id = result.unwrap();
|
let id = result.unwrap();
|
||||||
|
|
||||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||||
let eml = read_to_string(&eml_file).unwrap();
|
let eml = read_to_string(&eml_file).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -41,7 +43,7 @@ mod sync {
|
|||||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||||
"To: Hei <hei@domain.tld>\r\n",
|
"To: Hei <hei@domain.tld>\r\n",
|
||||||
"Subject: Happy new year\r\n",
|
"Subject: Happy new year\r\n",
|
||||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"Be happy!"
|
"Be happy!"
|
||||||
@@ -66,10 +68,10 @@ mod sync {
|
|||||||
let result = sender.send(&email);
|
let result = sender.send(&email);
|
||||||
let id = result.unwrap();
|
let id = result.unwrap();
|
||||||
|
|
||||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||||
let eml = read_to_string(&eml_file).unwrap();
|
let eml = read_to_string(&eml_file).unwrap();
|
||||||
|
|
||||||
let json_file = temp_dir().join(format!("{}.json", id));
|
let json_file = temp_dir().join(format!("{id}.json"));
|
||||||
let json = read_to_string(&json_file).unwrap();
|
let json = read_to_string(&json_file).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -79,7 +81,7 @@ mod sync {
|
|||||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||||
"To: Hei <hei@domain.tld>\r\n",
|
"To: Hei <hei@domain.tld>\r\n",
|
||||||
"Subject: Happy new year\r\n",
|
"Subject: Happy new year\r\n",
|
||||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"Be happy!"
|
"Be happy!"
|
||||||
@@ -104,15 +106,16 @@ mod sync {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
|
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
|
||||||
mod tokio_1 {
|
mod tokio_1 {
|
||||||
use crate::default_date;
|
|
||||||
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
|
||||||
use std::{
|
use std::{
|
||||||
env::temp_dir,
|
env::temp_dir,
|
||||||
fs::{read_to_string, remove_file},
|
fs::{read_to_string, remove_file},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
||||||
use tokio1_crate as tokio;
|
use tokio1_crate as tokio;
|
||||||
|
|
||||||
|
use crate::default_date;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn file_transport_tokio1() {
|
async fn file_transport_tokio1() {
|
||||||
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||||
@@ -128,7 +131,7 @@ mod tokio_1 {
|
|||||||
let result = sender.send(email).await;
|
let result = sender.send(email).await;
|
||||||
let id = result.unwrap();
|
let id = result.unwrap();
|
||||||
|
|
||||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||||
let eml = read_to_string(&eml_file).unwrap();
|
let eml = read_to_string(&eml_file).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -138,7 +141,7 @@ mod tokio_1 {
|
|||||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||||
"To: Hei <hei@domain.tld>\r\n",
|
"To: Hei <hei@domain.tld>\r\n",
|
||||||
"Subject: Happy new year\r\n",
|
"Subject: Happy new year\r\n",
|
||||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"Be happy!"
|
"Be happy!"
|
||||||
@@ -155,13 +158,15 @@ mod tokio_1 {
|
|||||||
feature = "async-std1"
|
feature = "async-std1"
|
||||||
))]
|
))]
|
||||||
mod asyncstd_1 {
|
mod asyncstd_1 {
|
||||||
use crate::default_date;
|
|
||||||
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
|
||||||
use std::{
|
use std::{
|
||||||
env::temp_dir,
|
env::temp_dir,
|
||||||
fs::{read_to_string, remove_file},
|
fs::{read_to_string, remove_file},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||||
|
|
||||||
|
use crate::default_date;
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
async fn file_transport_asyncstd1() {
|
async fn file_transport_asyncstd1() {
|
||||||
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||||
@@ -177,7 +182,7 @@ mod asyncstd_1 {
|
|||||||
let result = sender.send(email).await;
|
let result = sender.send(email).await;
|
||||||
let id = result.unwrap();
|
let id = result.unwrap();
|
||||||
|
|
||||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
let eml_file = temp_dir().join(format!("{id}.eml"));
|
||||||
let eml = read_to_string(&eml_file).unwrap();
|
let eml = read_to_string(&eml_file).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -187,7 +192,7 @@ mod asyncstd_1 {
|
|||||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||||
"To: Hei <hei@domain.tld>\r\n",
|
"To: Hei <hei@domain.tld>\r\n",
|
||||||
"Subject: Happy new year\r\n",
|
"Subject: Happy new year\r\n",
|
||||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"Be happy!"
|
"Be happy!"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ mod sync {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = sender.send(&email);
|
let result = sender.send(&email);
|
||||||
println!("{:?}", result);
|
println!("{result:?}");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ mod tokio_1 {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = sender.send(email).await;
|
let result = sender.send(email).await;
|
||||||
println!("{:?}", result);
|
println!("{result:?}");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ mod asyncstd_1 {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let result = sender.send(email).await;
|
let result = sender.send(email).await;
|
||||||
println!("{:?}", result);
|
println!("{result:?}");
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ mod sync {
|
|||||||
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
|
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
|
||||||
mod tokio_1 {
|
mod tokio_1 {
|
||||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||||
|
|
||||||
use tokio1_crate as tokio;
|
use tokio1_crate as tokio;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#[cfg(all(test, feature = "smtp-transport", feature = "pool"))]
|
#[cfg(all(test, feature = "smtp-transport", feature = "pool"))]
|
||||||
mod sync {
|
mod sync {
|
||||||
use lettre::{address::Envelope, SmtpTransport, Transport};
|
|
||||||
use std::{sync::mpsc, thread};
|
use std::{sync::mpsc, thread};
|
||||||
|
|
||||||
|
use lettre::{address::Envelope, SmtpTransport, Transport};
|
||||||
|
|
||||||
fn envelope() -> Envelope {
|
fn envelope() -> Envelope {
|
||||||
Envelope::new(
|
Envelope::new(
|
||||||
Some("user@localhost".parse().unwrap()),
|
Some("user@localhost".parse().unwrap()),
|
||||||
|
|||||||
@@ -17,20 +17,25 @@ mod sync {
|
|||||||
|
|
||||||
sender_ok.send(&email).unwrap();
|
sender_ok.send(&email).unwrap();
|
||||||
sender_ko.send(&email).unwrap_err();
|
sender_ko.send(&email).unwrap_err();
|
||||||
|
|
||||||
|
let expected_messages = [(
|
||||||
|
email.envelope().clone(),
|
||||||
|
String::from_utf8(email.formatted()).unwrap(),
|
||||||
|
)];
|
||||||
|
assert_eq!(sender_ok.messages(), expected_messages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[cfg(all(feature = "builder", feature = "tokio1"))]
|
#[cfg(all(feature = "builder", feature = "tokio1"))]
|
||||||
mod tokio_1 {
|
mod tokio_1 {
|
||||||
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
|
use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
|
||||||
|
|
||||||
use tokio1_crate as tokio;
|
use tokio1_crate as tokio;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn stub_transport_tokio1() {
|
async fn stub_transport_tokio1() {
|
||||||
let sender_ok = StubTransport::new_ok();
|
let sender_ok = AsyncStubTransport::new_ok();
|
||||||
let sender_ko = StubTransport::new_error();
|
let sender_ko = AsyncStubTransport::new_error();
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
@@ -40,19 +45,25 @@ mod tokio_1 {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
sender_ok.send(email.clone()).await.unwrap();
|
sender_ok.send(email.clone()).await.unwrap();
|
||||||
sender_ko.send(email).await.unwrap_err();
|
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(test)]
|
||||||
#[cfg(all(feature = "builder", feature = "async-std1"))]
|
#[cfg(all(feature = "builder", feature = "async-std1"))]
|
||||||
mod asyncstd_1 {
|
mod asyncstd_1 {
|
||||||
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
|
use lettre::{transport::stub::AsyncStubTransport, AsyncTransport, Message};
|
||||||
|
|
||||||
#[async_std::test]
|
#[async_std::test]
|
||||||
async fn stub_transport_asyncstd1() {
|
async fn stub_transport_asyncstd1() {
|
||||||
let sender_ok = StubTransport::new_ok();
|
let sender_ok = AsyncStubTransport::new_ok();
|
||||||
let sender_ko = StubTransport::new_error();
|
let sender_ko = AsyncStubTransport::new_error();
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||||
@@ -62,6 +73,12 @@ mod asyncstd_1 {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
sender_ok.send(email.clone()).await.unwrap();
|
sender_ok.send(email.clone()).await.unwrap();
|
||||||
sender_ko.send(email).await.unwrap_err();
|
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