Compare commits
122 Commits
v0.10.0-al
...
v0.10.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98fc0cb2f3 | ||
|
|
0439bab874 | ||
|
|
504fc51b26 | ||
|
|
d54343cf00 | ||
|
|
904789ac3d | ||
|
|
94cae6df0d | ||
|
|
f17dccc46d | ||
|
|
7e7f05eb45 | ||
|
|
99df9e8d7c | ||
|
|
1b5109b6ac | ||
|
|
a4be3c4cd8 | ||
|
|
4586f2ad8a | ||
|
|
31de9e508b | ||
|
|
69334fe5eb | ||
|
|
2ad2444183 | ||
|
|
8afa442e93 | ||
|
|
486e0f9d50 | ||
|
|
acc4ff4898 | ||
|
|
1728d57c34 | ||
|
|
53bfb65423 | ||
|
|
61b08814c9 | ||
|
|
0e74042b4e | ||
|
|
29affe9398 | ||
|
|
b10f6ff8de | ||
|
|
2002a9d75a | ||
|
|
1193e1134d | ||
|
|
7c6ade7afe | ||
|
|
3bc729ca64 | ||
|
|
f041c00df7 | ||
|
|
fe8dc4967d | ||
|
|
137566a4e4 | ||
|
|
216c612931 | ||
|
|
a429a24913 | ||
|
|
648bf2b2f6 | ||
|
|
509a623a27 | ||
|
|
a681c6b49d | ||
|
|
22efe341fe | ||
|
|
97fba6a47e | ||
|
|
f7066ac858 | ||
|
|
9379f2e328 | ||
|
|
05133a7102 | ||
|
|
d7d05bf48a | ||
|
|
34ac265d60 | ||
|
|
bbf56de83d | ||
|
|
b594945695 | ||
|
|
5c83120986 | ||
|
|
d4df9a2965 | ||
|
|
d2aa959845 | ||
|
|
d1f016e8e2 | ||
|
|
9146212a3e | ||
|
|
a04866acfb | ||
|
|
be88aabae2 | ||
|
|
6fbb3bf440 | ||
|
|
9d8c31bef8 | ||
|
|
0ea3bfbd13 | ||
|
|
a0980d017b | ||
|
|
40c8a9d000 | ||
|
|
ed50ea74ba | ||
|
|
20d0f8f3ba | ||
|
|
690b143ea3 | ||
|
|
7f384bc983 | ||
|
|
d8c4a66206 | ||
|
|
54cd221de7 | ||
|
|
1a0c344c91 | ||
|
|
15030fde53 | ||
|
|
89fa5cdb80 | ||
|
|
aac5c9929f | ||
|
|
210133a078 | ||
|
|
a4c0af9cf1 | ||
|
|
ad9699827e | ||
|
|
f06b8f3823 | ||
|
|
170e929a2b | ||
|
|
d3c73d8bd7 | ||
|
|
4e13963bf3 | ||
|
|
ca6399acbf | ||
|
|
3f03b6296b | ||
|
|
3683d122ba | ||
|
|
0e3526c1bc | ||
|
|
50ac1cdbec | ||
|
|
227da8ac09 | ||
|
|
86763ccefb | ||
|
|
65d952a64f | ||
|
|
2e8d43baae | ||
|
|
648cf4ce5e | ||
|
|
4a9d4fbf7e | ||
|
|
a0da9fc2b9 | ||
|
|
aa31e4fff6 | ||
|
|
b187885e70 | ||
|
|
6216fd92c8 | ||
|
|
0de49c000c | ||
|
|
b55bcfb6fb | ||
|
|
04f064ed5a | ||
|
|
13b48b656d | ||
|
|
2ac6a72a8e | ||
|
|
f1e86c809d | ||
|
|
0b8d5d20ad | ||
|
|
313eb74ea5 | ||
|
|
6afc078545 | ||
|
|
697da9f7db | ||
|
|
1ea562b15a | ||
|
|
b43c69af47 | ||
|
|
449f317246 | ||
|
|
17d644181a | ||
|
|
583df6af18 | ||
|
|
ed9ca92de8 | ||
|
|
0174a29a45 | ||
|
|
bfd3300df3 | ||
|
|
6526eff5b2 | ||
|
|
e00eff8b2a | ||
|
|
5beef57c18 | ||
|
|
b10b04ada8 | ||
|
|
30a8797acf | ||
|
|
c5fef28ac9 | ||
|
|
bf32554e51 | ||
|
|
9983bb53c3 | ||
|
|
8e49c60ff8 | ||
|
|
e156520feb | ||
|
|
ec7d63c8de | ||
|
|
e5460c4ba1 | ||
|
|
d2912a3e3f | ||
|
|
9b3bd00a61 | ||
|
|
990de687aa |
2
.github/workflows/audit.yml
vendored
2
.github/workflows/audit.yml
vendored
@@ -6,7 +6,7 @@ jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
191
.github/workflows/test.yml
vendored
191
.github/workflows/test.yml
vendored
@@ -1,104 +1,127 @@
|
||||
name: Continuous integration
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
rustfmt:
|
||||
name: rustfmt / stable
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- 1.40.0
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
- run: sudo DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
- run: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install postfix
|
||||
- run: smtp-sink 2525 1000&
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features=native-tls,builder,r2d2,smtp-transport,file-transport,sendmail-transport
|
||||
- run: rm target/debug/deps/liblettre-*
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
- run: rm target/debug/deps/liblettre-*
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --no-default-features --features=builder,smtp-transport,file-transport,sendmail-transport
|
||||
- run: rm target/debug/deps/liblettre-*
|
||||
- uses: actions-rs/cargo@v1
|
||||
if: matrix.rust != '1.40.0'
|
||||
with:
|
||||
command: test
|
||||
args: --features=async-std1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --features=tokio02
|
||||
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- 1.40.0
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup component add rustfmt
|
||||
|
||||
- name: cargo fmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
name: clippy / stable
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup component add clippy
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy --all-features --all-targets -- -D warnings
|
||||
|
||||
check:
|
||||
name: check / stable
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-check
|
||||
|
||||
- name: Install rust
|
||||
run: rustup update --no-self-update stable
|
||||
|
||||
- name: Install cargo hack
|
||||
run: cargo install cargo-hack --debug
|
||||
|
||||
- name: Check with cargo hack
|
||||
run: cargo hack check --feature-powerset --depth 3
|
||||
|
||||
test:
|
||||
name: test / ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
include:
|
||||
- name: stable
|
||||
rust: stable
|
||||
- name: beta
|
||||
rust: beta
|
||||
- name: 1.46.0
|
||||
rust: 1.46.0
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup default ${{ matrix.rust }}
|
||||
rustup update --no-self-update ${{ matrix.rust }}
|
||||
|
||||
- name: Install postfix
|
||||
run: |
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix
|
||||
|
||||
- name: Run SMTP server
|
||||
run: smtp-sink 2525 1000&
|
||||
|
||||
- name: Test with no default features
|
||||
run: cargo test --no-default-features
|
||||
|
||||
- name: Test with default features
|
||||
run: cargo test
|
||||
|
||||
- name: Test with all features
|
||||
run: cargo test --all-features
|
||||
|
||||
# coverage:
|
||||
# name: Coverage
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v1
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# toolchain: nightly
|
||||
|
||||
58
CHANGELOG.md
58
CHANGELOG.md
@@ -5,7 +5,8 @@
|
||||
|
||||
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
|
||||
|
||||
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::builder`
|
||||
* MSRV is now 1.46
|
||||
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message`
|
||||
and make sure to enable the `builder` feature (it's enabled by default).
|
||||
* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
|
||||
rename `SendableEmail` to `Email`.
|
||||
@@ -13,33 +14,46 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
||||
|
||||
#### Features
|
||||
|
||||
* Add `rustls` support ([29e4829](https://github.com/lettre/lettre/commit/29e4829), [39a0686](https://github.com/lettre/lettre/commit/39a0686))
|
||||
* Allow providing a custom message id ([50d96ad](https://github.com/lettre/lettre/commit/50d96ad))
|
||||
* Add `EmailAddress::is_valid` and `into_inner` ([e5a1248](https://github.com/lettre/lettre/commit/e5a1248))
|
||||
* Accept `Into<SendableEmail>` ([86e5181](https://github.com/lettre/lettre/commit/86e5181))
|
||||
* Allow forcing of a specific auth ([bf2adca](https://github.com/lettre/lettre/commit/bf2adca))
|
||||
* Add `build_body` ([e927d0b](https://github.com/lettre/lettre/commit/e927d0b))
|
||||
* Add `tokio` 1 support
|
||||
* Add `rustls` support
|
||||
* Add `async-std` support. NOTE: native-tls isn't supported when using async-std for the smtp transport.
|
||||
* Allow enabling multiple SMTP authentication mechanisms
|
||||
* Allow providing a custom message id
|
||||
* Allow sending raw emails
|
||||
|
||||
#### Changes
|
||||
#### Breaking Changes
|
||||
|
||||
* Move CI to Github Actions ([3eef024](https://github.com/lettre/lettre/commit/3eef024))
|
||||
* MSRV is now 1.36 ([d227cd4](https://github.com/lettre/lettre/commit/d227cd4))
|
||||
* Merged `lettre_email` into `lettre` ([0f3f27f](https://github.com/lettre/lettre/commit/0f3f27f))
|
||||
* Rename `serde-impls` feature to `serde` ([aac3e00](https://github.com/lettre/lettre/commit/aac3e00))
|
||||
* Use criterion for benchmarks ([eda7fc1](https://github.com/lettre/lettre/commit/eda7fc1))
|
||||
* Update to nom 5 ([5bc1cba](https://github.com/lettre/lettre/commit/5bc1cba))
|
||||
* Change website url schemes to https ([6014f5c](https://github.com/lettre/lettre/commit/6014f5c))
|
||||
* Use serde's `derive` feature instead of the `serde_derive` crate ([4fbe700](https://github.com/lettre/lettre/commit/4fbe700))
|
||||
* Merge `Email` and `SendableEmail` into `lettre::Email` ([ce37464](https://github.com/lettre/lettre/commit/ce37464))
|
||||
* 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)
|
||||
* Merge `lettre_email` into `lettre`
|
||||
* Merge `Email` and `SendableEmail` into `lettre::message::Email`
|
||||
* SmtpTransport is now an high level SMTP client. It provides connection pooling and shortcuts for building clients using commonly desired values
|
||||
* Refactor `TlsParameters` implementation to not expose the internal TLS library
|
||||
* `FileTransport` writes emails into `.eml` instead of `.json`
|
||||
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
|
||||
* The `new` method of `ClientId` is deprecated
|
||||
* Rename `serde-impls` feature to `serde`
|
||||
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* Timeout bug causing infinite hang ([6eff9d3](https://github.com/lettre/lettre/commit/6eff9d3))
|
||||
* Fix doc tests in website ([947af0a](https://github.com/lettre/lettre/commit/947af0a))
|
||||
* Fix docs for `domain` field ([0e05e0e](https://github.com/lettre/lettre/commit/0e05e0e))
|
||||
* Fix argument injection in `SendmailTransport` (see [RUSTSEC-2020-0069](https://github.com/RustSec/advisory-db/blob/master/crates/lettre/RUSTSEC-2020-0069.md))
|
||||
* Correctly encode header values containing non-ASCII characters
|
||||
* Timeout bug causing infinite hang
|
||||
* Fix doc tests in website
|
||||
* Fix docs for `domain` field
|
||||
|
||||
#### Misc
|
||||
|
||||
* Improve documentation, examples and tests
|
||||
* Replace `line-wrap`, `email`, `bufstream` with our own implementations
|
||||
* Remove `bytes`
|
||||
* Remove `time`
|
||||
* Remove `fast_chemail`
|
||||
* Update `base64` to 0.13
|
||||
* Update `hostname` to 0.3
|
||||
* Update to `nom` 6
|
||||
* Replace `log` with `tracing`
|
||||
* Move CI to Github Actions
|
||||
* Use criterion for benchmarks
|
||||
|
||||
<a name="v0.9.2"></a>
|
||||
### v0.9.2 (2019-06-11)
|
||||
|
||||
@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.at. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.rs. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
|
||||
129
Cargo.toml
129
Cargo.toml
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "lettre"
|
||||
version = "0.10.0-alpha.2" # remember to update html_root_url and README.md
|
||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||
version = "0.10.0-rc.2"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "https://lettre.at"
|
||||
homepage = "https://lettre.rs"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
|
||||
@@ -17,77 +18,123 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
async-attributes = { version = "1.1", optional = true }
|
||||
async-std = { version = "1.5", optional = true, features = ["unstable"] }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["fs", "process", "tcp", "dns", "io-util"], optional = true }
|
||||
tokio02_native_tls_crate = { package = "tokio-native-tls", version = "0.1", optional = true }
|
||||
tokio02_rustls = { package = "tokio-rustls", version = "0.14", optional = true }
|
||||
futures-io = { version = "0.3", optional = true }
|
||||
futures-util = { version = "0.3", features = ["io"], optional = true }
|
||||
base64 = { version = "0.12", optional = true }
|
||||
hostname = { version = "0.3", optional = true }
|
||||
hyperx = { version = "1", optional = true, features = ["headers"] }
|
||||
idna = "0.2"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true }
|
||||
mime = { version = "0.3", optional = true }
|
||||
native-tls = { version = "0.2", optional = true }
|
||||
nom = { version = "5", optional = true }
|
||||
once_cell = "1"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
||||
|
||||
# builder
|
||||
httpdate = { version = "1", optional = true }
|
||||
mime = { version = "0.3.4", optional = true }
|
||||
fastrand = { version = "1.4", optional = true }
|
||||
quoted_printable = { version = "0.4", optional = true }
|
||||
r2d2 = { version = "0.8", optional = true }
|
||||
rand = { version = "0.7", optional = true }
|
||||
regex = "1"
|
||||
rustls = { version = "0.18", optional = true }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
once_cell = "1"
|
||||
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
|
||||
|
||||
# file transport
|
||||
uuid = { version = "0.8", features = ["v4"], optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
# smtp
|
||||
nom = { version = "6", default-features = false, features = ["alloc", "std"], optional = true }
|
||||
r2d2 = { version = "0.8", optional = true } # feature
|
||||
hostname = { version = "0.3", optional = true } # feature
|
||||
|
||||
## tls
|
||||
native-tls = { version = "0.2", optional = true } # feature
|
||||
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true }
|
||||
webpki = { version = "0.21", optional = true }
|
||||
webpki-roots = { version = "0.20", optional = true }
|
||||
webpki-roots = { version = "0.21", optional = true }
|
||||
|
||||
# async
|
||||
futures-io = { version = "0.3.7", optional = true }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["io"], optional = true }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
|
||||
## async-std
|
||||
async-std = { version = "1.8", optional = true, features = ["unstable"] }
|
||||
#async-native-tls = { version = "0.3.3", optional = true }
|
||||
async-rustls = { version = "0.2", optional = true }
|
||||
|
||||
## tokio
|
||||
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "process", "net", "io-util"], optional = true }
|
||||
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
|
||||
tokio1_rustls = { package = "tokio-rustls", version = "0.22", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3"
|
||||
tracing-subscriber = "0.2.10"
|
||||
glob = "0.3"
|
||||
walkdir = "2"
|
||||
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
|
||||
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
async-std = { version = "1.8", features = ["attributes"] }
|
||||
serde_json = "1"
|
||||
maud = "0.22.1"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "transport_smtp"
|
||||
|
||||
[features]
|
||||
async-std1 = ["async-std", "async-trait", "async-attributes"]
|
||||
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"]
|
||||
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"]
|
||||
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
|
||||
default = ["file-transport", "smtp-transport", "native-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
|
||||
file-transport = ["serde", "serde_json"]
|
||||
# native-tls
|
||||
rustls-tls = ["webpki", "webpki-roots", "rustls"]
|
||||
default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"]
|
||||
builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable"]
|
||||
|
||||
# transports
|
||||
file-transport = ["uuid"]
|
||||
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
||||
sendmail-transport = []
|
||||
smtp-transport = ["base64", "nom"]
|
||||
|
||||
rustls-tls = ["webpki", "webpki-roots", "rustls"]
|
||||
|
||||
# async
|
||||
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
|
||||
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"]
|
||||
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[[example]]
|
||||
name = "basic_html"
|
||||
required-features = ["file-transport", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "maud_html"
|
||||
required-features = ["file-transport", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp"
|
||||
required-features = ["smtp-transport"]
|
||||
required-features = ["smtp-transport", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_tls"
|
||||
required-features = ["smtp-transport", "native-tls"]
|
||||
required-features = ["smtp-transport", "native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_starttls"
|
||||
required-features = ["smtp-transport", "native-tls"]
|
||||
required-features = ["smtp-transport", "native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio02_smtp_tls"
|
||||
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
|
||||
name = "smtp_selfsigned"
|
||||
required-features = ["smtp-transport", "native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio02_smtp_starttls"
|
||||
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
|
||||
name = "tokio1_smtp_tls"
|
||||
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio1_smtp_starttls"
|
||||
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "asyncstd1_smtp_tls"
|
||||
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "asyncstd1_smtp_starttls"
|
||||
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
|
||||
|
||||
30
README.md
30
README.md
@@ -21,19 +21,31 @@
|
||||
<img src="https://badges.gitter.im/lettre/lettre.svg"
|
||||
alt="chat on gitter" />
|
||||
</a>
|
||||
<a href="https://lettre.at">
|
||||
<a href="https://lettre.rs">
|
||||
<img src="https://img.shields.io/badge/visit-website-blueviolet"
|
||||
alt="website" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.10.0-rc.2">
|
||||
<img src="https://deps.rs/crate/lettre/0.10.0-rc.2/status.svg"
|
||||
alt="dependency status" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
**NOTE**: this readme refers to the 0.10 version of lettre, which is
|
||||
still being worked on. The master branch and the alpha releases will see
|
||||
API breaking changes and some features may be missing or incomplete until
|
||||
the stable 0.10.0 release is out.
|
||||
Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x) branch for stable releases.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -45,7 +57,7 @@ Lettre provides the following features:
|
||||
* Unicode support (for email content and addresses)
|
||||
* Secure delivery with SMTP using encryption and authentication
|
||||
* Easy email builders
|
||||
* Async support (incomplete)
|
||||
* Async support
|
||||
|
||||
Lettre does not provide (for now):
|
||||
|
||||
@@ -53,13 +65,13 @@ Lettre does not provide (for now):
|
||||
|
||||
## Example
|
||||
|
||||
This library requires Rust 1.40 or newer.
|
||||
This library requires Rust 1.46 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.10.0-alpha.2"
|
||||
lettre = "0.10.0-rc.2"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
@@ -71,7 +83,7 @@ let email = Message::builder()
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Report a security issue
|
||||
|
||||
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
|
||||
Security issues should be reported privately via [security@lettre.at](mailto:security@lettre.at). Security issues
|
||||
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
|
||||
should not be reported via the public Github Issue tracker.
|
||||
|
||||
## Security advisories
|
||||
|
||||
@@ -2,7 +2,9 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
fn bench_simple_send(c: &mut Criterion) {
|
||||
let sender = SmtpTransport::builder("127.0.0.1").port(2525).build();
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
|
||||
c.bench_function("send email", move |b| {
|
||||
b.iter(|| {
|
||||
@@ -11,7 +13,7 @@ fn bench_simple_send(c: &mut Criterion) {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
let result = black_box(sender.send(&email));
|
||||
assert!(result.is_ok());
|
||||
@@ -20,7 +22,9 @@ fn bench_simple_send(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn bench_reuse_send(c: &mut Criterion) {
|
||||
let sender = SmtpTransport::builder("127.0.0.1").port(2525).build();
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
c.bench_function("send email with connection reuse", move |b| {
|
||||
b.iter(|| {
|
||||
let email = Message::builder()
|
||||
@@ -28,7 +32,7 @@ fn bench_reuse_send(c: &mut Criterion) {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
let result = black_box(sender.send(&email));
|
||||
assert!(result.is_ok());
|
||||
|
||||
BIN
docs/lettre.png
Normal file
BIN
docs/lettre.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
24
examples/README.md
Normal file
24
examples/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Lettre Examples
|
||||
|
||||
This folder contains examples showing how to use lettre in your own projects.
|
||||
|
||||
## Message builder examples
|
||||
|
||||
- [basic_html.rs] - Create an HTML email.
|
||||
- [maud_html.rs] - Create an HTML email using a [maud](https://github.com/lambda-fairy/maud) template.
|
||||
|
||||
## SMTP Examples
|
||||
|
||||
- [smtp.rs] - Send an email using a local SMTP daemon on port 25 as a relay.
|
||||
- [smtp_tls.rs] - Send an email over SMTP encrypted with TLS and authenticating with username and password.
|
||||
- [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password.
|
||||
- [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password.
|
||||
- The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/).
|
||||
These files are prefixed with `tokio1_` or `asyncstd1_`.
|
||||
|
||||
[basic_html.rs]: ./basic_html.rs
|
||||
[maud_html.rs]: ./maud_html.rs
|
||||
[smtp.rs]: ./smtp.rs
|
||||
[smtp_tls.rs]: ./smtp_tls.rs
|
||||
[smtp_starttls.rs]: ./smtp_starttls.rs
|
||||
[smtp_selfsigned.rs]: ./smtp_selfsigned.rs
|
||||
32
examples/asyncstd1_smtp_starttls.rs
Normal file
32
examples/asyncstd1_smtp_starttls.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
|
||||
AsyncTransport, Message,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
AsyncSmtpTransport::<AsyncStd1Executor>::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
32
examples/asyncstd1_smtp_tls.rs
Normal file
32
examples/asyncstd1_smtp_tls.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
|
||||
AsyncTransport, Message,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
AsyncSmtpTransport::<AsyncStd1Executor>::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
49
examples/basic_html.rs
Normal file
49
examples/basic_html.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use lettre::{
|
||||
message::{header, MultiPart, SinglePart},
|
||||
FileTransport, Message, Transport,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
// The html we want to send.
|
||||
let html = r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hello from Lettre!</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||
<h2 style="font-family: Arial, Helvetica, sans-serif;">Hello from Lettre!</h2>
|
||||
<h4 style="font-family: Arial, Helvetica, sans-serif;">A mailer library for Rust</h4>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#;
|
||||
|
||||
// Build the message.
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Hello from Lettre!")
|
||||
.multipart(
|
||||
MultiPart::alternative() // This is composed of two parts.
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(String::from(html)),
|
||||
),
|
||||
)
|
||||
.expect("failed to build email");
|
||||
|
||||
// Create our mailer. Please see the other examples for creating SMTP mailers.
|
||||
// The path given here must exist on the filesystem.
|
||||
let mailer = FileTransport::new("./");
|
||||
|
||||
// Store the message when you're ready.
|
||||
mailer.send(&email).expect("failed to deliver message");
|
||||
}
|
||||
58
examples/maud_html.rs
Normal file
58
examples/maud_html.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use lettre::{
|
||||
message::{header, MultiPart, SinglePart},
|
||||
FileTransport, Message, Transport,
|
||||
};
|
||||
use maud::html;
|
||||
|
||||
fn main() {
|
||||
// The recipient's name. We might obtain this from a form or their email address.
|
||||
let recipient = "Hei";
|
||||
|
||||
// Create the html we want to send.
|
||||
let html = html! {
|
||||
head {
|
||||
title { "Hello from Lettre!" }
|
||||
style type="text/css" {
|
||||
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
|
||||
}
|
||||
}
|
||||
div style="display: flex; flex-direction: column; align-items: center;" {
|
||||
h2 { "Hello from Lettre!" }
|
||||
// Substitute in the name of our recipient.
|
||||
p { "Dear " (recipient) "," }
|
||||
p { "This email was sent with Lettre, a mailer library for Rust!"}
|
||||
p {
|
||||
"This example uses "
|
||||
a href="https://crates.io/crates/maud" { "maud" }
|
||||
". It is about 20% cooler than the basic HTML example."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Build the message.
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Hello from Lettre!")
|
||||
.multipart(
|
||||
MultiPart::alternative() // This is composed of two parts.
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(html.into_string()),
|
||||
),
|
||||
)
|
||||
.expect("failed to build email");
|
||||
|
||||
// Create our mailer. Please see the other examples for creating SMTP mailers.
|
||||
// The path given here must exist on the filesystem.
|
||||
let mailer = FileTransport::new("./");
|
||||
|
||||
// Store the message when you're ready.
|
||||
mailer.send(&email).expect("failed to deliver message");
|
||||
}
|
||||
@@ -8,7 +8,7 @@ fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
|
||||
44
examples/smtp_selfsigned.rs
Normal file
44
examples/smtp_selfsigned.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use std::fs;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::{
|
||||
authentication::Credentials,
|
||||
client::{Certificate, Tls, TlsParameters},
|
||||
},
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
// Use a custom certificate stored on disk to securely verify the server's certificate
|
||||
let pem_cert = fs::read("certificate.pem").unwrap();
|
||||
let cert = Certificate::from_pem(&pem_cert).unwrap();
|
||||
let tls = TlsParameters::builder("smtp.server.com".to_string())
|
||||
.add_root_certificate(cert)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to the smtp server
|
||||
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
|
||||
.port(465)
|
||||
.tls(Tls::Wrapper(tls))
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
@@ -8,7 +8,7 @@ fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use tokio02_crate as tokio;
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
|
||||
Tokio02Transport,
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
|
||||
Tokio1Executor,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
@@ -17,16 +17,17 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.body("Be happy with async!")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer = AsyncSmtpTransport::<Tokio02Connector>::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
@@ -1,11 +1,11 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use tokio02_crate as tokio;
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
|
||||
Tokio02Transport,
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
|
||||
Tokio1Executor,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
@@ -17,16 +17,17 @@ async fn main() {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.body("Be happy with async!")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer = AsyncSmtpTransport::<Tokio02Connector>::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
145
src/address/envelope.rs
Normal file
145
src/address/envelope.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
#[cfg(feature = "builder")]
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::Address;
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::message::header::{self, Headers};
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::message::{Mailbox, Mailboxes};
|
||||
use crate::Error;
|
||||
|
||||
/// Simple email envelope representation
|
||||
///
|
||||
/// We only accept mailboxes, and do not support source routes (as per RFC).
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Envelope {
|
||||
/// The envelope recipient's addresses
|
||||
///
|
||||
/// This can not be empty.
|
||||
forward_path: Vec<Address>,
|
||||
/// The envelope sender address
|
||||
reverse_path: Option<Address>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
/// Creates a new envelope, which may fail if `to` is empty.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = "sender@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `to` has no elements in it.
|
||||
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
|
||||
if to.is_empty() {
|
||||
return Err(Error::MissingTo);
|
||||
}
|
||||
Ok(Envelope {
|
||||
forward_path: to,
|
||||
reverse_path: from,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the destination addresses of the envelope.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = "from@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
|
||||
/// assert_eq!(envelope.to(), recipients.as_slice());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn to(&self) -> &[Address] {
|
||||
self.forward_path.as_slice()
|
||||
}
|
||||
|
||||
/// Gets the sender of the envelope.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = "from@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
|
||||
/// assert!(envelope.from().is_some());
|
||||
///
|
||||
/// let senderless = Envelope::new(None, recipients.clone())?;
|
||||
/// assert!(senderless.from().is_none());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from(&self) -> Option<&Address> {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
/// Check if any of the addresses in the envelope contains non-ascii chars
|
||||
pub(crate) fn has_non_ascii_addresses(&self) -> bool {
|
||||
self.reverse_path
|
||||
.iter()
|
||||
.chain(self.forward_path.iter())
|
||||
.any(|a| !a.is_ascii())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "builder")]
|
||||
impl TryFrom<&Headers> for Envelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
|
||||
let from = match headers.get::<header::Sender>() {
|
||||
// If there is a Sender, use it
|
||||
Some(sender) => Some(Mailbox::from(sender).email),
|
||||
// ... else try From
|
||||
None => match headers.get::<header::From>() {
|
||||
Some(header::From(a)) => {
|
||||
let mut from: Vec<Mailbox> = a.into();
|
||||
if from.len() > 1 {
|
||||
return Err(Error::TooManyFrom);
|
||||
}
|
||||
let from = from.pop().expect("From header has 1 Mailbox");
|
||||
Some(from.email)
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
fn add_addresses_from_mailboxes(
|
||||
addresses: &mut Vec<Address>,
|
||||
mailboxes: Option<Mailboxes>,
|
||||
) {
|
||||
if let Some(mailboxes) = mailboxes {
|
||||
addresses.extend(mailboxes.into_iter().map(|mb| mb.email));
|
||||
}
|
||||
}
|
||||
let mut to = vec![];
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| h.0));
|
||||
|
||||
Self::new(from, to)
|
||||
}
|
||||
}
|
||||
12
src/address/mod.rs
Normal file
12
src/address/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! Email addresses
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
|
||||
mod envelope;
|
||||
mod types;
|
||||
|
||||
pub use self::{
|
||||
envelope::Envelope,
|
||||
types::{Address, AddressError},
|
||||
};
|
||||
112
src/address/serde.rs
Normal file
112
src/address/serde.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
|
||||
use super::Address;
|
||||
|
||||
impl Serialize for Address {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Address {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
User,
|
||||
Domain,
|
||||
}
|
||||
|
||||
const FIELDS: &[&str] = &["user", "domain"];
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("'user' or 'domain'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Field, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
match value {
|
||||
"user" => Ok(Field::User),
|
||||
"domain" => Ok(Field::Domain),
|
||||
_ => Err(DeError::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_identifier(FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddressVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AddressVisitor {
|
||||
type Value = Address;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("email address string or object")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut user = None;
|
||||
let mut domain = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::User => {
|
||||
if user.is_some() {
|
||||
return Err(DeError::duplicate_field("user"));
|
||||
}
|
||||
let val = map.next_value()?;
|
||||
Address::check_user(val).map_err(DeError::custom)?;
|
||||
user = Some(val);
|
||||
}
|
||||
Field::Domain => {
|
||||
if domain.is_some() {
|
||||
return Err(DeError::duplicate_field("domain"));
|
||||
}
|
||||
let val = map.next_value()?;
|
||||
Address::check_domain(val).map_err(DeError::custom)?;
|
||||
domain = Some(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
|
||||
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
|
||||
Ok(Address::new(user, domain).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(AddressVisitor)
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,41 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Email address
|
||||
/// Represents an email address with a user and a domain name.
|
||||
///
|
||||
/// This type contains email in canonical form (_user@domain.tld_).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can create an `Address` from a user and a domain:
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can also create an `Address` from a string literal by parsing it:
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = "user@email.com".parse::<Address>()?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Address {
|
||||
/// Complete address
|
||||
@@ -25,6 +55,138 @@ pub struct Address {
|
||||
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 {
|
||||
/// Creates a new email address from a user and domain.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// let expected = "user@email.com".parse::<Address>()?;
|
||||
/// assert_eq!(expected, address);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new<U: AsRef<str>, D: AsRef<str>>(user: U, domain: D) -> Result<Self, AddressError> {
|
||||
(user, domain).try_into()
|
||||
}
|
||||
|
||||
/// Gets the user portion of the `Address`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn user(&self) -> &str {
|
||||
&self.serialized[..self.at_start]
|
||||
}
|
||||
|
||||
/// Gets the domain portion of the `Address`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn domain(&self) -> &str {
|
||||
&self.serialized[self.at_start + 1..]
|
||||
}
|
||||
|
||||
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
|
||||
if USER_RE.is_match(user) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AddressError::InvalidUser)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check_domain(domain: &str) -> Result<(), AddressError> {
|
||||
Address::check_domain_ascii(domain).or_else(|_| {
|
||||
domain_to_ascii(domain)
|
||||
.map_err(|_| AddressError::InvalidDomain)
|
||||
.and_then(|domain| Address::check_domain_ascii(&domain))
|
||||
})
|
||||
}
|
||||
|
||||
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
|
||||
if DOMAIN_RE.is_match(domain) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(caps) = LITERAL_RE.captures(domain) {
|
||||
if let Some(cap) = caps.get(1) {
|
||||
if cap.as_str().parse::<IpAddr>().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(AddressError::InvalidDomain)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
/// Check if the address contains non-ascii chars
|
||||
pub(super) fn is_ascii(&self) -> bool {
|
||||
self.serialized.is_ascii()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.write_str(&self.serialized)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, AddressError> {
|
||||
let mut parts = val.rsplitn(2, '@');
|
||||
let domain = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
let user = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
|
||||
Address::check_user(user)?;
|
||||
Address::check_domain(domain)?;
|
||||
Ok(Address {
|
||||
serialized: val.into(),
|
||||
at_start: user.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, D> TryFrom<(U, D)> for Address
|
||||
where
|
||||
U: AsRef<str>,
|
||||
@@ -47,92 +209,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
/// Create email address from parts
|
||||
pub fn new<U: AsRef<str>, D: AsRef<str>>(user: U, domain: D) -> Result<Self, AddressError> {
|
||||
(user, domain).try_into()
|
||||
}
|
||||
|
||||
/// Get the user part of this `Address`
|
||||
pub fn user(&self) -> &str {
|
||||
&self.serialized[..self.at_start]
|
||||
}
|
||||
|
||||
/// Get the domain part of this `Address`
|
||||
pub fn domain(&self) -> &str {
|
||||
&self.serialized[self.at_start + 1..]
|
||||
}
|
||||
|
||||
fn check_user(user: &str) -> Result<(), AddressError> {
|
||||
if USER_RE.is_match(user) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AddressError::InvalidUser)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_domain(domain: &str) -> Result<(), AddressError> {
|
||||
Address::check_domain_ascii(domain).or_else(|_| {
|
||||
domain_to_ascii(domain)
|
||||
.map_err(|_| AddressError::InvalidDomain)
|
||||
.and_then(|domain| Address::check_domain_ascii(&domain))
|
||||
})
|
||||
}
|
||||
|
||||
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
|
||||
if DOMAIN_RE.is_match(domain) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(caps) = LITERAL_RE.captures(domain) {
|
||||
if let Some(cap) = caps.get(1) {
|
||||
if cap.as_str().parse::<IpAddr>().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(AddressError::InvalidDomain)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
f.write_str(&self.serialized)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, AddressError> {
|
||||
let mut parts = val.rsplitn(2, '@');
|
||||
let domain = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
let user = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
|
||||
Address::check_user(user)?;
|
||||
Address::check_domain(domain)?;
|
||||
Ok(Address {
|
||||
serialized: val.into(),
|
||||
at_start: user.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Address {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.serialized
|
||||
@@ -146,138 +222,27 @@ impl AsRef<OsStr> for Address {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
/// Errors in email addresses parsing
|
||||
pub enum AddressError {
|
||||
/// Missing domain or user
|
||||
MissingParts,
|
||||
/// Unbalanced angle bracket
|
||||
Unbalanced,
|
||||
/// Invalid email user
|
||||
InvalidUser,
|
||||
/// Invalid email domain
|
||||
InvalidDomain,
|
||||
InvalidUtf8b,
|
||||
}
|
||||
|
||||
impl Error for AddressError {}
|
||||
|
||||
impl Display for AddressError {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
AddressError::MissingParts => f.write_str("Missing domain or user"),
|
||||
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
|
||||
AddressError::InvalidUser => f.write_str("Invalid email user"),
|
||||
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
|
||||
AddressError::InvalidUtf8b => f.write_str("Invalid UTF8b data"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde {
|
||||
use crate::address::Address;
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
impl Serialize for Address {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Address {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
User,
|
||||
Domain,
|
||||
};
|
||||
|
||||
const FIELDS: &[&str] = &["user", "domain"];
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("'user' or 'domain'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Field, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
match value {
|
||||
"user" => Ok(Field::User),
|
||||
"domain" => Ok(Field::Domain),
|
||||
_ => Err(DeError::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_identifier(FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddressVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AddressVisitor {
|
||||
type Value = Address;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("email address string or object")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut user = None;
|
||||
let mut domain = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::User => {
|
||||
if user.is_some() {
|
||||
return Err(DeError::duplicate_field("user"));
|
||||
}
|
||||
let val = map.next_value()?;
|
||||
Address::check_user(val).map_err(DeError::custom)?;
|
||||
user = Some(val);
|
||||
}
|
||||
Field::Domain => {
|
||||
if domain.is_some() {
|
||||
return Err(DeError::duplicate_field("domain"));
|
||||
}
|
||||
let val = map.next_value()?;
|
||||
Address::check_domain(val).map_err(DeError::custom)?;
|
||||
domain = Some(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
|
||||
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
|
||||
Ok(Address::new(user, domain).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(AddressVisitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Error type for email messages
|
||||
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
|
||||
200
src/executor.rs
Normal file
200
src/executor.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use std::fmt::Debug;
|
||||
#[cfg(feature = "file-transport")]
|
||||
use std::io::Result as IoResult;
|
||||
#[cfg(feature = "file-transport")]
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::client::AsyncSmtpConnection;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::client::Tls;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::extension::ClientId;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
/// Async executor abstraction trait
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[async_trait]
|
||||
pub trait Executor: Debug + Send + Sync + private::Sealed {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error>;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `tokio` `1.x`
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
#[derive(Debug)]
|
||||
pub struct Tokio1Executor;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Executor for Tokio1Executor {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_tokio1(hostname, port, hello_name, tls_parameters).await?;
|
||||
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||
tokio1_crate::fs::read(path).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||
tokio1_crate::fs::write(path, contents).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `async-std` `1.x`
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
#[derive(Debug)]
|
||||
pub struct AsyncStd1Executor;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Executor for AsyncStd1Executor {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_asyncstd1(hostname, port, hello_name, tls_parameters)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||
async_std::fs::read(path).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||
async_std::fs::write(path, contents).await
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Sealed for Tokio1Executor {}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Sealed for AsyncStd1Executor {}
|
||||
}
|
||||
311
src/lib.rs
311
src/lib.rs
@@ -4,206 +4,173 @@
|
||||
//! * Pluggable email transports
|
||||
//! * Unicode support
|
||||
//! * Secure defaults
|
||||
//! * Async support
|
||||
//!
|
||||
//! Lettre requires Rust 1.40 or newer.
|
||||
//! Lettre requires Rust 1.46 or newer.
|
||||
//!
|
||||
//! ## Optional features
|
||||
//! ## Features
|
||||
//!
|
||||
//! This section lists each lettre feature and briefly explains it.
|
||||
//! More info about each module can be found in the corresponding module page.
|
||||
//!
|
||||
//! Features with `📫` near them are enabled by default.
|
||||
//!
|
||||
//! ### Typed message builder
|
||||
//!
|
||||
//! _Strongly typed [`message`] builder_
|
||||
//!
|
||||
//! * **builder** 📫: Enable the [`Message`] builder
|
||||
//! * **hostname** 📫: Try to use the actual system hostname in the `Message-ID` header
|
||||
//!
|
||||
//! ### SMTP transport
|
||||
//!
|
||||
//! _Send emails using [`SMTP`]_
|
||||
//!
|
||||
//! * **smtp-transport** 📫: Enable the SMTP transport
|
||||
//! * **r2d2** 📫: Connection pool for SMTP transport
|
||||
//! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
|
||||
//!
|
||||
//! #### SMTP over TLS via the native-tls crate
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `native-tls` crate_
|
||||
//!
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
|
||||
//!
|
||||
//! * **native-tls** 📫: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
|
||||
//!
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! #### SMTP over TLS via the rustls crate
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
|
||||
//!
|
||||
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
|
||||
//!
|
||||
//! * **rustls-tls**: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
|
||||
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
|
||||
//!
|
||||
//! ### Sendmail transport
|
||||
//!
|
||||
//! _Send emails using the [`sendmail`] command_
|
||||
//!
|
||||
//! * **sendmail-transport**: Enable the `sendmail` transport
|
||||
//!
|
||||
//! ### File transport
|
||||
//!
|
||||
//! _Save emails as an `.eml` [`file`]_
|
||||
//!
|
||||
//! * **file-transport**: Enable the file transport (saves emails into an `.eml` file)
|
||||
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file (additionally saves envelopes into a `.json` file)
|
||||
//!
|
||||
//! ### Async execution runtimes
|
||||
//!
|
||||
//! _Use [tokio] or [async-std] as an async execution runtime for sending emails_
|
||||
//!
|
||||
//! The correct runtime version must be chosen in order for lettre to work correctly.
|
||||
//! For example, when sending emails from a Tokio 1.x context, the Tokio 1.x executor
|
||||
//! ([`Tokio1Executor`]) must be used. Using a different version (for example Tokio 0.2.x),
|
||||
//! or async-std, would result in a runtime panic.
|
||||
//!
|
||||
//! * **tokio1**: Allow to asynchronously send emails using [Tokio 1.x]
|
||||
//! * **async-std1**: Allow to asynchronously send emails using [async-std 1.x]
|
||||
//!
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! ### Misc features
|
||||
//!
|
||||
//! _Additional features_
|
||||
//!
|
||||
//! * **builder**: Message builder
|
||||
//! * **file-transport**: Transport that write messages into a file
|
||||
//! * **smtp-transport**: Transport over SMTP
|
||||
//! * **sendmail-transport**: Transport over SMTP
|
||||
//! * **rustls-tls**: TLS support with the `rustls` crate
|
||||
//! * **native-tls**: TLS support with the `native-tls` crate
|
||||
//! * **tokio02**: Allow to asyncronously send emails using tokio 0.2.x
|
||||
//! * **tokio02-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.2
|
||||
//! * **tokio02-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.2
|
||||
//! * **async-std1**: Allow to asyncronously send emails using async-std 1.x (SMTP isn't supported yet)
|
||||
//! * **r2d2**: Connection pool for SMTP transport
|
||||
//! * **tracing**: Logging using the `tracing` crate
|
||||
//! * **serde**: Serialization/Deserialization of entities
|
||||
//! * **hostname**: Ability to try to use actual hostname in SMTP transaction
|
||||
//! * **tracing**: Logging using the `tracing` crate
|
||||
//!
|
||||
//! [`SMTP`]: crate::transport::smtp
|
||||
//! [`sendmail`]: crate::transport::sendmail
|
||||
//! [`file`]: crate::transport::file
|
||||
//! [tokio]: https://docs.rs/tokio/1
|
||||
//! [async-std]: https://docs.rs/async-std/1
|
||||
//! [ring]: https://github.com/briansmith/ring#ring
|
||||
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
|
||||
//! [Tokio 1.x]: https://docs.rs/tokio/1
|
||||
//! [async-std 1.x]: https://docs.rs/async-std/1
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-alpha.2")]
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.2")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unstable_features,
|
||||
unused_import_braces,
|
||||
unsafe_code
|
||||
rust_2018_idioms
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
pub mod address;
|
||||
pub mod error;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod executor;
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
pub mod message;
|
||||
pub mod transport;
|
||||
|
||||
use crate::error::Error;
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub use self::executor::AsyncStd1Executor;
|
||||
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
||||
pub use self::executor::Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::executor::Tokio1Executor;
|
||||
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
||||
#[doc(inline)]
|
||||
pub use self::transport::AsyncTransport;
|
||||
pub use crate::address::Address;
|
||||
#[cfg(feature = "builder")]
|
||||
pub use crate::message::{
|
||||
header::{self, Headers},
|
||||
EmailFormat, Mailbox, Mailboxes, Message,
|
||||
};
|
||||
#[doc(inline)]
|
||||
pub use crate::message::Message;
|
||||
#[cfg(all(
|
||||
feature = "file-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::file::AsyncFileTransport;
|
||||
#[cfg(feature = "file-transport")]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::file::FileTransport;
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::sendmail::AsyncSendmailTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::sendmail::SendmailTransport;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
||||
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
pub use crate::transport::smtp::AsyncSmtpTransport;
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::Transport;
|
||||
use crate::{address::Envelope, error::Error};
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use crate::transport::smtp::SmtpTransport;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio02"))]
|
||||
pub use crate::transport::smtp::{AsyncSmtpTransport, Tokio02Connector};
|
||||
pub use crate::{address::Address, transport::stub::StubTransport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(feature = "builder")]
|
||||
use std::convert::TryFrom;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
/// Simple email envelope representation
|
||||
///
|
||||
/// We only accept mailboxes, and do not support source routes (as per RFC).
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Envelope {
|
||||
/// The envelope recipients' addresses
|
||||
///
|
||||
/// This can not be empty.
|
||||
forward_path: Vec<Address>,
|
||||
/// The envelope sender address
|
||||
reverse_path: Option<Address>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
/// Creates a new envelope, which may fail if `to` is empty.
|
||||
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
|
||||
if to.is_empty() {
|
||||
return Err(Error::MissingTo);
|
||||
}
|
||||
Ok(Envelope {
|
||||
forward_path: to,
|
||||
reverse_path: from,
|
||||
})
|
||||
}
|
||||
|
||||
/// Destination addresses of the envelope
|
||||
pub fn to(&self) -> &[Address] {
|
||||
self.forward_path.as_slice()
|
||||
}
|
||||
|
||||
/// Source address of the envelope
|
||||
pub fn from(&self) -> Option<&Address> {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Headers> for Envelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
|
||||
let from = match headers.get::<header::Sender>() {
|
||||
// If there is a Sender, use it
|
||||
Some(header::Sender(a)) => Some(a.email.clone()),
|
||||
// ... else try From
|
||||
None => match headers.get::<header::From>() {
|
||||
Some(header::From(a)) => {
|
||||
let from: Vec<Mailbox> = a.clone().into();
|
||||
if from.len() > 1 {
|
||||
return Err(Error::TooManyFrom);
|
||||
}
|
||||
Some(from[0].email.clone())
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
fn add_addresses_from_mailboxes(
|
||||
addresses: &mut Vec<Address>,
|
||||
mailboxes: Option<&Mailboxes>,
|
||||
) {
|
||||
if let Some(mailboxes) = mailboxes {
|
||||
for mailbox in mailboxes.iter() {
|
||||
addresses.push(mailbox.email.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut to = vec![];
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| &h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| &h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| &h.0));
|
||||
|
||||
Self::new(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Blocking Transport method for emails
|
||||
pub trait Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
self.send_raw(message.envelope(), &raw)
|
||||
}
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
/// async-std 1.x based Transport method for emails
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
pub trait AsyncStd1Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
// TODO take &Message
|
||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
let envelope = message.envelope();
|
||||
self.send_raw(&envelope, &raw).await
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
/// tokio 0.2.x based Transport method for emails
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
pub trait Tokio02Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
// TODO take &Message
|
||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
let envelope = message.envelope();
|
||||
self.send_raw(&envelope, &raw).await
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "builder")]
|
||||
mod test {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::*;
|
||||
use crate::message::{header, Mailbox, Mailboxes};
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::{header, header::Headers, Mailbox, Mailboxes};
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers() {
|
||||
@@ -231,9 +198,9 @@ mod test {
|
||||
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::Sender(sender));
|
||||
headers.set(header::To(to));
|
||||
headers.set(header::From::from(from));
|
||||
headers.set(header::Sender::from(sender));
|
||||
headers.set(header::To::from(to));
|
||||
|
||||
assert_eq!(
|
||||
Envelope::try_from(&headers).unwrap(),
|
||||
@@ -251,8 +218,8 @@ mod test {
|
||||
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::Sender(sender));
|
||||
headers.set(header::From::from(from));
|
||||
headers.set(header::Sender::from(sender));
|
||||
|
||||
assert!(Envelope::try_from(&headers).is_err(),);
|
||||
}
|
||||
|
||||
91
src/message/attachment.rs
Normal file
91
src/message/attachment.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::message::{
|
||||
header::{self, ContentType},
|
||||
IntoBody, SinglePart,
|
||||
};
|
||||
|
||||
/// `SinglePart` builder for attachments
|
||||
///
|
||||
/// Allows building attachment parts easily.
|
||||
#[derive(Clone)]
|
||||
pub struct Attachment {
|
||||
disposition: Disposition,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Disposition {
|
||||
/// file name
|
||||
Attached(String),
|
||||
/// content id
|
||||
Inline(String),
|
||||
}
|
||||
|
||||
impl Attachment {
|
||||
/// Creates a new attachment
|
||||
pub fn new(filename: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Attached(filename),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new inline attachment
|
||||
pub fn new_inline(content_id: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Inline(content_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the attachment part
|
||||
pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
|
||||
let mut builder = SinglePart::builder();
|
||||
builder = match self.disposition {
|
||||
Disposition::Attached(filename) => {
|
||||
builder.header(header::ContentDisposition::attachment(&filename))
|
||||
}
|
||||
Disposition::Inline(content_id) => builder
|
||||
.header(header::ContentId::from(format!("<{}>", content_id)))
|
||||
.header(header::ContentDisposition::inline()),
|
||||
};
|
||||
builder = builder.header(content_type);
|
||||
builder.body(content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::header::ContentType;
|
||||
|
||||
#[test]
|
||||
fn attachment() {
|
||||
let part = super::Attachment::new(String::from("test.txt")).body(
|
||||
String::from("Hello world!"),
|
||||
ContentType::parse("text/plain").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8_lossy(&part.formatted()),
|
||||
concat!(
|
||||
"Content-Disposition: attachment; filename=\"test.txt\"\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n\r\n",
|
||||
"Hello world!\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_inline() {
|
||||
let part = super::Attachment::new_inline(String::from("id")).body(
|
||||
String::from("Hello world!"),
|
||||
ContentType::parse("text/plain").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8_lossy(&part.formatted()),
|
||||
concat!(
|
||||
"Content-ID: <id>\r\n",
|
||||
"Content-Disposition: inline\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n\r\n",
|
||||
"Hello world!\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
661
src/message/body.rs
Normal file
661
src/message/body.rs
Normal file
@@ -0,0 +1,661 @@
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
mem,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::message::header::ContentTransferEncoding;
|
||||
|
||||
/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body that has already been encoded.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Body {
|
||||
buf: Vec<u8>,
|
||||
encoding: ContentTransferEncoding,
|
||||
}
|
||||
|
||||
/// Either a `Vec<u8>` or a `String`.
|
||||
///
|
||||
/// If the content is valid utf-8 a `String` should be passed, as it
|
||||
/// makes for a more efficient `Content-Transfer-Encoding` to be chosen.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MaybeString {
|
||||
/// Binary data
|
||||
Binary(Vec<u8>),
|
||||
/// UTF-8 string
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl Body {
|
||||
/// Encode the supplied `buf`, making it ready to be sent as a body.
|
||||
///
|
||||
/// Takes a `Vec<u8>` or a `String`.
|
||||
///
|
||||
/// Automatically chooses the most efficient encoding between
|
||||
/// `7bit`, `quoted-printable` and `base64`.
|
||||
///
|
||||
/// If `String` is passed, line endings are converted to `CRLF`.
|
||||
///
|
||||
/// If `buf` is valid utf-8 a `String` should be supplied, as `String`s
|
||||
/// can be encoded as `7bit` or `quoted-printable`, while `Vec<u8>` always
|
||||
/// get encoded as `base64`.
|
||||
pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
|
||||
let mut buf: MaybeString = buf.into();
|
||||
|
||||
let encoding = buf.encoding();
|
||||
buf.encode_crlf();
|
||||
Self::new_impl(buf.into(), encoding)
|
||||
}
|
||||
|
||||
/// Encode the supplied `buf`, using the provided `encoding`.
|
||||
///
|
||||
/// [`Body::new`] is generally the better option.
|
||||
///
|
||||
/// If `String` is passed, line endings are converted to `CRLF`.
|
||||
///
|
||||
/// Returns an [`Err`] giving back the supplied `buf`, in case the chosen
|
||||
/// encoding would have resulted into `buf` being encoded
|
||||
/// into an invalid body.
|
||||
pub fn new_with_encoding<B: Into<MaybeString>>(
|
||||
buf: B,
|
||||
encoding: ContentTransferEncoding,
|
||||
) -> Result<Self, Vec<u8>> {
|
||||
let mut buf: MaybeString = buf.into();
|
||||
|
||||
if !buf.is_encoding_ok(encoding) {
|
||||
return Err(buf.into());
|
||||
}
|
||||
|
||||
buf.encode_crlf();
|
||||
Ok(Self::new_impl(buf.into(), encoding))
|
||||
}
|
||||
|
||||
/// Builds a new `Body` using a pre-encoded buffer.
|
||||
///
|
||||
/// **Generally not you want.**
|
||||
///
|
||||
/// `buf` shouldn't contain non-ascii characters, lines longer than 1000 characters or nul bytes.
|
||||
#[inline]
|
||||
pub fn dangerous_pre_encoded(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
|
||||
Self { buf, encoding }
|
||||
}
|
||||
|
||||
/// Encodes the supplied `buf` using the provided `encoding`
|
||||
fn new_impl(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
|
||||
match encoding {
|
||||
ContentTransferEncoding::SevenBit
|
||||
| ContentTransferEncoding::EightBit
|
||||
| ContentTransferEncoding::Binary => Self { buf, encoding },
|
||||
ContentTransferEncoding::QuotedPrintable => {
|
||||
let encoded = quoted_printable::encode(buf);
|
||||
|
||||
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
|
||||
}
|
||||
ContentTransferEncoding::Base64 => {
|
||||
let base64_len = buf.len() * 4 / 3 + 4;
|
||||
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
|
||||
|
||||
let mut out = Vec::with_capacity(base64_endings_len);
|
||||
{
|
||||
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH);
|
||||
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
|
||||
|
||||
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails");
|
||||
|
||||
// modified Write::write_all to work around base64 crate bug
|
||||
// TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
|
||||
{
|
||||
let mut buf: &[u8] = buf.as_ref();
|
||||
while !buf.is_empty() {
|
||||
match writer.write(buf) {
|
||||
Ok(0) => {
|
||||
// ignore 0 writes
|
||||
}
|
||||
Ok(n) => {
|
||||
buf = &buf[n..];
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
|
||||
Err(e) => panic!("base64 encoding never fails: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::dangerous_pre_encoded(out, ContentTransferEncoding::Base64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the length of this `Body` in bytes.
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.buf.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if this `Body` has a length of zero, `false` otherwise.
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.buf.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the `Content-Transfer-Encoding` of this `Body`.
|
||||
#[inline]
|
||||
pub fn encoding(&self) -> ContentTransferEncoding {
|
||||
self.encoding
|
||||
}
|
||||
|
||||
/// Consumes `Body` and returns the inner `Vec<u8>`
|
||||
#[inline]
|
||||
pub fn into_vec(self) -> Vec<u8> {
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeString {
|
||||
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
|
||||
///
|
||||
/// If the `MaybeString` was created from a `String` composed only of US-ASCII
|
||||
/// characters, with no lines longer than 1000 characters, then 7bit
|
||||
/// encoding will be used, else quoted-printable will be chosen.
|
||||
///
|
||||
/// If the `MaybeString` was instead created from a `Vec<u8>`, base64 encoding is always
|
||||
/// chosen.
|
||||
///
|
||||
/// `8bit` and `binary` encodings are never returned, as they may not be
|
||||
/// supported by all SMTP servers.
|
||||
pub fn encoding(&self) -> ContentTransferEncoding {
|
||||
match &self {
|
||||
Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit,
|
||||
// TODO: consider when base64 would be a better option because of output size
|
||||
Self::String(_) => ContentTransferEncoding::QuotedPrintable,
|
||||
Self::Binary(_) => ContentTransferEncoding::Base64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode line endings to CRLF if the variant is `String`
|
||||
fn encode_crlf(&mut self) {
|
||||
match self {
|
||||
Self::String(string) => in_place_crlf_line_endings(string),
|
||||
Self::Binary(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`].
|
||||
///
|
||||
/// Used by [`MessageBuilder::body`][super::MessageBuilder::body] and
|
||||
/// [`SinglePartBuilder::body`][super::SinglePartBuilder::body],
|
||||
/// which can either take something that can be encoded into [`Body`]
|
||||
/// or a pre-encoded [`Body`].
|
||||
///
|
||||
/// If `encoding` is `None` the best encoding between `7bit`, `quoted-printable`
|
||||
/// and `base64` is chosen based on the input body. **Best option.**
|
||||
///
|
||||
/// If `encoding` is `Some` the supplied encoding is used.
|
||||
/// **NOTE:** if using the specified `encoding` would result into a malformed
|
||||
/// body, this will panic!
|
||||
pub trait IntoBody {
|
||||
/// Encode as valid body
|
||||
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body;
|
||||
}
|
||||
|
||||
impl<T> IntoBody for T
|
||||
where
|
||||
T: Into<MaybeString>,
|
||||
{
|
||||
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
|
||||
match encoding {
|
||||
Some(encoding) => Body::new_with_encoding(self, encoding).expect("invalid encoding"),
|
||||
None => Body::new(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoBody for Body {
|
||||
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
|
||||
let _ = encoding;
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Body {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.buf.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for MaybeString {
|
||||
#[inline]
|
||||
fn from(b: Vec<u8>) -> Self {
|
||||
Self::Binary(b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for MaybeString {
|
||||
#[inline]
|
||||
fn from(s: String) -> Self {
|
||||
Self::String(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MaybeString> for Vec<u8> {
|
||||
#[inline]
|
||||
fn from(s: MaybeString) -> Self {
|
||||
match s {
|
||||
MaybeString::Binary(b) => b,
|
||||
MaybeString::String(s) => s.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MaybeString {
|
||||
type Target = [u8];
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Binary(b) => b.as_ref(),
|
||||
Self::String(s) => s.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
fn in_place_crlf_line_endings(string: &mut String) {
|
||||
let indices = find_all_lf_char_indices(&string);
|
||||
|
||||
for i in indices {
|
||||
// this relies on `indices` being in reverse order
|
||||
string.insert(i, '\r');
|
||||
}
|
||||
}
|
||||
|
||||
/// Find indices to all places where `\r` should be inserted
|
||||
/// in order to make `s` have CRLF line endings
|
||||
///
|
||||
/// The list is reversed, which is more efficient.
|
||||
fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
|
||||
let mut indices = Vec::new();
|
||||
|
||||
let mut found_lf = false;
|
||||
for (i, c) in s.char_indices().rev() {
|
||||
if mem::take(&mut found_lf) && c != '\r' {
|
||||
// the previous character was `\n`, but this isn't a `\r`
|
||||
indices.push(i + c.len_utf8());
|
||||
}
|
||||
|
||||
found_lf = c == '\n';
|
||||
}
|
||||
|
||||
if found_lf {
|
||||
// the first character is `\n`
|
||||
indices.push(0);
|
||||
}
|
||||
|
||||
indices
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
|
||||
|
||||
#[test]
|
||||
fn seven_bit_detect() {
|
||||
let encoded = Body::new(String::from("Hello, world!"));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
|
||||
assert_eq!(encoded.as_ref(), b"Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_encode() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Hello, world!"),
|
||||
ContentTransferEncoding::SevenBit,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
|
||||
assert_eq!(encoded.as_ref(), b"Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_too_long_detect() {
|
||||
let encoded = Body::new("Hello, world!".repeat(100));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
|
||||
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
|
||||
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
|
||||
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
|
||||
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
|
||||
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
|
||||
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
|
||||
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
|
||||
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
|
||||
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
|
||||
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
|
||||
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
|
||||
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
|
||||
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
|
||||
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
|
||||
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
|
||||
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
|
||||
"ello, world!Hello, world!"
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_too_long_fail() {
|
||||
let result = Body::new_with_encoding(
|
||||
"Hello, world!".repeat(100),
|
||||
ContentTransferEncoding::SevenBit,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_too_long_encode_quotedprintable() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
"Hello, world!".repeat(100),
|
||||
ContentTransferEncoding::QuotedPrintable,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
|
||||
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
|
||||
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
|
||||
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
|
||||
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
|
||||
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
|
||||
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
|
||||
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
|
||||
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
|
||||
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
|
||||
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
|
||||
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
|
||||
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
|
||||
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
|
||||
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
|
||||
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
|
||||
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
|
||||
"ello, world!Hello, world!"
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_invalid() {
|
||||
let result = Body::new_with_encoding(
|
||||
String::from("Привет, мир!"),
|
||||
ContentTransferEncoding::SevenBit,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_bit_encode() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Привет, мир!"),
|
||||
ContentTransferEncoding::EightBit,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::EightBit);
|
||||
assert_eq!(encoded.as_ref(), "Привет, мир!".as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_bit_too_long_fail() {
|
||||
let result = Body::new_with_encoding(
|
||||
"Привет, мир!".repeat(200),
|
||||
ContentTransferEncoding::EightBit,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_detect() {
|
||||
let encoded = Body::new(String::from("Привет, мир!"));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_encode_ascii() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Hello, world!"),
|
||||
ContentTransferEncoding::QuotedPrintable,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(encoded.as_ref(), b"Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_encode_utf8() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Привет, мир!"),
|
||||
ContentTransferEncoding::QuotedPrintable,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_encode_line_wrap() {
|
||||
let encoded = Body::new(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
|
||||
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5"
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_detect() {
|
||||
let input = Body::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
let encoding = input.encoding();
|
||||
assert_eq!(encoding, ContentTransferEncoding::Base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_bytes() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
ContentTransferEncoding::Base64,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(encoded.as_ref(), b"AAECAwQFBgcICQ==");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_bytes_wrapping() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
|
||||
ContentTransferEncoding::Base64,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUG\r\n",
|
||||
"BwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQID\r\n",
|
||||
"BAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkA\r\n",
|
||||
"AQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAk="
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_ascii() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Hello World!"),
|
||||
ContentTransferEncoding::Base64,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(encoded.as_ref(), b"SGVsbG8gV29ybGQh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_ascii_wrapping() {
|
||||
let encoded =
|
||||
Body::new_with_encoding("Hello World!".repeat(20), ContentTransferEncoding::Base64)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"SGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29y\r\n",
|
||||
"bGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8g\r\n",
|
||||
"V29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVs\r\n",
|
||||
"bG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQh\r\n",
|
||||
"SGVsbG8gV29ybGQh"
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf() {
|
||||
let mut string = String::from("Send me a ✉️\nwith\nlettre!\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(string, "Send me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harsh_crlf() {
|
||||
let mut string = String::from("\n\nSend me a ✉️\r\n\nwith\n\nlettre!\n\r\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(
|
||||
string,
|
||||
"\r\n\r\nSend me a ✉️\r\n\r\nwith\r\n\r\nlettre!\r\n\r\n😀"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf_noop() {
|
||||
let mut string = String::from("\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(string, "\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
use crate::message::header::ContentTransferEncoding;
|
||||
|
||||
/// Encoder trait
|
||||
pub trait EncoderCodec: Send {
|
||||
/// Encode all data
|
||||
fn encode(&mut self, input: &[u8]) -> Vec<u8>;
|
||||
}
|
||||
|
||||
/// 7bit codec
|
||||
///
|
||||
/// WARNING: Panics when passed non-ascii chars
|
||||
struct SevenBitCodec {
|
||||
line_wrapper: EightBitCodec,
|
||||
}
|
||||
|
||||
impl SevenBitCodec {
|
||||
pub fn new() -> Self {
|
||||
SevenBitCodec {
|
||||
line_wrapper: EightBitCodec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for SevenBitCodec {
|
||||
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
|
||||
assert!(input.is_ascii(), "input must be valid ascii");
|
||||
|
||||
self.line_wrapper.encode(input)
|
||||
}
|
||||
}
|
||||
|
||||
/// Quoted-Printable codec
|
||||
///
|
||||
struct QuotedPrintableCodec();
|
||||
|
||||
impl QuotedPrintableCodec {
|
||||
pub fn new() -> Self {
|
||||
QuotedPrintableCodec()
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for QuotedPrintableCodec {
|
||||
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
|
||||
quoted_printable::encode(input)
|
||||
}
|
||||
}
|
||||
|
||||
/// Base64 codec
|
||||
///
|
||||
struct Base64Codec {
|
||||
line_wrapper: EightBitCodec,
|
||||
}
|
||||
|
||||
impl Base64Codec {
|
||||
pub fn new() -> Self {
|
||||
Base64Codec {
|
||||
// TODO probably 78, 76 is for qp
|
||||
line_wrapper: EightBitCodec::new().with_limit(78 - 2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for Base64Codec {
|
||||
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
|
||||
self.line_wrapper.encode(base64::encode(input).as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// 8bit codec
|
||||
///
|
||||
struct EightBitCodec {
|
||||
max_length: usize,
|
||||
}
|
||||
|
||||
const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
|
||||
|
||||
impl EightBitCodec {
|
||||
pub fn new() -> Self {
|
||||
EightBitCodec {
|
||||
max_length: DEFAULT_MAX_LINE_LENGTH,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_limit(mut self, max_length: usize) -> Self {
|
||||
self.max_length = max_length;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for EightBitCodec {
|
||||
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
|
||||
let ending = b"\r\n";
|
||||
let endings_len = input.len() / self.max_length * ending.len();
|
||||
let mut out = Vec::with_capacity(input.len() + endings_len);
|
||||
|
||||
for chunk in input.chunks(self.max_length) {
|
||||
// write the line ending after every chunk, except the last one
|
||||
if !out.is_empty() {
|
||||
out.extend_from_slice(ending);
|
||||
}
|
||||
|
||||
out.extend_from_slice(chunk);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Binary codec
|
||||
///
|
||||
struct BinaryCodec;
|
||||
|
||||
impl BinaryCodec {
|
||||
pub fn new() -> Self {
|
||||
BinaryCodec
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderCodec for BinaryCodec {
|
||||
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
|
||||
input.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec> {
|
||||
use self::ContentTransferEncoding::*;
|
||||
|
||||
match encoding {
|
||||
Some(SevenBit) => Box::new(SevenBitCodec::new()),
|
||||
Some(QuotedPrintable) => Box::new(QuotedPrintableCodec::new()),
|
||||
Some(Base64) => Box::new(Base64Codec::new()),
|
||||
Some(EightBit) => Box::new(EightBitCodec::new()),
|
||||
Some(Binary) | None => Box::new(BinaryCodec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn seven_bit_encode() {
|
||||
let mut c = SevenBitCodec::new();
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
|
||||
"Hello, world!"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn seven_bit_encode_panic() {
|
||||
let mut c = SevenBitCodec::new();
|
||||
c.encode("Hello, мир!".as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_encode() {
|
||||
let mut c = QuotedPrintableCodec::new();
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
|
||||
"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!"
|
||||
);
|
||||
|
||||
assert_eq!(&String::from_utf8(c.encode("Текст письма в уникоде".as_bytes())).unwrap(),
|
||||
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode() {
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
|
||||
"0J/RgNC40LLQtdGCLCDQvNC40YAh"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Текст письма в уникоде подлиннее.".as_bytes())).unwrap(),
|
||||
concat!(
|
||||
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ",
|
||||
"vtC00LUg0L/QvtC00LvQuNC90L3Q\r\ntdC1Lg=="
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode(
|
||||
"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую.".as_bytes()
|
||||
)).unwrap(),
|
||||
|
||||
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
|
||||
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
|
||||
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
|
||||
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=")
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode(
|
||||
"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это.".as_bytes()
|
||||
)).unwrap(),
|
||||
|
||||
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
|
||||
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
|
||||
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
|
||||
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRjiDRjdGC\r\n",
|
||||
"0L4u")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encodeed() {
|
||||
let mut c = Base64Codec::new();
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Chunk.".as_bytes())).unwrap(),
|
||||
"Q2h1bmsu"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_bit_encode() {
|
||||
let mut c = EightBitCodec::new();
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
|
||||
"Hello, world!"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
|
||||
"Hello, мир!"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_encode() {
|
||||
let mut c = BinaryCodec::new();
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
|
||||
"Hello, world!"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
|
||||
"Hello, мир!"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,52 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
|
||||
str::{from_utf8, FromStr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use super::{Header, HeaderName};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Transfer-Encoding` of the body
|
||||
///
|
||||
/// The `Message` builder takes care of choosing the most
|
||||
/// efficient encoding based on the chosen body, so in most
|
||||
/// use-caches this header shouldn't be set manually.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ContentTransferEncoding {
|
||||
/// ASCII
|
||||
SevenBit,
|
||||
/// Quoted-Printable encoding
|
||||
QuotedPrintable,
|
||||
/// base64 encoding
|
||||
Base64,
|
||||
// 8BITMIME
|
||||
/// Requires `8BITMIME`
|
||||
EightBit,
|
||||
/// Binary data
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::SevenBit
|
||||
impl Header for ContentTransferEncoding {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(s.parse()?)
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
self.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTransferEncoding {
|
||||
fn fmt(&self, f: &mut FmtFormatter) -> FmtResult {
|
||||
use self::ContentTransferEncoding::*;
|
||||
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
|
||||
f.write_str(match *self {
|
||||
SevenBit => "7bit",
|
||||
QuotedPrintable => "quoted-printable",
|
||||
Base64 => "base64",
|
||||
EightBit => "8bit",
|
||||
Binary => "binary",
|
||||
Self::SevenBit => "7bit",
|
||||
Self::QuotedPrintable => "quoted-printable",
|
||||
Self::Base64 => "base64",
|
||||
Self::EightBit => "8bit",
|
||||
Self::Binary => "binary",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -39,47 +54,27 @@ impl Display for ContentTransferEncoding {
|
||||
impl FromStr for ContentTransferEncoding {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use self::ContentTransferEncoding::*;
|
||||
match s {
|
||||
"7bit" => Ok(SevenBit),
|
||||
"quoted-printable" => Ok(QuotedPrintable),
|
||||
"base64" => Ok(Base64),
|
||||
"8bit" => Ok(EightBit),
|
||||
"binary" => Ok(Binary),
|
||||
"7bit" => Ok(Self::SevenBit),
|
||||
"quoted-printable" => Ok(Self::QuotedPrintable),
|
||||
"base64" => Ok(Self::Base64),
|
||||
"8bit" => Ok(Self::EightBit),
|
||||
"binary" => Ok(Self::Binary),
|
||||
_ => Err(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentTransferEncoding {
|
||||
fn header_name() -> &'static str {
|
||||
"Content-Transfer-Encoding"
|
||||
}
|
||||
|
||||
// FIXME HeaderError->HeaderError, same for result
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header))
|
||||
.and_then(|s| {
|
||||
s.parse::<ContentTransferEncoding>()
|
||||
.map_err(|_| HeaderError::Header)
|
||||
})
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&format!("{}", self))
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::Base64
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ContentTransferEncoding;
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::header::{HeaderName, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_transfer_encoding() {
|
||||
@@ -87,35 +82,35 @@ mod test {
|
||||
|
||||
headers.set(ContentTransferEncoding::SevenBit);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: 7bit\r\n"
|
||||
);
|
||||
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: 7bit\r\n");
|
||||
|
||||
headers.set(ContentTransferEncoding::Base64);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: base64\r\n"
|
||||
);
|
||||
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: base64\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("Content-Transfer-Encoding", "7bit");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::SevenBit)
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"7bit".to_string(),
|
||||
);
|
||||
|
||||
headers.set_raw("Content-Transfer-Encoding", "base64");
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(ContentTransferEncoding::SevenBit)
|
||||
);
|
||||
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"base64".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::Base64)
|
||||
Some(ContentTransferEncoding::Base64)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
89
src/message/header/content_disposition.rs
Normal file
89
src/message/header/content_disposition.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use super::{Header, HeaderName};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Disposition` of an attachment
|
||||
///
|
||||
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ContentDisposition(String);
|
||||
|
||||
impl ContentDisposition {
|
||||
/// An attachment which should be displayed inline into the message
|
||||
pub fn inline() -> Self {
|
||||
Self("inline".into())
|
||||
}
|
||||
|
||||
/// An attachment which should be displayed inline into the message, but that also
|
||||
/// species the filename in case it were to be downloaded
|
||||
pub fn inline_with_name(file_name: &str) -> Self {
|
||||
debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'");
|
||||
Self(format!("inline; filename=\"{}\"", file_name))
|
||||
}
|
||||
|
||||
/// An attachment which is separate from the body of the message, and can be downloaded separately
|
||||
pub fn attachment(file_name: &str) -> Self {
|
||||
debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'");
|
||||
Self(format!("attachment; filename=\"{}\"", file_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentDisposition {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Disposition")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(Self(s.into()))
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ContentDisposition;
|
||||
use crate::message::header::{HeaderName, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_disposition() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentDisposition::inline());
|
||||
|
||||
assert_eq!(format!("{}", headers), "Content-Disposition: inline\r\n");
|
||||
|
||||
headers.set(ContentDisposition::attachment("something.txt"));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_disposition() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||
"inline".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentDisposition>(),
|
||||
Some(ContentDisposition::inline())
|
||||
);
|
||||
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||
"attachment; filename=\"something.txt\"".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentDisposition>(),
|
||||
Some(ContentDisposition::attachment("something.txt"))
|
||||
);
|
||||
}
|
||||
}
|
||||
123
src/message/header/content_type.rs
Normal file
123
src/message/header/content_type.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use super::{Header, HeaderName};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Type` of the body
|
||||
///
|
||||
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ContentType(Mime);
|
||||
|
||||
impl ContentType {
|
||||
/// A `ContentType` of type `text/plain; charset=utf-8`
|
||||
///
|
||||
/// Indicates that the body is in utf-8 encoded plain text.
|
||||
pub const TEXT_PLAIN: ContentType = Self::from_mime(mime::TEXT_PLAIN_UTF_8);
|
||||
|
||||
/// A `ContentType` of type `text/html; charset=utf-8`
|
||||
///
|
||||
/// Indicates that the body is in utf-8 encoded html.
|
||||
pub const TEXT_HTML: ContentType = Self::from_mime(mime::TEXT_HTML_UTF_8);
|
||||
|
||||
/// Parse `s` into `ContentType`
|
||||
pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
|
||||
Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
|
||||
}
|
||||
|
||||
pub(crate) const fn from_mime(mime: Mime) -> Self {
|
||||
Self(mime)
|
||||
}
|
||||
|
||||
pub(crate) fn as_ref(&self) -> &Mime {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentType {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Type")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(Self(s.parse()?))
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ContentType {
|
||||
type Err = ContentTypeErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error occurred while trying to [`ContentType::parse`].
|
||||
#[derive(Debug)]
|
||||
pub struct ContentTypeErr(mime::FromStrError);
|
||||
|
||||
impl StdError for ContentTypeErr {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTypeErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ContentType;
|
||||
use crate::message::header::{HeaderName, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_type() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentType::TEXT_PLAIN);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
);
|
||||
|
||||
headers.set(ContentType::TEXT_HTML);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Content-Type: text/html; charset=utf-8\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_type() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Content-Type"),
|
||||
"text/plain; charset=utf-8".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
|
||||
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Content-Type"),
|
||||
"text/html; charset=utf-8".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
|
||||
}
|
||||
}
|
||||
133
src/message/header/date.rs
Normal file
133
src/message/header/date.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use httpdate::HttpDate;
|
||||
|
||||
use super::{Header, HeaderName};
|
||||
use crate::BoxError;
|
||||
|
||||
/// Message `Date` header
|
||||
///
|
||||
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Date(HttpDate);
|
||||
|
||||
impl Date {
|
||||
/// Build a `Date` from [`SystemTime`]
|
||||
pub fn new(st: SystemTime) -> Self {
|
||||
Self(st.into())
|
||||
}
|
||||
|
||||
/// Get the current date
|
||||
///
|
||||
/// Shortcut for `Date::new(SystemTime::now())`
|
||||
pub fn now() -> Self {
|
||||
Self::new(SystemTime::now())
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for Date {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Date")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mut s = String::from(s);
|
||||
if s.ends_with(" -0000") {
|
||||
// The httpdate crate expects the `Date` to end in ` GMT`, but email
|
||||
// uses `-0000`, so we crudely fix this issue here.
|
||||
|
||||
s.truncate(s.len() - "-0000".len());
|
||||
s.push_str("GMT");
|
||||
}
|
||||
|
||||
Ok(Self(s.parse::<HttpDate>()?))
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
let mut s = self.0.to_string();
|
||||
if s.ends_with(" GMT") {
|
||||
// The httpdate crate always appends ` GMT` to the end of the string,
|
||||
// but this is considered an obsolete date format for email
|
||||
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
|
||||
// so we replace `GMT` with `-0000`
|
||||
s.truncate(s.len() - "GMT".len());
|
||||
s.push_str("-0000");
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemTime> for Date {
|
||||
fn from(st: SystemTime) -> Self {
|
||||
Self::new(st)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Date> for SystemTime {
|
||||
fn from(this: Date) -> SystemTime {
|
||||
this.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use super::Date;
|
||||
use crate::message::header::{HeaderName, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_date() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
headers.set(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n".to_string()
|
||||
);
|
||||
|
||||
// Tue, 15 Nov 1994 08:12:32 GMT
|
||||
headers.set(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Date: Tue, 15 Nov 1994 08:12:32 -0000\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_date() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
"Tue, 15 Nov 1994 08:12:31 -0000".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Date>(),
|
||||
Some(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
|
||||
))
|
||||
);
|
||||
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
"Tue, 15 Nov 1994 08:12:32 -0000".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Date>(),
|
||||
Some(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
use crate::message::{
|
||||
mailbox::{Mailbox, Mailboxes},
|
||||
utf8_b,
|
||||
use super::{Header, HeaderName};
|
||||
use crate::{
|
||||
message::mailbox::{Mailbox, Mailboxes},
|
||||
BoxError,
|
||||
};
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, slice::Iter, str::from_utf8};
|
||||
|
||||
/// Header which can contains multiple mailboxes
|
||||
pub trait MailboxesHeader {
|
||||
@@ -17,26 +13,34 @@ macro_rules! mailbox_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailbox);
|
||||
pub struct $type_name(Mailbox);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self> where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized {
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_mailboxes)
|
||||
.and_then(|mbs| {
|
||||
mbs.into_single().ok_or(HeaderError::Header)
|
||||
}).map($type_name)
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mailbox: Mailbox = s.parse()?;
|
||||
Ok(Self(mailbox))
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&self.0.recode_name(utf8_b::encode))
|
||||
fn display(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Mailbox> for $type_name {
|
||||
#[inline]
|
||||
fn from(mailbox: Mailbox) -> Self {
|
||||
Self(mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<$type_name> for Mailbox {
|
||||
#[inline]
|
||||
fn from(this: $type_name) -> Mailbox {
|
||||
this.0
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -46,7 +50,7 @@ macro_rules! mailboxes_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailboxes);
|
||||
pub struct $type_name(pub(crate) Mailboxes);
|
||||
|
||||
impl MailboxesHeader for $type_name {
|
||||
fn join_mailboxes(&mut self, other: Self) {
|
||||
@@ -55,23 +59,31 @@ macro_rules! mailboxes_header {
|
||||
}
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_mailboxes)
|
||||
.map($type_name)
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mailbox: Mailboxes = s.parse()?;
|
||||
Ok(Self(mailbox))
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
format_mailboxes(self.0.iter(), f)
|
||||
fn display(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Mailboxes> for $type_name {
|
||||
#[inline]
|
||||
fn from(mailboxes: Mailboxes) -> Self {
|
||||
Self(mailboxes)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<$type_name> for Mailboxes {
|
||||
#[inline]
|
||||
fn from(this: $type_name) -> Mailboxes {
|
||||
this.0
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -146,26 +158,10 @@ mailboxes_header! {
|
||||
(Bcc, "Bcc")
|
||||
}
|
||||
|
||||
fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
|
||||
if let Ok(src) = from_utf8(raw) {
|
||||
if let Ok(mbs) = src.parse() {
|
||||
return Ok(mbs);
|
||||
}
|
||||
}
|
||||
Err(HeaderError::Header)
|
||||
}
|
||||
|
||||
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&Mailboxes::from(
|
||||
mbs.map(|mb| mb.recode_name(utf8_b::encode))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{From, Mailbox, Mailboxes};
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::header::{HeaderName, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_single_without_name() {
|
||||
@@ -174,7 +170,7 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n");
|
||||
assert_eq!(headers.to_string(), "From: kayo@example.com\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -184,7 +180,7 @@ mod test {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n");
|
||||
assert_eq!(headers.to_string(), "From: K. <kayo@example.com>\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -197,7 +193,7 @@ mod test {
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
headers.to_string(),
|
||||
"From: kayo@example.com, pony@domain.tld\r\n"
|
||||
);
|
||||
}
|
||||
@@ -213,7 +209,7 @@ mod test {
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
headers.to_string(),
|
||||
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
|
||||
);
|
||||
}
|
||||
@@ -226,7 +222,7 @@ mod test {
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
headers.to_string(),
|
||||
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
@@ -236,9 +232,12 @@ mod test {
|
||||
let from = vec!["kayo@example.com".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "kayo@example.com");
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"kayo@example.com".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from)));
|
||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -246,9 +245,12 @@ mod test {
|
||||
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "K. <kayo@example.com>");
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"K. <kayo@example.com>".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from)));
|
||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -259,9 +261,12 @@ mod test {
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "kayo@example.com, pony@domain.tld");
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"kayo@example.com, pony@domain.tld".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -272,18 +277,11 @@ mod test {
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_with_utf8_name() {
|
||||
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,800 @@
|
||||
/*!
|
||||
//! Headers widely used in email messages
|
||||
|
||||
## Headers widely used in email messages
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
error::Error,
|
||||
fmt::{self, Display, Formatter},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
*/
|
||||
pub use self::{
|
||||
content::*,
|
||||
content_disposition::ContentDisposition,
|
||||
content_type::{ContentType, ContentTypeErr},
|
||||
date::Date,
|
||||
mailbox::*,
|
||||
special::*,
|
||||
textual::*,
|
||||
};
|
||||
use crate::BoxError;
|
||||
|
||||
mod content;
|
||||
mod content_disposition;
|
||||
mod content_type;
|
||||
mod date;
|
||||
mod mailbox;
|
||||
mod special;
|
||||
mod textual;
|
||||
|
||||
pub use self::{content::*, mailbox::*, special::*, textual::*};
|
||||
pub trait Header: Clone {
|
||||
fn name() -> HeaderName;
|
||||
|
||||
pub use hyperx::header::{
|
||||
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
|
||||
DispositionType, Header, Headers, HttpDate as EmailDate,
|
||||
};
|
||||
fn parse(s: &str) -> Result<Self, BoxError>;
|
||||
|
||||
fn display(&self) -> String;
|
||||
}
|
||||
|
||||
/// A set of email headers
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Headers {
|
||||
headers: Vec<(HeaderName, String)>,
|
||||
}
|
||||
|
||||
impl Headers {
|
||||
/// Create an empty `Headers`
|
||||
///
|
||||
/// This function does not allocate.
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
headers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty `Headers` with a pre-allocated capacity
|
||||
///
|
||||
/// Pre-allocates a capacity of at least `capacity`.
|
||||
#[inline]
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
headers: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a copy of an `Header` present in `Headers`
|
||||
///
|
||||
/// Returns `None` if `Header` isn't present in `Headers`.
|
||||
pub fn get<H: Header>(&self) -> Option<H> {
|
||||
self.get_raw(&H::name()).and_then(|raw| H::parse(raw).ok())
|
||||
}
|
||||
|
||||
/// Sets `Header` into `Headers`, overriding `Header` if it
|
||||
/// was already present in `Headers`
|
||||
pub fn set<H: Header>(&mut self, header: H) {
|
||||
self.insert_raw(H::name(), header.display());
|
||||
}
|
||||
|
||||
/// Remove `Header` from `Headers`, returning it
|
||||
///
|
||||
/// Returns `None` if `Header` isn't in `Headers`.
|
||||
pub fn remove<H: Header>(&mut self) -> Option<H> {
|
||||
self.remove_raw(&H::name())
|
||||
.and_then(|(_name, raw)| H::parse(&raw).ok())
|
||||
}
|
||||
|
||||
/// Clears `Headers`, removing all headers from it
|
||||
///
|
||||
/// Any pre-allocated capacity is left untouched.
|
||||
#[inline]
|
||||
pub fn clear(&mut self) {
|
||||
self.headers.clear();
|
||||
}
|
||||
|
||||
/// Returns a reference to the raw value of header `name`
|
||||
///
|
||||
/// Returns `None` if `name` isn't present in `Headers`.
|
||||
pub fn get_raw(&self, name: &str) -> Option<&str> {
|
||||
self.find_header(name).map(|(_name, value)| value)
|
||||
}
|
||||
|
||||
/// Inserts a raw header into `Headers`, overriding `value` if it
|
||||
/// was already present in `Headers`.
|
||||
pub fn insert_raw(&mut self, name: HeaderName, value: String) {
|
||||
match self.find_header_mut(&name) {
|
||||
Some((_, current_value)) => {
|
||||
*current_value = value;
|
||||
}
|
||||
None => {
|
||||
self.headers.push((name, 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
|
||||
///
|
||||
/// Returns `None` if `name` isn't present in `Headers`.
|
||||
pub fn remove_raw(&mut self, name: &str) -> Option<(HeaderName, String)> {
|
||||
self.find_header_index(name).map(|i| self.headers.remove(i))
|
||||
}
|
||||
|
||||
fn find_header(&self, name: &str) -> Option<(&HeaderName, &str)> {
|
||||
self.headers
|
||||
.iter()
|
||||
.find(|&(name_, _value)| name.eq_ignore_ascii_case(name_))
|
||||
.map(|t| (&t.0, t.1.as_str()))
|
||||
}
|
||||
|
||||
fn find_header_mut(&mut self, name: &str) -> Option<(&HeaderName, &mut String)> {
|
||||
self.headers
|
||||
.iter_mut()
|
||||
.find(|(name_, _value)| name.eq_ignore_ascii_case(name_))
|
||||
.map(|t| (&t.0, &mut t.1))
|
||||
}
|
||||
|
||||
fn find_header_index(&self, name: &str) -> Option<usize> {
|
||||
self.headers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|&(_i, (name_, _value))| name.eq_ignore_ascii_case(name_))
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Headers {
|
||||
/// Formats `Headers`, ready to put them into an email
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (name, value) in &self.headers {
|
||||
Display::fmt(name, f)?;
|
||||
f.write_str(": ")?;
|
||||
HeaderValueEncoder::encode(&name, &value, f)?;
|
||||
f.write_str("\r\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A possible error when converting a `HeaderName` from another type.
|
||||
// comes from `http` crate
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct InvalidHeaderName {
|
||||
_priv: (),
|
||||
}
|
||||
|
||||
impl fmt::Debug for InvalidHeaderName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InvalidHeaderName")
|
||||
// skip _priv noise
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidHeaderName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("invalid header name")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for InvalidHeaderName {}
|
||||
|
||||
/// A valid header name
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeaderName(Cow<'static, str>);
|
||||
|
||||
impl HeaderName {
|
||||
/// Creates a new header name
|
||||
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
|
||||
if !ascii.is_empty()
|
||||
&& ascii.len() <= 76
|
||||
&& ascii.is_ascii()
|
||||
&& !ascii.contains(|c| c == ':' || c == ' ')
|
||||
{
|
||||
Ok(Self(Cow::Owned(ascii)))
|
||||
} else {
|
||||
Err(InvalidHeaderName { _priv: () })
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new header name, panics on invalid name
|
||||
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
|
||||
macro_rules! static_assert {
|
||||
($condition:expr) => {
|
||||
let _ = [()][(!($condition)) as usize];
|
||||
};
|
||||
}
|
||||
|
||||
static_assert!(!ascii.is_empty());
|
||||
static_assert!(ascii.len() <= 76);
|
||||
|
||||
let bytes = ascii.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
static_assert!(bytes[i].is_ascii());
|
||||
static_assert!(bytes[i] != b' ');
|
||||
static_assert!(bytes[i] != b':');
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Self(Cow::Borrowed(ascii))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HeaderName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for HeaderName {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for HeaderName {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
let s: &str = self.as_ref();
|
||||
s.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for HeaderName {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HeaderName> for HeaderName {
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
let s1: &str = self.as_ref();
|
||||
let s2: &str = other.as_ref();
|
||||
s1 == s2
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for HeaderName {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
let s: &str = self.as_ref();
|
||||
s == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HeaderName> for &str {
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
let s: &str = other.as_ref();
|
||||
*self == s
|
||||
}
|
||||
}
|
||||
|
||||
const ENCODING_START_PREFIX: &str = "=?utf-8?b?";
|
||||
const ENCODING_END_SUFFIX: &str = "?=";
|
||||
const MAX_LINE_LEN: usize = 76;
|
||||
|
||||
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
|
||||
struct HeaderValueEncoder {
|
||||
line_len: usize,
|
||||
encode_buf: String,
|
||||
}
|
||||
|
||||
impl HeaderValueEncoder {
|
||||
fn encode(name: &str, value: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let (words_iter, encoder) = Self::new(name, value);
|
||||
encoder.format(words_iter, f)
|
||||
}
|
||||
|
||||
fn new<'a>(name: &str, value: &'a str) -> (WordsPlusFillIterator<'a>, Self) {
|
||||
(
|
||||
WordsPlusFillIterator { s: value },
|
||||
Self {
|
||||
line_len: name.len() + ": ".len(),
|
||||
encode_buf: String::new(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn format(
|
||||
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 {
|
||||
let allowed = allowed_str(next_word);
|
||||
|
||||
if allowed {
|
||||
// This word only contains allowed characters
|
||||
|
||||
// the next word is allowed, but we may have accumulated some words to encode
|
||||
self.flush_encode_buf(f, true)?;
|
||||
|
||||
if next_word.len() > self.remaining_line_len() {
|
||||
// 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 {
|
||||
// This word contains unallowed characters
|
||||
|
||||
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() {
|
||||
if self.remaining_line_len() <= base64_len(1) {
|
||||
self.flush_encode_buf(f, false)?;
|
||||
self.new_line(f)?;
|
||||
}
|
||||
|
||||
let mut len = available_len_to_max_encode_len(self.remaining_line_len())
|
||||
.min(next_word.len());
|
||||
// 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)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the number of bytes left for the current line
|
||||
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() {
|
||||
// nothing to encode
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut write_after = None;
|
||||
|
||||
if switching_to_allowed {
|
||||
// If the next word only contains allowed characters, and the string to encode
|
||||
// ends with a space, take the space out of the part to encode
|
||||
|
||||
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();
|
||||
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 {
|
||||
s.chars().all(allowed_char)
|
||||
}
|
||||
|
||||
const fn allowed_char(c: char) -> bool {
|
||||
c >= 1 as char && c <= 9 as char
|
||||
|| c == 11 as char
|
||||
|| c == 12 as char
|
||||
|| c >= 14 as char && c <= 127 as char
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{HeaderName, Headers};
|
||||
|
||||
#[test]
|
||||
fn valid_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ascii_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("🌎")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spaces_in_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From ")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colons_in_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From:")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn const_valid_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_non_ascii_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("🌎");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_spaces_in_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_colons_in_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From:");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_empty_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("");
|
||||
}
|
||||
|
||||
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
|
||||
|
||||
#[test]
|
||||
fn format_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: John Doe <example@example.com>, Jean Dupont <jean@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
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(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith \r\n",
|
||||
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand \r\n",
|
||||
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_long_line() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello! This is lettre, and this \r\n ",
|
||||
"IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n",
|
||||
" guess that's it!\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_very_long_line() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n",
|
||||
" ouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_giant_word() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n",
|
||||
" mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n",
|
||||
" ghijklmnopqrstuvwxyz\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"Seán <sean@example.com>".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special_emoji() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"🌎 <world@example.com>".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: =?utf-8?b?8J+Mjg==?= <world@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special_with_folding() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
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(),
|
||||
);
|
||||
|
||||
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?0JjQstCw0L3QvtCyIA==?=\r\n",
|
||||
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
|
||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
|
||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_slice_on_char_boundary_bug() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_bad_stuff() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! \r\n This is \" bad \0. 👋".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Subject: Hello! =?utf-8?b?DQo=?= This is \" bad =?utf-8?b?AC4g8J+Riw==?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_everything() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
|
||||
);
|
||||
headers.insert_raw(
|
||||
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(),
|
||||
);
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"Someone <somewhere@example.com>".to_string(),
|
||||
);
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"quoted-printable".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello! This is lettre, and this \r\n",
|
||||
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n",
|
||||
" guess that's it!\r\n",
|
||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
|
||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
|
||||
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
|
||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
|
||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
||||
"From: Someone <somewhere@example.com>\r\n",
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,55 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
use crate::{
|
||||
message::header::{Header, HeaderName},
|
||||
BoxError,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct MimeVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
major: u8,
|
||||
minor: u8,
|
||||
}
|
||||
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
|
||||
|
||||
impl MimeVersion {
|
||||
pub fn new(major: u8, minor: u8) -> Self {
|
||||
pub const fn new(major: u8, minor: u8) -> Self {
|
||||
MimeVersion { major, minor }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn major(self) -> u8 {
|
||||
self.major
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn minor(self) -> u8 {
|
||||
self.minor
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for MimeVersion {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("MIME-Version")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mut s = s.split('.');
|
||||
|
||||
let major = s
|
||||
.next()
|
||||
.expect("The first call to next for a Split<char> always succeeds");
|
||||
let minor = s
|
||||
.next()
|
||||
.ok_or_else(|| String::from("MIME-Version header doesn't contain '.'"))?;
|
||||
let major = major.parse()?;
|
||||
let minor = minor.parse()?;
|
||||
Ok(MimeVersion::new(major, minor))
|
||||
}
|
||||
|
||||
fn display(&self) -> String {
|
||||
format!("{}.{}", self.major, self.minor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MimeVersion {
|
||||
@@ -24,36 +58,10 @@ impl Default for MimeVersion {
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for MimeVersion {
|
||||
fn header_name() -> &'static str {
|
||||
"MIME-Version"
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one().ok_or(HeaderError::Header).and_then(|r| {
|
||||
let mut s = from_utf8(r).map_err(|_| HeaderError::Header)?.split('.');
|
||||
|
||||
let major = s.next().ok_or(HeaderError::Header)?;
|
||||
let minor = s.next().ok_or(HeaderError::Header)?;
|
||||
let major = major.parse().map_err(|_| HeaderError::Header)?;
|
||||
let minor = minor.parse().map_err(|_| HeaderError::Header)?;
|
||||
Ok(MimeVersion::new(major, minor))
|
||||
})
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&format!("{}.{}", self.major, self.minor))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{MimeVersion, MIME_VERSION_1_0};
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::header::{HeaderName, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_mime_version() {
|
||||
@@ -61,23 +69,29 @@ mod test {
|
||||
|
||||
headers.set(MIME_VERSION_1_0);
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
|
||||
assert_eq!(headers.to_string(), "MIME-Version: 1.0\r\n");
|
||||
|
||||
headers.set(MimeVersion::new(0, 1));
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
|
||||
assert_eq!(headers.to_string(), "MIME-Version: 0.1\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("MIME-Version", "1.0");
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("MIME-Version"),
|
||||
"1.0".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0));
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
|
||||
|
||||
headers.set_raw("MIME-Version", "0.1");
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("MIME-Version"),
|
||||
"0.1".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,99 @@
|
||||
use crate::message::utf8_b;
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
use super::{Header, HeaderName};
|
||||
use crate::BoxError;
|
||||
|
||||
macro_rules! text_header {
|
||||
( $type_name: ident, $header_name: expr ) => {
|
||||
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
|
||||
$(#[$attr])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub String);
|
||||
pub struct $type_name(String);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_text)
|
||||
.map($type_name)
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(Self(s.into()))
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
fmt_text(&self.0, f)
|
||||
fn display(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for $type_name {
|
||||
#[inline]
|
||||
fn from(text: String) -> Self {
|
||||
Self(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for $type_name {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
text_header!(Subject, "Subject");
|
||||
text_header!(Comments, "Comments");
|
||||
text_header!(Keywords, "Keywords");
|
||||
text_header!(InReplyTo, "In-Reply-To");
|
||||
text_header!(References, "References");
|
||||
text_header!(MessageId, "Message-Id");
|
||||
text_header!(UserAgent, "User-Agent");
|
||||
|
||||
fn parse_text(raw: &[u8]) -> HyperResult<String> {
|
||||
if let Ok(src) = from_utf8(raw) {
|
||||
if let Some(txt) = utf8_b::decode(src) {
|
||||
return Ok(txt);
|
||||
}
|
||||
}
|
||||
Err(HeaderError::Header)
|
||||
text_header!(
|
||||
/// `Subject` of the message, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
|
||||
Header(Subject, "Subject")
|
||||
);
|
||||
text_header!(
|
||||
/// `Comments` of the message, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
|
||||
Header(Comments, "Comments")
|
||||
);
|
||||
text_header!(
|
||||
/// `Keywords` header. Should contain a comma-separated list of one or more
|
||||
/// words or quoted-strings, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
|
||||
Header(Keywords, "Keywords")
|
||||
);
|
||||
text_header!(
|
||||
/// `In-Reply-To` header. Contains one or more
|
||||
/// unique message identifiers,
|
||||
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
Header(InReplyTo, "In-Reply-To")
|
||||
);
|
||||
text_header!(
|
||||
/// `References` header. Contains one or more
|
||||
/// unique message identifiers,
|
||||
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
Header(References, "References")
|
||||
);
|
||||
text_header!(
|
||||
/// `Message-Id` header. Contains a unique message identifier,
|
||||
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
Header(MessageId, "Message-ID")
|
||||
);
|
||||
text_header!(
|
||||
/// `User-Agent` header. Contains information about the client,
|
||||
/// defined in [draft-melnikov-email-user-agent-00](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00#section-3)
|
||||
Header(UserAgent, "User-Agent")
|
||||
);
|
||||
text_header! {
|
||||
/// `Content-Id` header,
|
||||
/// defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
|
||||
Header(ContentId, "Content-ID")
|
||||
}
|
||||
|
||||
fn fmt_text(s: &str, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&utf8_b::encode(s))
|
||||
text_header! {
|
||||
/// `Content-Location` header,
|
||||
/// defined in [RFC2110](https://tools.ietf.org/html/rfc2110#section-4.3)
|
||||
Header(ContentLocation, "Content-Location")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Subject;
|
||||
use hyperx::header::Headers;
|
||||
use crate::message::header::{HeaderName, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Sample subject".into()));
|
||||
|
||||
assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n");
|
||||
assert_eq!(headers.to_string(), "Subject: Sample subject\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -73,7 +102,7 @@ mod test {
|
||||
headers.set(Subject("Тема сообщения".into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
headers.to_string(),
|
||||
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
|
||||
);
|
||||
}
|
||||
@@ -81,25 +110,14 @@ mod test {
|
||||
#[test]
|
||||
fn parse_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("Subject", "Sample subject");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Sample subject".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_utf8() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw(
|
||||
"Subject",
|
||||
"=?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=",
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Sample subject".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Тема сообщения".into()))
|
||||
Some(Subject("Sample subject".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ impl<'de> Deserialize<'de> for Mailbox {
|
||||
enum Field {
|
||||
Name,
|
||||
Email,
|
||||
};
|
||||
}
|
||||
|
||||
const FIELDS: &[&str] = &["name", "email"];
|
||||
|
||||
@@ -37,7 +37,7 @@ impl<'de> Deserialize<'de> for Mailbox {
|
||||
impl<'de> Visitor<'de> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("'name' or 'email'")
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ impl<'de> Deserialize<'de> for Mailbox {
|
||||
impl<'de> Visitor<'de> for MailboxVisitor {
|
||||
type Value = Mailbox;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("mailbox string or object")
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ impl<'de> Deserialize<'de> for Mailboxes {
|
||||
impl<'de> Visitor<'de> for MailboxesVisitor {
|
||||
type Value = Mailboxes;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("mailboxes string or sequence")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
address::{Address, AddressError},
|
||||
message::utf8_b,
|
||||
};
|
||||
use crate::address::{Address, AddressError};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||
@@ -9,37 +6,67 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Email address with optional addressee name
|
||||
/// Represents an email address with an optional name for the sender/recipient.
|
||||
///
|
||||
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can create a `Mailbox` from a string and an [`Address`]:
|
||||
///
|
||||
/// ```
|
||||
/// # use lettre::{Address, message::Mailbox};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let mailbox = Mailbox::new(None, address);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can also create one from a string literal:
|
||||
///
|
||||
/// ```
|
||||
/// # use lettre::message::Mailbox;
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let mailbox: Mailbox = "John Smith <example@email.com>".parse()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Mailbox {
|
||||
/// User name part
|
||||
/// The name associated with the address.
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Email address part
|
||||
/// The email address itself.
|
||||
pub email: Address,
|
||||
}
|
||||
|
||||
impl Mailbox {
|
||||
/// Create new mailbox using email address and addressee name
|
||||
/// Creates a new `Mailbox` using an email address and the name of the recipient if there is one.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{message::Mailbox, Address};
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let mailbox = Mailbox::new(None, address);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(name: Option<String>, email: Address) -> Self {
|
||||
Mailbox { name, email }
|
||||
}
|
||||
|
||||
/// Encode addressee name using function
|
||||
pub(crate) fn recode_name<F>(&self, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(&str) -> String,
|
||||
{
|
||||
Mailbox::new(self.name.clone().map(|s| f(&s)), self.email.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mailbox {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
if let Some(ref name) = self.name {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
@@ -99,7 +126,7 @@ impl FromStr for Mailbox {
|
||||
}
|
||||
}
|
||||
|
||||
/// List or email mailboxes
|
||||
/// 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, ..._).
|
||||
///
|
||||
@@ -108,29 +135,119 @@ impl FromStr for Mailbox {
|
||||
pub struct Mailboxes(Vec<Mailbox>);
|
||||
|
||||
impl Mailboxes {
|
||||
/// Create mailboxes list
|
||||
/// Creates a new list of [`Mailbox`] instances.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::message::Mailboxes;
|
||||
/// let mailboxes = Mailboxes::new();
|
||||
/// ```
|
||||
pub fn new() -> Self {
|
||||
Mailboxes(Vec::new())
|
||||
}
|
||||
|
||||
/// Add mailbox to a list
|
||||
/// Adds a new [`Mailbox`] to the list, in a builder style pattern.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{
|
||||
/// message::{Mailbox, Mailboxes},
|
||||
/// Address,
|
||||
/// };
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let mut mailboxes = Mailboxes::new().with(Mailbox::new(None, address));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn with(mut self, mbox: Mailbox) -> Self {
|
||||
self.0.push(mbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add mailbox to a list
|
||||
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{
|
||||
/// message::{Mailbox, Mailboxes},
|
||||
/// Address,
|
||||
/// };
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let mut mailboxes = Mailboxes::new();
|
||||
/// mailboxes.push(Mailbox::new(None, address));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn push(&mut self, mbox: Mailbox) {
|
||||
self.0.push(mbox);
|
||||
}
|
||||
|
||||
/// Extract first mailbox
|
||||
/// Extracts the first [`Mailbox`] if it exists.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{
|
||||
/// message::{Mailbox, Mailboxes},
|
||||
/// Address,
|
||||
/// };
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let empty = Mailboxes::new();
|
||||
/// assert!(empty.into_single().is_none());
|
||||
///
|
||||
/// let mut mailboxes = Mailboxes::new();
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
///
|
||||
/// mailboxes.push(Mailbox::new(None, address));
|
||||
/// assert!(mailboxes.into_single().is_some());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn into_single(self) -> Option<Mailbox> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Iterate over mailboxes
|
||||
pub fn iter(&self) -> Iter<Mailbox> {
|
||||
/// Creates an iterator over the [`Mailbox`] instances that are currently stored.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{
|
||||
/// message::{Mailbox, Mailboxes},
|
||||
/// Address,
|
||||
/// };
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let mut mailboxes = Mailboxes::new();
|
||||
///
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// mailboxes.push(Mailbox::new(None, address));
|
||||
///
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// mailboxes.push(Mailbox::new(None, address));
|
||||
///
|
||||
/// let mut iter = mailboxes.iter();
|
||||
///
|
||||
/// assert!(iter.next().is_some());
|
||||
/// assert!(iter.next().is_some());
|
||||
///
|
||||
/// assert!(iter.next().is_none());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn iter(&self) -> Iter<'_, Mailbox> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
@@ -142,26 +259,26 @@ impl Default for Mailboxes {
|
||||
}
|
||||
|
||||
impl From<Mailbox> for Mailboxes {
|
||||
fn from(single: Mailbox) -> Self {
|
||||
Mailboxes(vec![single])
|
||||
fn from(mailbox: Mailbox) -> Self {
|
||||
Mailboxes(vec![mailbox])
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Option<Mailbox>> for Mailboxes {
|
||||
fn into(self) -> Option<Mailbox> {
|
||||
self.into_iter().next()
|
||||
impl From<Mailboxes> for Option<Mailbox> {
|
||||
fn from(mailboxes: Mailboxes) -> Option<Mailbox> {
|
||||
mailboxes.into_iter().next()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Mailbox>> for Mailboxes {
|
||||
fn from(list: Vec<Mailbox>) -> Self {
|
||||
Mailboxes(list)
|
||||
fn from(vec: Vec<Mailbox>) -> Self {
|
||||
Mailboxes(vec)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Vec<Mailbox>> for Mailboxes {
|
||||
fn into(self) -> Vec<Mailbox> {
|
||||
self.0
|
||||
impl From<Mailboxes> for Vec<Mailbox> {
|
||||
fn from(mailboxes: Mailboxes) -> Vec<Mailbox> {
|
||||
mailboxes.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +300,7 @@ impl Extend<Mailbox> 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();
|
||||
|
||||
if let Some(mbox) = iter.next() {
|
||||
@@ -204,19 +321,7 @@ impl FromStr for Mailboxes {
|
||||
|
||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||
src.split(',')
|
||||
.map(|m| {
|
||||
m.trim().parse().and_then(|Mailbox { name, email }| {
|
||||
if let Some(name) = name {
|
||||
if let Some(name) = utf8_b::decode(&name) {
|
||||
Ok(Mailbox::new(Some(name), email))
|
||||
} else {
|
||||
Err(AddressError::InvalidUtf8b)
|
||||
}
|
||||
} else {
|
||||
Ok(Mailbox::new(None, email))
|
||||
}
|
||||
})
|
||||
})
|
||||
.map(|m| m.trim().parse())
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(Mailboxes)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
use std::io::Write;
|
||||
|
||||
use crate::message::{
|
||||
encoder::codec,
|
||||
header::{ContentTransferEncoding, ContentType, Header, Headers},
|
||||
EmailFormat,
|
||||
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
|
||||
EmailFormat, IntoBody,
|
||||
};
|
||||
use mime::Mime;
|
||||
use rand::Rng;
|
||||
use std::iter::repeat_with;
|
||||
|
||||
/// MIME part variants
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Part {
|
||||
pub(super) enum Part {
|
||||
/// Single part with content
|
||||
///
|
||||
Single(SinglePart),
|
||||
|
||||
/// Multiple parts of content
|
||||
///
|
||||
Multi(MultiPart),
|
||||
}
|
||||
|
||||
@@ -28,21 +26,7 @@ impl EmailFormat for Part {
|
||||
}
|
||||
}
|
||||
|
||||
impl Part {
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Parts of multipart body
|
||||
///
|
||||
pub type Parts = Vec<Part>;
|
||||
|
||||
/// Creates builder for single part
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinglePartBuilder {
|
||||
headers: Headers,
|
||||
@@ -69,10 +53,15 @@ impl SinglePartBuilder {
|
||||
}
|
||||
|
||||
/// Build singlepart using body
|
||||
pub fn body<T: Into<Vec<u8>>>(self, body: T) -> SinglePart {
|
||||
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
|
||||
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
|
||||
let body = body.into_body(maybe_encoding);
|
||||
|
||||
self.headers.set(body.encoding());
|
||||
|
||||
SinglePart {
|
||||
headers: self.headers,
|
||||
body: body.into(),
|
||||
body: body.into_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,14 +77,16 @@ impl Default for SinglePartBuilder {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::message::{SinglePart, header};
|
||||
/// use lettre::message::{header, SinglePart};
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let part = SinglePart::builder()
|
||||
/// .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
/// .header(header::ContentTransferEncoding::Binary)
|
||||
/// .body("Текст письма в уникоде");
|
||||
/// .header(header::ContentType::TEXT_PLAIN)
|
||||
/// .body(String::from("Текст письма в уникоде"));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinglePart {
|
||||
headers: Headers,
|
||||
@@ -103,57 +94,39 @@ pub struct SinglePart {
|
||||
}
|
||||
|
||||
impl SinglePart {
|
||||
/// Creates a default builder for singlepart
|
||||
/// Creates a builder for singlepart
|
||||
#[inline]
|
||||
pub fn builder() -> SinglePartBuilder {
|
||||
SinglePartBuilder::new()
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with 7bit encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::SevenBit)`.
|
||||
pub fn seven_bit() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::SevenBit)
|
||||
/// Directly create a `SinglePart` from an plain UTF-8 content
|
||||
pub fn plain<T: IntoBody>(body: T) -> Self {
|
||||
Self::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(body)
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with quoted-printable encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::QuotedPrintable)`.
|
||||
pub fn quoted_printable() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::QuotedPrintable)
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with base64 encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Base64)`.
|
||||
pub fn base64() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::Base64)
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with 8-bit encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::EightBit)`.
|
||||
pub fn eight_bit() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::EightBit)
|
||||
}
|
||||
|
||||
/// Creates a singlepart builder with binary encoding
|
||||
///
|
||||
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Binary)`.
|
||||
pub fn binary() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::Binary)
|
||||
/// Directly create a `SinglePart` from an UTF-8 HTML content
|
||||
pub fn html<T: IntoBody>(body: T) -> Self {
|
||||
Self::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(body)
|
||||
}
|
||||
|
||||
/// Get the headers from singlepart
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Read the body from singlepart
|
||||
pub fn body_ref(&self) -> &[u8] {
|
||||
/// Get the encoded body
|
||||
#[inline]
|
||||
pub fn raw_body(&self) -> &[u8] {
|
||||
&self.body
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
/// Get message content formatted for sending
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
@@ -163,19 +136,15 @@ impl SinglePart {
|
||||
|
||||
impl EmailFormat for SinglePart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
|
||||
let encoding = self.headers.get::<ContentTransferEncoding>();
|
||||
let mut encoder = codec(encoding);
|
||||
|
||||
out.extend_from_slice(&encoder.encode(&self.body));
|
||||
out.extend_from_slice(&self.body);
|
||||
out.extend_from_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of multipart
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MultiPartKind {
|
||||
/// Mixed kind to combine unrelated content parts
|
||||
@@ -201,31 +170,28 @@ pub enum MultiPartKind {
|
||||
}
|
||||
|
||||
/// Create a random MIME boundary.
|
||||
/// (Not cryptographically random)
|
||||
fn make_boundary() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(68)
|
||||
.collect()
|
||||
repeat_with(fastrand::alphanumeric).take(40).collect()
|
||||
}
|
||||
|
||||
impl MultiPartKind {
|
||||
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
|
||||
let boundary = boundary.map_or_else(make_boundary, |s| s.into());
|
||||
pub(crate) fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
|
||||
let boundary = boundary.map_or_else(make_boundary, Into::into);
|
||||
|
||||
use self::MultiPartKind::*;
|
||||
format!(
|
||||
"multipart/{}; boundary=\"{}\"{}",
|
||||
match self {
|
||||
Mixed => "mixed",
|
||||
Alternative => "alternative",
|
||||
Related => "related",
|
||||
Encrypted { .. } => "encrypted",
|
||||
Signed { .. } => "signed",
|
||||
Self::Mixed => "mixed",
|
||||
Self::Alternative => "alternative",
|
||||
Self::Related => "related",
|
||||
Self::Encrypted { .. } => "encrypted",
|
||||
Self::Signed { .. } => "signed",
|
||||
},
|
||||
boundary,
|
||||
match self {
|
||||
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
|
||||
Signed { protocol, micalg } =>
|
||||
Self::Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
|
||||
Self::Signed { protocol, micalg } =>
|
||||
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
|
||||
_ => String::new(),
|
||||
}
|
||||
@@ -235,18 +201,17 @@ impl MultiPartKind {
|
||||
}
|
||||
|
||||
fn from_mime(m: &Mime) -> Option<Self> {
|
||||
use self::MultiPartKind::*;
|
||||
match m.subtype().as_ref() {
|
||||
"mixed" => Some(Mixed),
|
||||
"alternative" => Some(Alternative),
|
||||
"related" => Some(Related),
|
||||
"mixed" => Some(Self::Mixed),
|
||||
"alternative" => Some(Self::Alternative),
|
||||
"related" => Some(Self::Related),
|
||||
"signed" => m.get_param("protocol").and_then(|p| {
|
||||
m.get_param("micalg").map(|micalg| Signed {
|
||||
m.get_param("micalg").map(|micalg| Self::Signed {
|
||||
protocol: p.as_str().to_owned(),
|
||||
micalg: micalg.as_str().to_owned(),
|
||||
})
|
||||
}),
|
||||
"encrypted" => m.get_param("protocol").map(|p| Encrypted {
|
||||
"encrypted" => m.get_param("protocol").map(|p| Self::Encrypted {
|
||||
protocol: p.as_str().to_owned(),
|
||||
}),
|
||||
_ => None,
|
||||
@@ -254,14 +219,7 @@ impl MultiPartKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MultiPartKind> for Mime {
|
||||
fn from(m: MultiPartKind) -> Self {
|
||||
m.to_mime::<String>(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Multipart builder
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPartBuilder {
|
||||
headers: Headers,
|
||||
@@ -283,17 +241,17 @@ impl MultiPartBuilder {
|
||||
|
||||
/// Set `Content-Type` header using [`MultiPartKind`]
|
||||
pub fn kind(self, kind: MultiPartKind) -> Self {
|
||||
self.header(ContentType(kind.into()))
|
||||
self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
|
||||
}
|
||||
|
||||
/// Set custom boundary
|
||||
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
|
||||
pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
|
||||
let kind = {
|
||||
let mime = &self.headers.get::<ContentType>().unwrap().0;
|
||||
MultiPartKind::from_mime(mime).unwrap()
|
||||
let content_type = self.headers.get::<ContentType>().unwrap();
|
||||
MultiPartKind::from_mime(content_type.as_ref()).unwrap()
|
||||
};
|
||||
let mime = kind.to_mime(Some(boundary.as_ref()));
|
||||
self.header(ContentType(mime))
|
||||
let mime = kind.to_mime(Some(boundary));
|
||||
self.header(ContentType::from_mime(mime))
|
||||
}
|
||||
|
||||
/// Creates multipart without parts
|
||||
@@ -304,11 +262,6 @@ impl MultiPartBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates multipart using part
|
||||
pub fn part(self, part: Part) -> MultiPart {
|
||||
self.build().part(part)
|
||||
}
|
||||
|
||||
/// Creates multipart using singlepart
|
||||
pub fn singlepart(self, part: SinglePart) -> MultiPart {
|
||||
self.build().singlepart(part)
|
||||
@@ -327,11 +280,10 @@ impl Default for MultiPartBuilder {
|
||||
}
|
||||
|
||||
/// Multipart variant with parts
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPart {
|
||||
headers: Headers,
|
||||
parts: Parts,
|
||||
parts: Vec<Part>,
|
||||
}
|
||||
|
||||
impl MultiPart {
|
||||
@@ -375,10 +327,11 @@ impl MultiPart {
|
||||
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
|
||||
}
|
||||
|
||||
/// Add part to multipart
|
||||
pub fn part(mut self, part: Part) -> Self {
|
||||
self.parts.push(part);
|
||||
self
|
||||
/// Alias for HTML and plain text versions of an email
|
||||
pub fn alternative_plain_html<T: IntoBody, V: IntoBody>(plain: T, html: V) -> Self {
|
||||
Self::alternative()
|
||||
.singlepart(SinglePart::plain(plain))
|
||||
.singlepart(SinglePart::html(html))
|
||||
}
|
||||
|
||||
/// Add single part to multipart
|
||||
@@ -395,8 +348,13 @@ impl MultiPart {
|
||||
|
||||
/// Get the boundary of multipart contents
|
||||
pub fn boundary(&self) -> String {
|
||||
let content_type = &self.headers.get::<ContentType>().unwrap().0;
|
||||
content_type.get_param("boundary").unwrap().as_str().into()
|
||||
let content_type = self.headers.get::<ContentType>().unwrap();
|
||||
content_type
|
||||
.as_ref()
|
||||
.get_param("boundary")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Get the headers from the multipart
|
||||
@@ -409,16 +367,6 @@ impl MultiPart {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Get the parts from the multipart
|
||||
pub fn parts(&self) -> &Parts {
|
||||
&self.parts
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the parts
|
||||
pub fn parts_mut(&mut self) -> &mut Parts {
|
||||
&mut self.parts
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
@@ -429,7 +377,8 @@ impl MultiPart {
|
||||
|
||||
impl EmailFormat for MultiPart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
|
||||
let boundary = self.boundary();
|
||||
@@ -455,16 +404,14 @@ mod test {
|
||||
#[test]
|
||||
fn single_part_binary() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n"
|
||||
@@ -475,16 +422,14 @@ mod test {
|
||||
#[test]
|
||||
fn single_part_quoted_printable() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::QuotedPrintable)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||
"\r\n",
|
||||
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
|
||||
@@ -496,16 +441,14 @@ mod test {
|
||||
#[test]
|
||||
fn single_part_base64() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
|
||||
@@ -516,75 +459,60 @@ mod test {
|
||||
#[test]
|
||||
fn multi_part_mixed() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")),
|
||||
))
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"example.c".as_bytes().into(),
|
||||
)],
|
||||
})
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentDisposition::attachment("example.c"))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")),
|
||||
);
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/mixed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/mixed; \r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_encrypted() {
|
||||
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"application/pgp-encrypted".parse().unwrap(),
|
||||
))
|
||||
.body(String::from("Version: 1")),
|
||||
))
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType(
|
||||
"application/octet-stream; name=\"encrypted.asc\""
|
||||
.parse()
|
||||
.header(header::ContentType::parse("application/pgp-encrypted").unwrap())
|
||||
.body(String::from("Version: 1")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(
|
||||
ContentType::parse("application/octet-stream; name=\"encrypted.asc\"")
|
||||
.unwrap(),
|
||||
)
|
||||
.header(header::ContentDisposition::inline_with_name(
|
||||
"encrypted.asc",
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Inline,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"encrypted.asc".as_bytes().into(),
|
||||
)],
|
||||
})
|
||||
.body(String::from(concat!(
|
||||
"-----BEGIN PGP MESSAGE-----\r\n",
|
||||
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
|
||||
@@ -593,25 +521,31 @@ mod test {
|
||||
))),
|
||||
);
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/encrypted;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
|
||||
" protocol=\"application/pgp-encrypted\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: application/pgp-encrypted\r\n",
|
||||
"\r\n",
|
||||
"Version: 1\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
|
||||
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
|
||||
"\r\n",
|
||||
"-----BEGIN PGP MESSAGE-----\r\n",
|
||||
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
|
||||
"...\r\n",
|
||||
"-----END PGP MESSAGE-----\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/encrypted; \r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n",
|
||||
" protocol=\"application/pgp-encrypted\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/pgp-encrypted\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Version: 1\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
|
||||
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"-----BEGIN PGP MESSAGE-----\r\n",
|
||||
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
|
||||
"...\r\n",
|
||||
"-----END PGP MESSAGE-----\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_signed() {
|
||||
@@ -619,27 +553,19 @@ mod test {
|
||||
"application/pgp-signature".to_owned(),
|
||||
"pgp-sha256".to_owned(),
|
||||
)
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType("text/plain".parse().unwrap()))
|
||||
.body(String::from("Test email for signature")),
|
||||
))
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType(
|
||||
"application/pgp-signature; name=\"signature.asc\""
|
||||
.parse()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Test email for signature")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(
|
||||
ContentType::parse("application/pgp-signature; name=\"signature.asc\"")
|
||||
.unwrap(),
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"signature.asc".as_bytes().into(),
|
||||
)],
|
||||
})
|
||||
)
|
||||
.header(header::ContentDisposition::attachment("signature.asc"))
|
||||
.body(String::from(concat!(
|
||||
"-----BEGIN PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
@@ -651,99 +577,102 @@ mod test {
|
||||
))),
|
||||
);
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/signed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
|
||||
" protocol=\"application/pgp-signature\";",
|
||||
" micalg=\"pgp-sha256\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"\r\n",
|
||||
"Test email for signature\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
|
||||
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
|
||||
"\r\n",
|
||||
"-----BEGIN PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
|
||||
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
|
||||
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
|
||||
"=3FYZ\r\n",
|
||||
"-----END PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/signed; \r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n",
|
||||
" protocol=\"application/pgp-signature\";",
|
||||
" micalg=\"pgp-sha256\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Test email for signature\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
|
||||
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"-----BEGIN PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
|
||||
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
|
||||
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
|
||||
"=3FYZ\r\n",
|
||||
"-----END PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_alternative() {
|
||||
let part = MultiPart::alternative()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(SinglePart::builder()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"))))
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/alternative;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
concat!("Content-Type: multipart/alternative; \r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/html; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/html; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed_related() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.multipart(MultiPart::related()
|
||||
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("image/png".parse().unwrap()))
|
||||
.header(header::ContentLocation("/image.png".into()))
|
||||
.header(header::ContentType::parse("image/png").unwrap())
|
||||
.header(header::ContentLocation::from(String::from("/image.png")))
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".as_bytes().into())]
|
||||
})
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentDisposition::attachment("example.c"))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")));
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/mixed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
concat!("Content-Type: multipart/mixed; \r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: multipart/related;",
|
||||
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: multipart/related; \r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
|
||||
"Content-Type: text/html; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/html; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: image/png\r\n",
|
||||
"Content-Location: /image.png\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
@@ -751,14 +680,14 @@ mod test {
|
||||
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
|
||||
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
|
||||
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -773,7 +702,7 @@ mod test {
|
||||
|
||||
// Ensure correct length
|
||||
for boundary in boundaries {
|
||||
assert_eq!(68, boundary.len());
|
||||
assert_eq!(40, boundary.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,87 @@
|
||||
//! Provides a strongly typed way to build emails
|
||||
//!
|
||||
//! ### Creating messages
|
||||
//!
|
||||
//! This section explains how to create emails.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ### Format email messages
|
||||
//! This section demonstrates how to build messages.
|
||||
//!
|
||||
//! #### With string body
|
||||
//! <!--
|
||||
//! style for <details><summary>Blablabla</summary> Lots of stuff</details>
|
||||
//! borrowed from https://docs.rs/time/0.2.23/src/time/lib.rs.html#49-54
|
||||
//! -->
|
||||
//! <style>
|
||||
//! summary, details:not([open]) { cursor: pointer; }
|
||||
//! summary { display: list-item; }
|
||||
//! summary::marker { content: '▶ '; }
|
||||
//! details[open] summary::marker { content: '▼ '; }
|
||||
//! </style>
|
||||
//!
|
||||
//! The easiest way how we can create email message with simple string.
|
||||
//!
|
||||
//! ### Plain body
|
||||
//!
|
||||
//! The easiest way of creating a message, which uses a plain text body.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::Message;
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let m = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Will produce:
|
||||
//! Which produces:
|
||||
//! <details>
|
||||
//! <summary>Click to expand</summary>
|
||||
//!
|
||||
//! ```sh
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! Be happy!
|
||||
//! ```
|
||||
//! </details>
|
||||
//! <br />
|
||||
//!
|
||||
//! The unicode header data will be encoded using _UTF8-Base64_ encoding.
|
||||
//! The unicode header data is encoded using _UTF8-Base64_ encoding, when necessary.
|
||||
//!
|
||||
//! ### With MIME body
|
||||
//! The `Content-Transfer-Encoding` is chosen based on the best encoding
|
||||
//! available for the given body, between `7bit`, `quoted-printable` and `base64`.
|
||||
//!
|
||||
//! ##### Single part
|
||||
//! ### Plain and HTML body
|
||||
//!
|
||||
//! The more complex way is using MIME contents.
|
||||
//! Uses a MIME body to include both plain text and HTML versions of the body.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::{header, Message, SinglePart, Part};
|
||||
//! # use std::error::Error;
|
||||
//! use lettre::message::{header, Message, MultiPart, SinglePart};
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let m = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType(
|
||||
//! "text/plain; charset=utf8".parse().unwrap(),
|
||||
//! )).header(header::ContentTransferEncoding::QuotedPrintable)
|
||||
//! .body("Привет, мир!"),
|
||||
//! )
|
||||
//! .unwrap();
|
||||
//! .multipart(MultiPart::alternative_plain_html(
|
||||
//! String::from("Hello, world! :)"),
|
||||
//! String::from("<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>"),
|
||||
//! ))?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! The body will be encoded using selected `Content-Transfer-Encoding`.
|
||||
//! Which produces:
|
||||
//! <details>
|
||||
//! <summary>Click to expand</summary>
|
||||
//!
|
||||
//! ```sh
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
@@ -69,133 +89,142 @@
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! MIME-Version: 1.0
|
||||
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
||||
//! Content-Type: multipart/alternative; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
|
||||
//!
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
//! Content-Type: text/plain; charset=utf8
|
||||
//! Content-Transfer-Encoding: quoted-printable
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
|
||||
//! Hello, world! :)
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
//! Content-Type: text/html; charset=utf8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! <p><b>Hello</b>, <i>world</i>! <img src="cid:123"></p>
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
|
||||
//! ```
|
||||
//! </details>
|
||||
//!
|
||||
//! ##### Multiple parts
|
||||
//! ### Complex MIME body
|
||||
//!
|
||||
//! And more advanced way of building message by using multipart MIME contents.
|
||||
//! This example shows how to include both plain and HTML versions of the body,
|
||||
//! attachments and inlined images.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::{header, Message, MultiPart, SinglePart, Part};
|
||||
//! # use std::error::Error;
|
||||
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
|
||||
//! use std::fs;
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let image = fs::read("docs/lettre.png")?;
|
||||
//! // this image_body can be cloned and reused between emails.
|
||||
//! // since `Body` holds a pre-encoded body, reusing it means avoiding having
|
||||
//! // to re-encode the same body for every email (this clearly only applies
|
||||
//! // when sending multiple emails with the same attachment).
|
||||
//! let image_body = Body::new(image);
|
||||
//!
|
||||
//! let m = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .multipart(
|
||||
//! MultiPart::mixed()
|
||||
//! .multipart(
|
||||
//! MultiPart::alternative()
|
||||
//! .singlepart(
|
||||
//! SinglePart::quoted_printable()
|
||||
//! .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
//! .body("Привет, мир!")
|
||||
//! )
|
||||
//! .multipart(
|
||||
//! MultiPart::related()
|
||||
//! .singlepart(
|
||||
//! SinglePart::eight_bit()
|
||||
//! .header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
//! .body("<p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>")
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::base64()
|
||||
//! .header(header::ContentType("image/png".parse().unwrap()))
|
||||
//! .header(header::ContentDisposition {
|
||||
//! disposition: header::DispositionType::Inline,
|
||||
//! parameters: vec![],
|
||||
//! })
|
||||
//! .body("<smile-raw-image-data>")
|
||||
//! )
|
||||
//! MultiPart::alternative()
|
||||
//! .singlepart(SinglePart::plain(String::from("Hello, world! :)")))
|
||||
//! .multipart(
|
||||
//! MultiPart::related()
|
||||
//! .singlepart(SinglePart::html(String::from(
|
||||
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
//! )))
|
||||
//! .singlepart(
|
||||
//! Attachment::new_inline(String::from("123"))
|
||||
//! .body(image_body, "image/png".parse().unwrap()),
|
||||
//! ),
|
||||
//! ),
|
||||
//! )
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::seven_bit()
|
||||
//! .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
//! .header(header::ContentDisposition {
|
||||
//! disposition: header::DispositionType::Attachment,
|
||||
//! parameters: vec![
|
||||
//! header::DispositionParam::Filename(
|
||||
//! header::Charset::Ext("utf-8".into()),
|
||||
//! None, "example.c".as_bytes().into()
|
||||
//! )
|
||||
//! ]
|
||||
//! })
|
||||
//! .body("int main() { return 0; }")
|
||||
//! )
|
||||
//! ).unwrap();
|
||||
//! .singlepart(Attachment::new(String::from("example.rs")).body(
|
||||
//! String::from("fn main() { println!(\"Hello, World!\") }"),
|
||||
//! "text/plain".parse().unwrap(),
|
||||
//! )),
|
||||
//! )?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Which produces:
|
||||
//! <details>
|
||||
//! <summary>Click to expand</summary>
|
||||
//!
|
||||
//! ```sh
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! MIME-Version: 1.0
|
||||
//! Content-Type: multipart/mixed; boundary="RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m"
|
||||
//! Date: Sat, 12 Dec 2020 16:30:45 GMT
|
||||
//! Content-Type: multipart/mixed; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
|
||||
//!
|
||||
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
|
||||
//! Content-Type: multipart/alternative; boundary="qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy"
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
//! Content-Type: multipart/alternative; boundary="EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk"
|
||||
//!
|
||||
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
|
||||
//! Content-Transfer-Encoding: quoted-printable
|
||||
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
|
||||
//! Content-Type: text/plain; charset=utf8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
|
||||
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
|
||||
//! Content-Type: multipart/related; boundary="BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8"
|
||||
//! Hello, world! :)
|
||||
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
|
||||
//! Content-Type: multipart/related; boundary="eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr"
|
||||
//!
|
||||
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
|
||||
//! Content-Transfer-Encoding: 8bit
|
||||
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
|
||||
//! Content-Type: text/html; charset=utf8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! <p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>
|
||||
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
|
||||
//! Content-Transfer-Encoding: base64
|
||||
//! <p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
|
||||
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
|
||||
//! Content-Type: image/png
|
||||
//! Content-Disposition: inline
|
||||
//! Content-ID: <123>
|
||||
//! Content-Transfer-Encoding: base64
|
||||
//!
|
||||
//! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg==
|
||||
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8--
|
||||
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy--
|
||||
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr--
|
||||
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk--
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
//! Content-Type: text/plain; charset=utf8
|
||||
//! Content-Disposition: attachment; filename="example.c"
|
||||
//!
|
||||
//! int main() { return 0; }
|
||||
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m--
|
||||
//! Content-Disposition: attachment; filename="example.rs"
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! fn main() { println!("Hello, World!") }
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
|
||||
//! ```
|
||||
//! </details>
|
||||
|
||||
pub use encoder::*;
|
||||
use std::{convert::TryFrom, io::Write, iter, time::SystemTime};
|
||||
|
||||
pub use attachment::Attachment;
|
||||
pub use body::{Body, IntoBody, MaybeString};
|
||||
pub use mailbox::*;
|
||||
pub use mimebody::*;
|
||||
|
||||
pub use mime;
|
||||
|
||||
mod encoder;
|
||||
mod attachment;
|
||||
mod body;
|
||||
pub mod header;
|
||||
mod mailbox;
|
||||
mod mimebody;
|
||||
mod utf8_b;
|
||||
|
||||
use crate::{
|
||||
message::header::{EmailDate, Header, Headers, MailboxesHeader},
|
||||
Envelope, Error as EmailError,
|
||||
address::Envelope,
|
||||
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
|
||||
Error as EmailError,
|
||||
};
|
||||
use std::{convert::TryFrom, time::SystemTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
|
||||
|
||||
pub trait EmailFormat {
|
||||
/// Something that can be formatted as an email message
|
||||
trait EmailFormat {
|
||||
// Use a writer?
|
||||
fn format(&self, out: &mut Vec<u8>);
|
||||
}
|
||||
@@ -223,37 +252,40 @@ impl MessageBuilder {
|
||||
}
|
||||
|
||||
/// Add mailbox to header
|
||||
pub fn mailbox<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
|
||||
if self.headers.has::<H>() {
|
||||
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
|
||||
self
|
||||
} else {
|
||||
self.header(header)
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add `Date` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Date(date))`.
|
||||
pub fn date(self, date: EmailDate) -> Self {
|
||||
self.header(header::Date(date))
|
||||
/// Shortcut for `self.header(header::Date::new(st))`.
|
||||
pub fn date(self, st: SystemTime) -> Self {
|
||||
self.header(header::Date::new(st))
|
||||
}
|
||||
|
||||
/// Set `Date` header using current date/time
|
||||
///
|
||||
/// Shortcut for `self.date(SystemTime::now())`.
|
||||
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
|
||||
/// if no date has been provided.
|
||||
pub fn date_now(self) -> Self {
|
||||
self.date(SystemTime::now().into())
|
||||
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 {
|
||||
self.header(header::Subject(subject.into()))
|
||||
let s: String = subject.into();
|
||||
self.header(header::Subject::from(s))
|
||||
}
|
||||
|
||||
/// Set `Mime-Version` header to 1.0
|
||||
/// Set `MIME-Version` header to 1.0
|
||||
///
|
||||
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
|
||||
///
|
||||
@@ -264,25 +296,25 @@ impl MessageBuilder {
|
||||
|
||||
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
|
||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||
///
|
||||
/// Shortcut for `self.header(header::Sender(mbox))`.
|
||||
pub fn sender(self, mbox: Mailbox) -> Self {
|
||||
self.header(header::Sender(mbox))
|
||||
self.header(header::Sender::from(mbox))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `From` header
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
|
||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::From(mbox))`.
|
||||
pub fn from(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::From(mbox.into()))
|
||||
self.mailbox(header::From::from(Mailboxes::from(mbox)))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `ReplyTo` header
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
|
||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
|
||||
pub fn reply_to(self, mbox: Mailbox) -> Self {
|
||||
@@ -313,16 +345,16 @@ impl MessageBuilder {
|
||||
/// Set or add message id to [`In-Reply-To`
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
pub fn in_reply_to(self, id: String) -> Self {
|
||||
self.header(header::InReplyTo(id))
|
||||
self.header(header::InReplyTo::from(id))
|
||||
}
|
||||
|
||||
/// Set or add message id to [`References`
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
pub fn references(self, id: String) -> Self {
|
||||
self.header(header::References(id))
|
||||
self.header(header::References::from(id))
|
||||
}
|
||||
|
||||
/// Set [Message-Id
|
||||
/// Set [Message-ID
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
///
|
||||
/// Should generally be inserted by the mail relay.
|
||||
@@ -331,7 +363,7 @@ impl MessageBuilder {
|
||||
/// `<UUID@HOSTNAME>`.
|
||||
pub fn message_id(self, id: Option<String>) -> Self {
|
||||
match id {
|
||||
Some(i) => self.header(header::MessageId(i)),
|
||||
Some(i) => self.header(header::MessageId::from(i)),
|
||||
None => {
|
||||
#[cfg(feature = "hostname")]
|
||||
let hostname = hostname::get()
|
||||
@@ -341,9 +373,9 @@ impl MessageBuilder {
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
|
||||
|
||||
self.header(header::MessageId(
|
||||
self.header(header::MessageId::from(
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
||||
format!("<{}@{}>", Uuid::new_v4(), hostname),
|
||||
format!("<{}@{}>", make_message_id(), hostname),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -352,7 +384,7 @@ impl MessageBuilder {
|
||||
/// Set [User-Agent
|
||||
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
|
||||
pub fn user_agent(self, id: String) -> Self {
|
||||
self.header(header::UserAgent(id))
|
||||
self.header(header::UserAgent::from(id))
|
||||
}
|
||||
|
||||
/// Force specific envelope (by default it is derived from headers)
|
||||
@@ -364,12 +396,12 @@ impl MessageBuilder {
|
||||
// TODO: High-level methods for attachments and embedded files
|
||||
|
||||
/// Create message from body
|
||||
fn build(self, body: Body) -> Result<Message, EmailError> {
|
||||
fn build(self, body: MessageBody) -> Result<Message, EmailError> {
|
||||
// Check for missing required headers
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6
|
||||
|
||||
// Insert Date if missing
|
||||
let res = if self.headers.get::<header::Date>().is_none() {
|
||||
let mut res = if self.headers.get::<header::Date>().is_none() {
|
||||
self.date_now()
|
||||
} else {
|
||||
self
|
||||
@@ -378,7 +410,7 @@ impl MessageBuilder {
|
||||
// Fail is missing correct originator (Sender or From)
|
||||
match res.headers.get::<header::From>() {
|
||||
Some(header::From(f)) => {
|
||||
let from: Vec<Mailbox> = f.clone().into();
|
||||
let from: Vec<Mailbox> = f.into();
|
||||
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
|
||||
return Err(EmailError::TooManyFrom);
|
||||
}
|
||||
@@ -392,6 +424,10 @@ impl MessageBuilder {
|
||||
Some(e) => e,
|
||||
None => Envelope::try_from(&res.headers)?,
|
||||
};
|
||||
|
||||
// Remove `Bcc` headers now the envelope is set
|
||||
res.headers.remove::<header::Bcc>();
|
||||
|
||||
Ok(Message {
|
||||
headers: res.headers,
|
||||
body,
|
||||
@@ -399,47 +435,43 @@ impl MessageBuilder {
|
||||
})
|
||||
}
|
||||
|
||||
// In theory having a body is optional
|
||||
|
||||
/// Plain ASCII body
|
||||
/// Create [`Message`] using a [`Vec<u8>`], [`String`], or [`Body`] body
|
||||
///
|
||||
/// *WARNING*: Generally not what you want
|
||||
pub fn body<T: Into<String>>(self, body: T) -> Result<Message, EmailError> {
|
||||
// 998 chars by line
|
||||
// CR and LF MUST only occur together as CRLF; they MUST NOT appear
|
||||
// independently in the body.
|
||||
let body = body.into();
|
||||
/// Automatically gets encoded with `7bit`, `quoted-printable` or `base64`
|
||||
/// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
|
||||
/// for `body`.
|
||||
pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
|
||||
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
|
||||
let body = body.into_body(maybe_encoding);
|
||||
|
||||
if !&body.is_ascii() {
|
||||
return Err(EmailError::NonAsciiChars);
|
||||
}
|
||||
|
||||
self.build(Body::Raw(body))
|
||||
self.headers.set(body.encoding());
|
||||
self.build(MessageBody::Raw(body.into_vec()))
|
||||
}
|
||||
|
||||
/// Create message using mime body ([`MultiPart`][self::MultiPart])
|
||||
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
|
||||
self.mime_1_0().build(Body::Mime(Part::Multi(part)))
|
||||
self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
|
||||
}
|
||||
|
||||
/// Create message using mime body ([`SinglePart`][self::SinglePart])
|
||||
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
|
||||
self.mime_1_0().build(Body::Mime(Part::Single(part)))
|
||||
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Email message which can be formatted
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Message {
|
||||
headers: Headers,
|
||||
body: Body,
|
||||
body: MessageBody,
|
||||
envelope: Envelope,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Body {
|
||||
enum MessageBody {
|
||||
Mime(Part),
|
||||
Raw(String),
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
@@ -468,12 +500,14 @@ impl Message {
|
||||
|
||||
impl EmailFormat for Message {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
|
||||
match &self.body {
|
||||
Body::Mime(p) => p.format(out),
|
||||
Body::Raw(r) => {
|
||||
MessageBody::Mime(p) => p.format(out),
|
||||
MessageBody::Raw(r) => {
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend(r.as_bytes())
|
||||
out.extend_from_slice(&r)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,13 +519,23 @@ impl Default for MessageBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a random message id.
|
||||
/// (Not cryptographically random)
|
||||
fn make_message_id() -> String {
|
||||
iter::repeat_with(fastrand::alphanumeric).take(36).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::message::{header, mailbox::Mailbox, Message};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
|
||||
|
||||
#[test]
|
||||
fn email_missing_originator() {
|
||||
assert!(Message::builder().body("Happy new year!").is_err());
|
||||
assert!(Message::builder()
|
||||
.body(String::from("Happy new year!"))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -499,7 +543,7 @@ mod test {
|
||||
assert!(Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.body("Happy new year!")
|
||||
.body(String::from("Happy new year!"))
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
@@ -508,16 +552,18 @@ mod test {
|
||||
assert!(Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.from("AnyBody <anybody@domain.tld>".parse().unwrap())
|
||||
.body("Happy new year!")
|
||||
.body(String::from("Happy new year!"))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_message() {
|
||||
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
|
||||
|
||||
let email = Message::builder()
|
||||
.date(date)
|
||||
.bcc("hidden@example.com".parse().unwrap())
|
||||
.header(header::From(
|
||||
vec![Mailbox::new(
|
||||
Some("Каи".into()),
|
||||
@@ -528,20 +574,80 @@ mod test {
|
||||
.header(header::To(
|
||||
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
|
||||
))
|
||||
.header(header::Subject("яңа ел белән!".into()))
|
||||
.body("Happy new year!")
|
||||
.header(header::Subject::from(String::from("яңа ел белән!")))
|
||||
.body(String::from("Happy new year!"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(email.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
||||
"To: Pony O.P. <pony@domain.tld>\r\n",
|
||||
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Happy new year!"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_with_png() {
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
|
||||
let img = std::fs::read("./docs/lettre.png").unwrap();
|
||||
let m = Message::builder()
|
||||
.date(date)
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.multipart(
|
||||
MultiPart::related()
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(String::from(
|
||||
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
)),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::parse("image/png").unwrap())
|
||||
.header(header::ContentDisposition::inline())
|
||||
.header(header::ContentId::from(String::from("<123>")))
|
||||
.body(img),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = String::from_utf8(m.formatted()).unwrap();
|
||||
let file_expected = std::fs::read("./testdata/email_with_png.eml").unwrap();
|
||||
let expected = String::from_utf8(file_expected).unwrap();
|
||||
|
||||
for (i, line) in output.lines().zip(expected.lines()).enumerate() {
|
||||
if i == 7 || i == 9 || i == 14 || i == 233 {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert_eq!(line.0, line.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_message_id() {
|
||||
let mut ids = std::collections::HashSet::with_capacity(10);
|
||||
for _ in 0..1000 {
|
||||
ids.insert(make_message_id());
|
||||
}
|
||||
|
||||
// Ensure there are no duplicates
|
||||
assert_eq!(1000, ids.len());
|
||||
|
||||
// Ensure correct length
|
||||
for id in ids {
|
||||
assert_eq!(36, id.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// https://tools.ietf.org/html/rfc1522
|
||||
|
||||
fn allowed_char(c: char) -> bool {
|
||||
c >= 1 as char && c <= 9 as char
|
||||
|| c == 11 as char
|
||||
|| c == 12 as char
|
||||
|| c >= 14 as char && c <= 127 as char
|
||||
}
|
||||
|
||||
pub fn encode(s: &str) -> String {
|
||||
if s.chars().all(allowed_char) {
|
||||
s.into()
|
||||
} else {
|
||||
format!("=?utf-8?b?{}?=", base64::encode(s))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(s: &str) -> Option<String> {
|
||||
const PREFIX: &str = "=?utf-8?b?";
|
||||
const SUFFIX: &str = "?=";
|
||||
|
||||
let s = s.trim();
|
||||
if s.starts_with(PREFIX) && s.ends_with(SUFFIX) {
|
||||
let s = &s[PREFIX.len()..];
|
||||
let s = &s[..s.len() - SUFFIX.len()];
|
||||
base64::decode(s)
|
||||
.ok()
|
||||
.and_then(|v| String::from_utf8(v).ok())
|
||||
} else {
|
||||
Some(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{decode, encode};
|
||||
|
||||
#[test]
|
||||
fn encode_ascii() {
|
||||
assert_eq!(&encode("Kayo. ?"), "Kayo. ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ascii() {
|
||||
assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_utf8() {
|
||||
assert_eq!(
|
||||
&encode("Привет, мир!"),
|
||||
"=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8() {
|
||||
assert_eq!(
|
||||
decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="),
|
||||
Some("Привет, мир!".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,96 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
};
|
||||
use crate::BoxError;
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// JSON serialization error
|
||||
JsonSerialization(serde_json::Error),
|
||||
/// The Errors that may occur when sending an email over SMTP
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
JsonSerialization(ref err) => err.fmt(fmt),
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the error is a file I/O error
|
||||
pub fn is_io(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Io)
|
||||
}
|
||||
|
||||
/// Returns true if the error is an envelope serialization or deserialization error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub fn is_envelope(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Envelope)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// File I/O error
|
||||
Io,
|
||||
/// Envelope serialization/deserialization error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Envelope,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::file::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(ref source) = self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.inner.kind {
|
||||
Kind::Io => f.write_str("response error")?,
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Kind::Envelope => f.write_str("internal client error")?,
|
||||
};
|
||||
|
||||
if let Some(ref e) = self.inner.source {
|
||||
write!(f, ": {}", e)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
JsonSerialization(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
pub(crate) fn io<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Io, Some(e))
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Error {
|
||||
Error::JsonSerialization(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Error::Client(string)
|
||||
}
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub(crate) fn envelope<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Envelope, Some(e))
|
||||
}
|
||||
|
||||
@@ -1,98 +1,145 @@
|
||||
//! The file transport writes the emails to the given directory. The name of the file will be
|
||||
//! `message_id.json`.
|
||||
//! `message_id.eml`.
|
||||
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{Transport, Envelope, Message, FileTransport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "file-transport", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 0.2
|
||||
//! ## Sync example with envelope
|
||||
//!
|
||||
//! It is possible to also write the envelope content in a separate JSON file
|
||||
//! by using the `with_envelope` builder. The JSON file will be written in the
|
||||
//! target directory with same name and a `json` extension.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "tokio02")]
|
||||
//! # async fn run() {
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{FileTransport, Message, Transport};
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{Tokio02Transport, Envelope, Message, FileTransport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
//! let sender = FileTransport::with_envelope(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "file-transport-envelope", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 1.x
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{AsyncTransport, Tokio1Executor, Message, AsyncFileTransport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async async-std 1.x
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "async-std1")]
|
||||
//! # async fn run() {
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{AsyncStd1Transport, Envelope, Message, FileTransport};
|
||||
//! use lettre::{AsyncTransport, AsyncStd1Executor, Message, AsyncFileTransport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! Example result
|
||||
//! Example email content result
|
||||
//!
|
||||
//! ```eml
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! Date: Tue, 18 Aug 2020 22:50:17 GMT
|
||||
//!
|
||||
//! Be happy!
|
||||
//! ```
|
||||
//!
|
||||
//! Example envelope result
|
||||
//!
|
||||
//! ```json
|
||||
//! {
|
||||
//! "envelope": {
|
||||
//! "forward_path": [
|
||||
//! "hei@domain.tld"
|
||||
//! ],
|
||||
//! "reverse_path": "nobody@domain.tld"
|
||||
//! },
|
||||
//! "raw_message": null,
|
||||
//! "message": "From: NoBody <nobody@domain.tld>\r\nReply-To: Yuin <yuin@domain.tld>\r\nTo: Hei <hei@domain.tld>\r\nSubject: Happy new year\r\nDate: Tue, 18 Aug 2020 22:50:17 GMT\r\n\r\nBe happy!"
|
||||
//! }
|
||||
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
|
||||
//! ```
|
||||
|
||||
pub use self::error::Error;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Transport;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Transport;
|
||||
use crate::{Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
|
||||
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"))]
|
||||
use std::marker::PhantomData;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
@@ -106,51 +153,110 @@ type Id = String;
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: bool,
|
||||
}
|
||||
|
||||
/// Asynchronously writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
pub struct AsyncFileTransport<E: Executor> {
|
||||
inner: FileTransport,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format and the envelope
|
||||
/// in json format.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub fn with_envelope<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a message that was written using the file transport.
|
||||
///
|
||||
/// Reads the envelope and the raw message content.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||
use std::fs;
|
||||
|
||||
let eml_file = self.path.join(format!("{}.eml", email_id));
|
||||
let eml = fs::read(eml_file).map_err(error::io)?;
|
||||
|
||||
let json_file = self.path.join(format!("{}.json", email_id));
|
||||
let json = fs::read(&json_file).map_err(error::io)?;
|
||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||
|
||||
Ok((envelope, eml))
|
||||
}
|
||||
|
||||
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
|
||||
self.path.join(format!("{}.{}", email_id, extension))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
struct SerializableEmail<'a> {
|
||||
envelope: Envelope,
|
||||
raw_message: Option<&'a [u8]>,
|
||||
message: Option<&'a str>,
|
||||
}
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> AsyncFileTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
inner: FileTransport::new(path),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
fn send_raw_impl(
|
||||
&self,
|
||||
envelope: &Envelope,
|
||||
email: &[u8],
|
||||
) -> Result<(Uuid, PathBuf, String), serde_json::Error> {
|
||||
let email_id = Uuid::new_v4();
|
||||
let file = self.path.join(format!("{}.json", email_id));
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format and the envelope
|
||||
/// in json format.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub fn with_envelope<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
inner: FileTransport::with_envelope(path),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
let serialized = match str::from_utf8(email) {
|
||||
// Serialize as UTF-8 string if possible
|
||||
Ok(m) => serde_json::to_string(&SerializableEmail {
|
||||
envelope: envelope.clone(),
|
||||
message: Some(m),
|
||||
raw_message: None,
|
||||
}),
|
||||
Err(_) => serde_json::to_string(&SerializableEmail {
|
||||
envelope: envelope.clone(),
|
||||
message: None,
|
||||
raw_message: Some(email),
|
||||
}),
|
||||
}?;
|
||||
/// Read a message that was written using the file transport.
|
||||
///
|
||||
/// Reads the envelope and the raw message content.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||
let eml_file = self.inner.path.join(format!("{}.eml", email_id));
|
||||
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
|
||||
|
||||
Ok((email_id, file, serialized))
|
||||
let json_file = self.inner.path.join(format!("{}.json", email_id));
|
||||
let json = E::fs_read(&json_file).await.map_err(error::io)?;
|
||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||
|
||||
Ok((envelope, eml))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,41 +267,52 @@ impl Transport for FileTransport {
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use std::fs;
|
||||
|
||||
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let file = self.path(&email_id, "eml");
|
||||
fs::write(file, email).map_err(error::io)?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.save_envelope {
|
||||
let file = self.path(&email_id, "json");
|
||||
let buf = serde_json::to_string(&envelope).map_err(error::envelope)?;
|
||||
fs::write(file, buf).map_err(error::io)?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
let _ = envelope;
|
||||
|
||||
fs::write(file, serialized)?;
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for FileTransport {
|
||||
impl<E> AsyncTransport for AsyncFileTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use async_std::fs;
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
|
||||
let file = self.inner.path(&email_id, "eml");
|
||||
E::fs_write(&file, email).await.map_err(error::io)?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.inner.save_envelope {
|
||||
let file = self.inner.path(&email_id, "json");
|
||||
let buf = serde_json::to_vec(&envelope).map_err(error::envelope)?;
|
||||
E::fs_write(&file, &buf).await.map_err(error::io)?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
let _ = envelope;
|
||||
|
||||
fs::write(file, serialized).await?;
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for FileTransport {
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio02_crate::fs;
|
||||
|
||||
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
|
||||
|
||||
fs::write(file, serialized).await?;
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,159 @@
|
||||
//! ### Sending Messages
|
||||
//! ## Transports for sending emails
|
||||
//!
|
||||
//! This section explains how to manipulate emails you have created.
|
||||
//! This module contains `Transport`s for sending emails. A `Transport` implements a high-level API
|
||||
//! for sending emails. It automatically manages the underlying resources and doesn't require any
|
||||
//! specific knowledge of email protocols in order to be used.
|
||||
//!
|
||||
//! This mailer contains several different transports for your emails. To be sendable, the
|
||||
//! emails have to implement `Email`, which is the case for emails created with `lettre::builder`.
|
||||
//! ### Getting started
|
||||
//!
|
||||
//! Sending emails from your programs requires using an email relay, as client libraries are not
|
||||
//! designed to handle email delivery by themselves. Depending on your infrastructure, your relay
|
||||
//! could be:
|
||||
//!
|
||||
//! * a service from your Cloud or hosting provider
|
||||
//! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either
|
||||
//! locally on your servers or accessible over the network
|
||||
//! * a dedicated external service, like Mailchimp, Mailgun, etc.
|
||||
//!
|
||||
//! In most cases, the best option is to:
|
||||
//!
|
||||
//! * Use the [`SMTP`] transport, with the [`relay`] builder (or one of its async counterparts)
|
||||
//! with your server's hostname. They provide modern and secure defaults.
|
||||
//! * Use the [`credentials`] method of the builder to pass your credentials.
|
||||
//!
|
||||
//! These should be enough to safely cover most use cases.
|
||||
//!
|
||||
//! ### Available transports
|
||||
//!
|
||||
//! The following transports are available:
|
||||
//!
|
||||
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
|
||||
//! the preferred way of sending emails.
|
||||
//! * The `SendmailTransport` uses the sendmail command to send messages. It is an alternative to
|
||||
//! the SMTP transport.
|
||||
//! * The `FileTransport` creates a file containing the email content to be sent. It can be used
|
||||
//! for debugging or if you want to keep all sent emails.
|
||||
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the
|
||||
//! logs.
|
||||
//! | Module | Protocol | Sync API | Async API | Description |
|
||||
//! | ------------ | -------- | --------------------- | -------------------------- | ------------------------------------------------------- |
|
||||
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
|
||||
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
|
||||
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
|
||||
//! | [`stub`] | Debug | [`StubTransport`] | [`StubTransport`] | Drops the email - Useful for debugging |
|
||||
//!
|
||||
//! ## Building an email
|
||||
//!
|
||||
//! Emails can either be built though [`Message`], which is a typed API for constructing emails
|
||||
//! (find out more about it by going over the [`message`][crate::message] module),
|
||||
//! or via external means.
|
||||
//!
|
||||
//! [`Message`]s can be sent via [`Transport::send`] or [`AsyncTransport::send`], while messages
|
||||
//! built without lettre's [`message`][crate::message] APIs can be sent via [`Transport::send_raw`]
|
||||
//! or [`AsyncTransport::send_raw`].
|
||||
//!
|
||||
//! ## Brief example
|
||||
//!
|
||||
//! This example shows how to build an email and send it via an SMTP relay server.
|
||||
//! It is in no way a complete example, but it shows how to get started with lettre.
|
||||
//! More examples can be found by looking at the specific modules, linked in the _Module_ column
|
||||
//! of the [table above](#transports-for-sending-emails).
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::transport::smtp::authentication::Credentials;
|
||||
//! use lettre::{Message, SmtpTransport, Transport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
//!
|
||||
//! // Open a remote connection to the SMTP relay server
|
||||
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
|
||||
//! .credentials(creds)
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email
|
||||
//! match mailer.send(&email) {
|
||||
//! Ok(_) => println!("Email sent successfully!"),
|
||||
//! Err(e) => panic!("Could not send email: {:?}", e),
|
||||
//! }
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # #[cfg(not(all(feature = "builder", feature = "smtp-transport")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent
|
||||
//! [`SMTP`]: crate::transport::smtp
|
||||
//! [`relay`]: crate::SmtpTransport::relay
|
||||
//! [`starttls_relay`]: crate::SmtpTransport::starttls_relay
|
||||
//! [`credentials`]: crate::transport::smtp::SmtpTransportBuilder::credentials
|
||||
//! [`Message`]: crate::Message
|
||||
//! [`file`]: self::file
|
||||
//! [`SmtpTransport`]: crate::SmtpTransport
|
||||
//! [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
//! [`SendmailTransport`]: crate::SendmailTransport
|
||||
//! [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
//! [`FileTransport`]: crate::FileTransport
|
||||
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::Envelope;
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::Message;
|
||||
|
||||
#[cfg(feature = "file-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
|
||||
/// Blocking Transport method for emails
|
||||
pub trait Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
self.send_raw(message.envelope(), &raw)
|
||||
}
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
/// Async Transport method for emails
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[async_trait]
|
||||
pub trait AsyncTransport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
// TODO take &Message
|
||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
let envelope = message.envelope();
|
||||
self.send_raw(&envelope, &raw).await
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
@@ -1,52 +1,92 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
use crate::BoxError;
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(String),
|
||||
/// Error parsing UTF8 in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// The Errors that may occur when sending an email over sendmail
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Client(ref err) => err.fmt(fmt),
|
||||
Utf8Parsing(ref err) => err.fmt(fmt),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the error is from client
|
||||
pub fn is_client(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Client)
|
||||
}
|
||||
|
||||
/// Returns true if the error comes from the response
|
||||
pub fn is_response(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Response)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// Error parsing a response
|
||||
Response,
|
||||
/// Internal client error
|
||||
Client,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::sendmail::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(ref source) = self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.inner.kind {
|
||||
Kind::Response => f.write_str("response error")?,
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
};
|
||||
|
||||
if let Some(ref e) = self.inner.source {
|
||||
write!(f, ": {}", e)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Response, Some(e))
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Client, Some(e))
|
||||
}
|
||||
|
||||
@@ -1,96 +1,122 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//! The sendmail transport sends the email using the local `sendmail` command.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::{Message, Envelope, Transport, SendmailTransport};
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, SendmailTransport, Transport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "sendmail-transport", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 0.2 example
|
||||
//! ## Async tokio 1.x example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "tokio02")]
|
||||
//! # async fn run() {
|
||||
//! use lettre::{Message, Envelope, Tokio02Transport, SendmailTransport};
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, Tokio1Executor, AsyncSendmailTransport, SendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async async-std 1.x example
|
||||
//!
|
||||
//!```rust
|
||||
//! # #[cfg(feature = "async-std1")]
|
||||
//! # async fn run() {
|
||||
//! use lettre::{Message, Envelope, AsyncStd1Transport, SendmailTransport};
|
||||
//!```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub use self::error::Error;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Transport;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Transport;
|
||||
use crate::{Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
#[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::{
|
||||
convert::AsRef,
|
||||
ffi::OsString,
|
||||
io::prelude::*,
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
mod error;
|
||||
|
||||
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
|
||||
const DEFAULT_SENDMAIL: &str = "/usr/sbin/sendmail";
|
||||
|
||||
/// Sends an email using the `sendmail` command
|
||||
#[derive(Debug, Default)]
|
||||
/// Sends emails using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
|
||||
pub struct SendmailTransport {
|
||||
command: OsString,
|
||||
}
|
||||
|
||||
/// Asynchronously sends emails using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSendmailTransport<E: Executor> {
|
||||
inner: SendmailTransport,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: DEFAUT_SENDMAIL.into(),
|
||||
command: DEFAULT_SENDMAIL.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,99 +129,179 @@ impl SendmailTransport {
|
||||
|
||||
fn command(&self, envelope: &Envelope) -> Command {
|
||||
let mut c = Command::new(&self.command);
|
||||
c.arg("-i")
|
||||
.arg("-f")
|
||||
.arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\""))
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped());
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> AsyncSendmailTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: SendmailTransport::new(),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given sendmail command
|
||||
pub fn new_with_command<S: Into<OsString>>(command: S) -> Self {
|
||||
Self {
|
||||
inner: SendmailTransport::new_with_command(command),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
|
||||
use tokio1_crate::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.inner.command);
|
||||
c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
|
||||
use tokio02_crate::process::Command;
|
||||
#[cfg(feature = "async-std1")]
|
||||
fn async_std_command(&self, envelope: &Envelope) -> async_std::process::Command {
|
||||
use async_std::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.command);
|
||||
c.kill_on_drop(true);
|
||||
c.arg("-i")
|
||||
.arg("-f")
|
||||
.arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\""))
|
||||
let mut c = Command::new(&self.inner.command);
|
||||
// TODO: figure out why enabling this kills it earlier
|
||||
// c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped());
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SendmailTransport {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> Default for AsyncSendmailTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for SendmailTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
// Spawn the sendmail command
|
||||
let mut process = self.command(envelope).spawn()?;
|
||||
let mut process = self.command(envelope).spawn().map_err(error::client)?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(email)?;
|
||||
let output = process.wait_with_output()?;
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(email)
|
||||
.map_err(error::client)?;
|
||||
let output = process.wait_with_output().map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error::Error::Client(String::from_utf8(output.stderr)?))
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for SendmailTransport {
|
||||
impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut command = self.command(envelope);
|
||||
let email = email.to_vec();
|
||||
use async_std::io::prelude::WriteExt;
|
||||
|
||||
// TODO: Convert to real async, once async-std has a process implementation.
|
||||
let output = async_std::task::spawn_blocking(move || {
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email)?;
|
||||
process.wait_with_output()
|
||||
})
|
||||
.await?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for SendmailTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio02_crate::io::AsyncWriteExt;
|
||||
|
||||
let mut command = self.tokio02_command(envelope);
|
||||
let mut command = self.async_std_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
let mut process = command.spawn().map_err(error::client)?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email).await?;
|
||||
let output = process.wait_with_output().await?;
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&email)
|
||||
.await
|
||||
.map_err(error::client)?;
|
||||
let output = process.output().await.map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio1_crate::io::AsyncWriteExt;
|
||||
|
||||
let mut command = self.tokio1_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn().map_err(error::client)?;
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&email)
|
||||
.await
|
||||
.map_err(error::client)?;
|
||||
let output = process.wait_with_output().await.map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use super::Tls;
|
||||
use super::{
|
||||
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
|
||||
};
|
||||
use crate::{Envelope, Tokio02Transport};
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{Envelope, Executor};
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncSmtpTransport<C> {
|
||||
/// Asynchronously sends emails using the SMTP protocol
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransport<E> {
|
||||
// TODO: pool
|
||||
inner: AsyncSmtpClient<C>,
|
||||
inner: AsyncSmtpClient<E>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
|
||||
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
@@ -31,21 +41,52 @@ impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsyncSmtpTransport<C>
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> AsyncSmtpTransport<E>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
E: Executor,
|
||||
{
|
||||
/// Simple and secure transport, using TLS connections to comunicate with the SMTP server
|
||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||
///
|
||||
/// The right option for most SMTP servers.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
)))
|
||||
)]
|
||||
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{TlsParameters, SUBMISSIONS_PORT};
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT};
|
||||
|
||||
let tls_parameters = TlsParameters::new_tokio02(relay.into())?;
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSIONS_PORT)
|
||||
@@ -63,9 +104,22 @@ where
|
||||
///
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
)))
|
||||
)]
|
||||
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{TlsParameters, SUBMISSION_PORT};
|
||||
use super::{Tls, TlsParameters, SUBMISSION_PORT};
|
||||
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
@@ -77,7 +131,7 @@ where
|
||||
/// Creates a new local SMTP client to port 25
|
||||
///
|
||||
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
|
||||
pub fn unencrypted_localhost() -> AsyncSmtpTransport<C> {
|
||||
pub fn unencrypted_localhost() -> AsyncSmtpTransport<E> {
|
||||
Self::builder_dangerous("localhost").build()
|
||||
}
|
||||
|
||||
@@ -93,15 +147,37 @@ where
|
||||
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
||||
let mut new = SmtpInfo::default();
|
||||
new.server = server.into();
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
AsyncSmtpTransportBuilder { info: new }
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
impl<E> Debug for AsyncSmtpTransport<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("AsyncSmtpTransport");
|
||||
builder.field("inner", &self.inner);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Clone for AsyncSmtpTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
@@ -133,42 +209,53 @@ impl AsyncSmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
)))
|
||||
)]
|
||||
pub fn tls(mut self, tls: super::Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport (with default pool if enabled)
|
||||
pub fn build<C>(self) -> AsyncSmtpTransport<C>
|
||||
/// Build the transport
|
||||
pub fn build<E>(self) -> AsyncSmtpTransport<E>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
E: Executor,
|
||||
{
|
||||
let connector = Default::default();
|
||||
let client = AsyncSmtpClient {
|
||||
connector,
|
||||
info: self.info,
|
||||
marker_: PhantomData,
|
||||
};
|
||||
AsyncSmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncSmtpClient<C> {
|
||||
connector: C,
|
||||
pub struct AsyncSmtpClient<E> {
|
||||
info: SmtpInfo,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<C> AsyncSmtpClient<C>
|
||||
impl<E> AsyncSmtpClient<E>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
E: Executor,
|
||||
{
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
|
||||
let mut conn = C::connect(
|
||||
let mut conn = E::connect(
|
||||
&self.info.server,
|
||||
self.info.port,
|
||||
&self.info.hello_name,
|
||||
@@ -183,61 +270,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait AsyncSmtpConnector: Default + private::Sealed {
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub struct Tokio02Connector;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl AsyncSmtpConnector for Tokio02Connector {
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
impl<E> Debug for AsyncSmtpClient<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("AsyncSmtpClient");
|
||||
builder.field("info", &self.info);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl Sealed for Tokio02Connector {}
|
||||
impl<E> AsyncSmtpClient<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
info: self.info.clone(),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,14 @@
|
||||
//! Provides limited SASL authentication mechanisms
|
||||
|
||||
use crate::transport::smtp::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use crate::transport::smtp::error::{self, Error};
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
|
||||
/// Accepted authentication mechanisms
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Convertible to user credentials
|
||||
pub trait IntoCredentials {
|
||||
/// Converts to a `Credentials` struct
|
||||
fn into_credentials(self) -> Credentials;
|
||||
}
|
||||
|
||||
impl IntoCredentials for Credentials {
|
||||
fn into_credentials(self) -> Credentials {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
|
||||
fn into_credentials(self) -> Credentials {
|
||||
let (username, password) = self;
|
||||
Credentials::new(username.into(), password.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains user credentials
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Credentials {
|
||||
authentication_identity: String,
|
||||
@@ -44,24 +25,41 @@ impl Credentials {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> From<(S, T)> for Credentials
|
||||
where
|
||||
S: Into<String>,
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from((username, password): (S, T)) -> Self {
|
||||
Credentials::new(username.into(), password.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Credentials {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Credentials").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Mechanism {
|
||||
/// PLAIN authentication mechanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
/// PLAIN authentication mechanism, defined in
|
||||
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
|
||||
Plain,
|
||||
/// LOGIN authentication mechanism
|
||||
/// Obsolete but needed for some providers (like office365)
|
||||
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
|
||||
///
|
||||
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
|
||||
Login,
|
||||
/// Non-standard XOAUTH2 mechanism
|
||||
/// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
/// Non-standard XOAUTH2 mechanism, defined in
|
||||
/// [xoauth2-protocol](https://developers.google.com/gmail/imap/xoauth2-protocol)
|
||||
Xoauth2,
|
||||
}
|
||||
|
||||
impl Display for Mechanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
@@ -88,15 +86,15 @@ impl Mechanism {
|
||||
) -> Result<String, Error> {
|
||||
match self {
|
||||
Mechanism::Plain => match challenge {
|
||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"\u{0}{}\u{0}{}",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
)),
|
||||
},
|
||||
Mechanism::Login => {
|
||||
let decoded_challenge =
|
||||
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
|
||||
let decoded_challenge = challenge
|
||||
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.authentication_identity.to_string());
|
||||
@@ -106,10 +104,10 @@ impl Mechanism {
|
||||
return Ok(credentials.secret.to_string());
|
||||
}
|
||||
|
||||
Err(Error::Client("Unrecognized challenge"))
|
||||
Err(error::client("Unrecognized challenge"))
|
||||
}
|
||||
Mechanism::Xoauth2 => match challenge {
|
||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"user={}\x01auth=Bearer {}\x01\x01",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
@@ -168,4 +166,12 @@ mod test {
|
||||
);
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_user_pass_for_credentials() {
|
||||
assert_eq!(
|
||||
Credentials::new("alice".to_string(), "wonderland".to_string()),
|
||||
Credentials::from(("alice", "wonderland"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use std::{fmt::Display, io};
|
||||
|
||||
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
||||
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::*,
|
||||
error,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
},
|
||||
Envelope,
|
||||
};
|
||||
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use std::fmt::Display;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
@@ -45,18 +44,31 @@ impl AsyncSmtpConnection {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
// FIXME add simple connect and rename this one
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
pub async fn connect_tokio02(
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub async fn connect_tokio1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_tokio02(hostname, port, tls_parameters).await?;
|
||||
let stream = AsyncNetworkStream::connect_tokio1(hostname, port, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub async fn connect_asyncstd1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_asyncstd1(hostname, port, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
@@ -85,9 +97,32 @@ impl AsyncSmtpConnection {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
// Internationalization handling
|
||||
//
|
||||
// * 8BITMIME: https://tools.ietf.org/html/rfc6152
|
||||
// * SMTPUTF8: https://tools.ietf.org/html/rfc653
|
||||
|
||||
// Check for non-ascii addresses and use the SMTPUTF8 option if any.
|
||||
if envelope.has_non_ascii_addresses() {
|
||||
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
|
||||
// don't try to send non-ascii addresses (per RFC)
|
||||
return Err(error::client(
|
||||
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
// Check for non-ascii content in message
|
||||
if !email.is_ascii() {
|
||||
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
return Err(error::client(
|
||||
"Message contains non-ascii chars but server does not support 8BITMIME",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options))
|
||||
.await,
|
||||
@@ -136,7 +171,7 @@ impl AsyncSmtpConnection {
|
||||
try_smtp!(self.ehlo(hello_name).await, self);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||
Err(error::client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,12 +218,10 @@ impl AsyncSmtpConnection {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or(Error::Client(
|
||||
"No compatible authentication mechanism was found",
|
||||
))?;
|
||||
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
let mut challenges: u8 = 10;
|
||||
let mut response = self
|
||||
.command(Auth::new(mechanism, credentials.clone(), None)?)
|
||||
.await?;
|
||||
@@ -207,7 +240,7 @@ impl AsyncSmtpConnection {
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
Err(error::response("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
@@ -231,8 +264,16 @@ impl AsyncSmtpConnection {
|
||||
|
||||
/// Writes a string to the server
|
||||
async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream.get_mut().write_all(string).await?;
|
||||
self.stream.get_mut().flush().await?;
|
||||
self.stream
|
||||
.get_mut()
|
||||
.write_all(string)
|
||||
.await
|
||||
.map_err(error::network)?;
|
||||
self.stream
|
||||
.get_mut()
|
||||
.flush()
|
||||
.await
|
||||
.map_err(error::network)?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
@@ -243,27 +284,33 @@ impl AsyncSmtpConnection {
|
||||
pub async fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer).await? > 0 {
|
||||
while self
|
||||
.stream
|
||||
.read_line(&mut buffer)
|
||||
.await
|
||||
.map_err(error::network)?
|
||||
> 0
|
||||
{
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
if response.is_positive() {
|
||||
return Ok(response);
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(error::code(response.code))
|
||||
}
|
||||
|
||||
return Err(response.into());
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(Error::Parsing(e.1));
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(Error::Parsing(e.1));
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
|
||||
Err(error::response("incomplete response"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
net::{Shutdown, SocketAddr},
|
||||
mem,
|
||||
net::SocketAddr,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures_io::{Error as IoError, ErrorKind, Result as IoResult};
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate::io::{AsyncRead, AsyncWrite};
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate::net::TcpStream;
|
||||
use futures_io::{
|
||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
|
||||
Result as IoResult,
|
||||
};
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
|
||||
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
use tokio02_native_tls_crate::TlsStream;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use async_std::net::TcpStream as AsyncStd1TcpStream;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::net::TcpStream as Tokio1TcpStream;
|
||||
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
use tokio02_rustls::client::TlsStream as RustlsTlsStream;
|
||||
#[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(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
use crate::transport::smtp::Error;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// A network stream
|
||||
pub struct AsyncNetworkStream {
|
||||
@@ -29,17 +43,29 @@ pub struct AsyncNetworkStream {
|
||||
}
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
// usually only one TLS backend at a time is going to be enabled,
|
||||
// so clippy::large_enum_variant doesn't make sense here
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[allow(dead_code)]
|
||||
enum InnerAsyncNetworkStream {
|
||||
/// Plain TCP stream
|
||||
#[cfg(feature = "tokio02")]
|
||||
Tokio02Tcp(TcpStream),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
Tokio02NativeTls(TlsStream<TcpStream>),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
Tokio02RustlsTls(Box<RustlsTlsStream<TcpStream>>),
|
||||
/// Plain Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1")]
|
||||
Tokio1Tcp(Tokio1TcpStream),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>),
|
||||
/// Plain Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1")]
|
||||
AsyncStd1Tcp(AsyncStd1TcpStream),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
AsyncStd1NativeTls(AsyncStd1TlsStream<AsyncStd1TcpStream>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
@@ -47,7 +73,7 @@ enum InnerAsyncNetworkStream {
|
||||
impl AsyncNetworkStream {
|
||||
fn new(inner: InnerAsyncNetworkStream) -> Self {
|
||||
if let InnerAsyncNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
}
|
||||
|
||||
AsyncNetworkStream { inner }
|
||||
@@ -56,51 +82,58 @@ impl AsyncNetworkStream {
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
"InnerAsyncNetworkStream::None should never be built",
|
||||
"InnerAsyncNetworkStream::None must never be built",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdowns the connection
|
||||
pub fn shutdown(&self, how: Shutdown) -> IoResult<()> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.shutdown(how),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
|
||||
s.get_ref().get_ref().get_ref().shutdown(how)
|
||||
}
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.shutdown(how),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub async fn connect_tokio02(
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub async fn connect_tokio1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = TcpStream::connect((hostname, port)).await?;
|
||||
let tcp_stream = Tokio1TcpStream::connect((hostname, port))
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub async fn connect_asyncstd1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port))
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
@@ -109,22 +142,50 @@ impl AsyncNetworkStream {
|
||||
|
||||
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls")))]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-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 = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream) => tcp_stream,
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
|
||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
not(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))
|
||||
))]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the async-std1-native-tls or the async-std1-rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
@@ -132,42 +193,101 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
async fn upgrade_tokio02_tls(
|
||||
tcp_stream: TcpStream,
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
async fn upgrade_tokio1_tls(
|
||||
tcp_stream: Tokio1TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = std::mem::take(&mut tls_parameters.domain);
|
||||
let domain = mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
#[cfg(not(feature = "tokio02-native-tls"))]
|
||||
panic!("built without the tokio02-native-tls feature");
|
||||
#[cfg(not(feature = "tokio1-native-tls"))]
|
||||
panic!("built without the tokio1-native-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
return {
|
||||
use tokio02_native_tls_crate::TlsConnector;
|
||||
use tokio1_native_tls_crate::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(connector);
|
||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
|
||||
let stream = connector
|
||||
.connect(&domain, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "tokio02-rustls-tls"))]
|
||||
panic!("built without the tokio02-rustls-tls feature");
|
||||
#[cfg(not(feature = "tokio1-rustls-tls"))]
|
||||
panic!("built without the tokio1-rustls-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
return {
|
||||
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
|
||||
let domain =
|
||||
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = connector.connect(domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(Box::new(stream)))
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
.connect(domain, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
async fn upgrade_asyncstd1_tls(
|
||||
tcp_stream: AsyncStd1TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
|
||||
|
||||
/*
|
||||
#[cfg(not(feature = "async-std1-native-tls"))]
|
||||
panic!("built without the async-std1-native-tls feature");
|
||||
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
return {
|
||||
use async_native_tls::TlsConnector;
|
||||
|
||||
// TODO: fix
|
||||
let connector: TlsConnector = todo!();
|
||||
// let connector = TlsConnector::from(connector);
|
||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1NativeTls(stream))
|
||||
};
|
||||
*/
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "async-std1-rustls-tls"))]
|
||||
panic!("built without the async-std1-rustls-tls feature");
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
return {
|
||||
use async_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
|
||||
let domain =
|
||||
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
|
||||
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
.connect(domain, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -175,70 +295,144 @@ impl AsyncNetworkStream {
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => false,
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
|
||||
InnerAsyncNetworkStream::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl futures_io::AsyncRead for AsyncNetworkStream {
|
||||
impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(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 = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(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 = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(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")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
||||
Pin::new(s).poll_read(cx, buf)
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
||||
Pin::new(s).poll_read(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl futures_io::AsyncWrite for AsyncNetworkStream {
|
||||
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<IoResult<usize>> {
|
||||
impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
||||
Pin::new(s).poll_write(cx, buf)
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
||||
Pin::new(s).poll_write(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<IoResult<()>> {
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<IoResult<()>> {
|
||||
Poll::Ready(self.shutdown(Shutdown::Write))
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ use std::{
|
||||
|
||||
use super::{ClientCodec, NetworkStream, TlsParameters};
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::*,
|
||||
error,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
},
|
||||
Envelope,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
@@ -66,7 +67,7 @@ impl SmtpConnection {
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
conn.set_timeout(timeout)?;
|
||||
conn.set_timeout(timeout).map_err(error::network)?;
|
||||
// TODO log
|
||||
let _response = conn.read_response()?;
|
||||
|
||||
@@ -82,9 +83,32 @@ impl SmtpConnection {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
// Internationalization handling
|
||||
//
|
||||
// * 8BITMIME: https://tools.ietf.org/html/rfc6152
|
||||
// * SMTPUTF8: https://tools.ietf.org/html/rfc653
|
||||
|
||||
// Check for non-ascii addresses and use the SMTPUTF8 option if any.
|
||||
if envelope.has_non_ascii_addresses() {
|
||||
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
|
||||
// don't try to send non-ascii addresses (per RFC)
|
||||
return Err(error::client(
|
||||
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
// Check for non-ascii content in message
|
||||
if !email.is_ascii() {
|
||||
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
return Err(error::client(
|
||||
"Message contains non-ascii chars but server does not support 8BITMIME",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options)),
|
||||
self
|
||||
@@ -133,7 +157,7 @@ impl SmtpConnection {
|
||||
// when a TLS library is enabled
|
||||
unreachable!("TLS support required but not supported");
|
||||
} else {
|
||||
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||
Err(error::client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +210,7 @@ impl SmtpConnection {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or(Error::Client(
|
||||
"No compatible authentication mechanism was found",
|
||||
))?;
|
||||
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
@@ -207,7 +229,7 @@ impl SmtpConnection {
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
Err(error::response("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
@@ -231,8 +253,11 @@ impl SmtpConnection {
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream.get_mut().write_all(string)?;
|
||||
self.stream.get_mut().flush()?;
|
||||
self.stream
|
||||
.get_mut()
|
||||
.write_all(string)
|
||||
.map_err(error::network)?;
|
||||
self.stream.get_mut().flush().map_err(error::network)?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
@@ -243,27 +268,27 @@ impl SmtpConnection {
|
||||
pub fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer)? > 0 {
|
||||
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
if response.is_positive() {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
return Err(response.into());
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(error::code(response.code))
|
||||
};
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(Error::Parsing(e.1));
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(Error::Parsing(e.1));
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
|
||||
Err(error::response("incomplete response"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Write},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
pub type MockCursor = Cursor<Vec<u8>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockStream {
|
||||
reader: Arc<Mutex<MockCursor>>,
|
||||
writer: Arc<Mutex<MockCursor>>,
|
||||
}
|
||||
|
||||
impl Default for MockStream {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MockStream {
|
||||
pub fn new() -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_vec(vec: Vec<u8>) -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_vec(&mut self) -> Vec<u8> {
|
||||
let mut cursor = self.writer.lock().unwrap();
|
||||
let vec = cursor.get_ref().to_vec();
|
||||
cursor.set_position(0);
|
||||
cursor.get_mut().clear();
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn next_vec(&mut self, vec: &[u8]) {
|
||||
let mut cursor = self.reader.lock().unwrap();
|
||||
cursor.set_position(0);
|
||||
cursor.get_mut().clear();
|
||||
cursor.get_mut().extend_from_slice(vec);
|
||||
}
|
||||
|
||||
pub fn swap(&mut self) {
|
||||
let mut cur_write = self.writer.lock().unwrap();
|
||||
let mut cur_read = self.reader.lock().unwrap();
|
||||
let vec_write = cur_write.get_ref().to_vec();
|
||||
let vec_read = cur_read.get_ref().to_vec();
|
||||
cur_write.set_position(0);
|
||||
cur_read.set_position(0);
|
||||
cur_write.get_mut().clear();
|
||||
cur_read.get_mut().clear();
|
||||
// swap cursors
|
||||
cur_read.get_mut().extend_from_slice(vec_write.as_slice());
|
||||
cur_write.get_mut().extend_from_slice(vec_read.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for MockStream {
|
||||
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
|
||||
self.writer.lock().unwrap().write(msg)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for MockStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.reader.lock().unwrap().read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::MockStream;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[test]
|
||||
fn write_take_test() {
|
||||
let mut mock = MockStream::new();
|
||||
// write to mock stream
|
||||
mock.write_all(&[1, 2, 3]).unwrap();
|
||||
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_with_vec_test() {
|
||||
let mut mock = MockStream::with_vec(vec![4, 5]);
|
||||
let mut vec = Vec::new();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut cloned = mock.clone();
|
||||
mock.write_all(&[6, 7]).unwrap();
|
||||
assert_eq!(cloned.take_vec(), vec![6, 7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut vec = Vec::new();
|
||||
mock.write_all(&[8, 9, 10]).unwrap();
|
||||
mock.swap();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![8, 9, 10]);
|
||||
}
|
||||
}
|
||||
@@ -3,53 +3,54 @@
|
||||
//! `SmtpConnection` allows manually sending SMTP commands.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(feature = "smtp-transport")]
|
||||
//! # {
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection};
|
||||
//!
|
||||
//! let hello = ClientId::Domain("my_hostname".to_string());
|
||||
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None).unwrap();
|
||||
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None)?;
|
||||
//! client.command(
|
||||
//! Mail::new(Some("user@example.com".parse().unwrap()), vec![])
|
||||
//! ).unwrap();
|
||||
//! Mail::new(Some("user@example.com".parse()?), vec![])
|
||||
//! )?;
|
||||
//! client.command(
|
||||
//! Rcpt::new("user@example.org".parse().unwrap(), vec![])
|
||||
//! ).unwrap();
|
||||
//! client.command(Data).unwrap();
|
||||
//! client.message("Test email".as_bytes()).unwrap();
|
||||
//! client.command(Quit).unwrap();
|
||||
//! Rcpt::new("user@example.org".parse()?, vec![])
|
||||
//! )?;
|
||||
//! client.command(Data)?;
|
||||
//! client.message("Test email".as_bytes())?;
|
||||
//! client.command(Quit)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub(crate) use self::async_connection::AsyncSmtpConnection;
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub(crate) use self::async_net::AsyncNetworkStream;
|
||||
use self::net::NetworkStream;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
pub(super) use self::tls::InnerTlsParameters;
|
||||
pub use self::{
|
||||
connection::SmtpConnection,
|
||||
mock::MockStream,
|
||||
tls::{Tls, TlsParameters},
|
||||
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
|
||||
};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_connection;
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_net;
|
||||
mod connection;
|
||||
mod mock;
|
||||
mod net;
|
||||
mod tls;
|
||||
|
||||
/// The codec used for transparency
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ClientCodec {
|
||||
struct ClientCodec {
|
||||
escape_count: u8,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
mem,
|
||||
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -14,8 +13,8 @@ use rustls::{ClientSession, StreamOwned};
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::{MockStream, TlsParameters};
|
||||
use crate::transport::smtp::Error;
|
||||
use super::TlsParameters;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// A network stream
|
||||
pub struct NetworkStream {
|
||||
@@ -23,6 +22,9 @@ pub struct NetworkStream {
|
||||
}
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
// usually only one TLS backend at a time is going to be enabled,
|
||||
// so clippy::large_enum_variant doesn't make sense here
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum InnerNetworkStream {
|
||||
/// Plain TCP stream
|
||||
Tcp(TcpStream),
|
||||
@@ -31,18 +33,18 @@ enum InnerNetworkStream {
|
||||
NativeTls(TlsStream<TcpStream>),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(Box<StreamOwned<ClientSession, TcpStream>>),
|
||||
/// Mock stream
|
||||
Mock(MockStream),
|
||||
RustlsTls(StreamOwned<ClientSession, TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
impl NetworkStream {
|
||||
fn new(inner: InnerNetworkStream) -> Self {
|
||||
NetworkStream { inner }
|
||||
}
|
||||
if let InnerNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
}
|
||||
|
||||
pub fn new_mock(mock: MockStream) -> Self {
|
||||
Self::new(InnerNetworkStream::Mock(mock))
|
||||
NetworkStream { inner }
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
@@ -53,10 +55,13 @@ impl NetworkStream {
|
||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
80,
|
||||
))),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
80,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +73,10 @@ impl NetworkStream {
|
||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,18 +89,18 @@ impl NetworkStream {
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
) -> Result<TcpStream, Error> {
|
||||
let addrs = server.to_socket_addrs()?;
|
||||
let addrs = server.to_socket_addrs().map_err(error::connection)?;
|
||||
for addr in addrs {
|
||||
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Err(Error::Client("Could not connect"))
|
||||
Err(error::connection("Could not connect"))
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
Some(t) => try_connect_timeout(server, t)?,
|
||||
None => TcpStream::connect(server)?,
|
||||
None => TcpStream::connect(server).map_err(error::connection)?,
|
||||
};
|
||||
|
||||
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
|
||||
@@ -113,8 +121,7 @@ impl NetworkStream {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream =
|
||||
std::mem::replace(&mut self.inner, InnerNetworkStream::Mock(MockStream::new()));
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
@@ -137,31 +144,33 @@ impl NetworkStream {
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
let stream = connector
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(|err| Error::Io(io::Error::new(io::ErrorKind::Other, err)))?;
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::NativeTls(stream)
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(connector) => {
|
||||
use webpki::DNSNameRef;
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())?;
|
||||
let stream = StreamOwned::new(
|
||||
ClientSession::new(&Arc::new(connector.clone()), domain),
|
||||
tcp_stream,
|
||||
);
|
||||
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())
|
||||
.map_err(error::connection)?;
|
||||
let stream = StreamOwned::new(ClientSession::new(&connector, domain), tcp_stream);
|
||||
|
||||
InnerNetworkStream::RustlsTls(Box::new(stream))
|
||||
InnerNetworkStream::RustlsTls(stream)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(_) | InnerNetworkStream::Mock(_) => false,
|
||||
InnerNetworkStream::Tcp(_) => false,
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => true,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(_) => true,
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +185,10 @@ impl NetworkStream {
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +206,10 @@ impl NetworkStream {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,7 +222,10 @@ impl Read for NetworkStream {
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,7 +238,10 @@ impl Write for NetworkStream {
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +252,10 @@ impl Write for NetworkStream {
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use crate::transport::smtp::error::Error;
|
||||
|
||||
use crate::transport::smtp::{error, Error};
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::ClientConfig;
|
||||
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
|
||||
use std::fmt::{self, Debug};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use webpki::DNSNameRef;
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
@@ -20,79 +24,273 @@ pub enum Tls {
|
||||
None,
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
Opportunistic(TlsParameters),
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
Required(TlsParameters),
|
||||
/// Use TLS wrapped connection
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
Wrapper(TlsParameters),
|
||||
}
|
||||
|
||||
impl Debug for Tls {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::None => f.pad("None"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Self::Opportunistic(_) => f.pad("Opportunistic"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Self::Required(_) => f.pad("Required"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Self::Wrapper(_) => f.pad("Wrapper"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct TlsParameters {
|
||||
pub(crate) connector: InnerTlsParameters,
|
||||
/// The domain name which is expected in the TLS certificate from the server
|
||||
pub(super) domain: String,
|
||||
}
|
||||
|
||||
/// Builder for `TlsParameters`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TlsParametersBuilder {
|
||||
domain: String,
|
||||
root_certs: Vec<Certificate>,
|
||||
accept_invalid_hostnames: bool,
|
||||
accept_invalid_certs: bool,
|
||||
}
|
||||
|
||||
impl TlsParametersBuilder {
|
||||
/// Creates a new builder for `TlsParameters`
|
||||
pub fn new(domain: String) -> Self {
|
||||
Self {
|
||||
domain,
|
||||
root_certs: Vec::new(),
|
||||
accept_invalid_hostnames: false,
|
||||
accept_invalid_certs: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a custom root certificate
|
||||
///
|
||||
/// Can be used to safely connect to a server using a self signed certificate, for example.
|
||||
pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
|
||||
self.root_certs.push(cert);
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls whether certificates with an invalid hostname are accepted
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// You should think very carefully before using this method.
|
||||
/// If hostname verification is disabled *any* valid certificate,
|
||||
/// including those from other sites, are trusted.
|
||||
///
|
||||
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
||||
///
|
||||
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
|
||||
self.accept_invalid_hostnames = accept_invalid_hostnames;
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls whether invalid certificates are accepted
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// You should think very carefully before using this method.
|
||||
/// If certificate verification is disabled, *any* certificate
|
||||
/// is trusted for use, including:
|
||||
///
|
||||
/// * Self signed certificates
|
||||
/// * Certificates from different hostnames
|
||||
/// * Expired certificates
|
||||
///
|
||||
/// This method should only be used as a last resort, as it introduces
|
||||
/// significant vulnerabilities to man-in-the-middle attacks.
|
||||
pub fn dangerous_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
|
||||
self.accept_invalid_certs = accept_invalid_certs;
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
pub fn build(self) -> Result<TlsParameters, Error> {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
return self.build_rustls();
|
||||
|
||||
#[cfg(not(feature = "rustls-tls"))]
|
||||
return self.build_native();
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls with the provided configuration
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn build_native(self) -> Result<TlsParameters, Error> {
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
|
||||
for cert in self.root_certs {
|
||||
tls_builder.add_root_certificate(cert.native_tls);
|
||||
}
|
||||
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
|
||||
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
|
||||
|
||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
|
||||
let connector = tls_builder.build().map_err(error::tls)?;
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::NativeTls(connector),
|
||||
domain: self.domain,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls with the provided configuration
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||
use webpki_roots::TLS_SERVER_ROOTS;
|
||||
|
||||
let mut tls = ClientConfig::new();
|
||||
|
||||
for cert in self.root_certs {
|
||||
for rustls_cert in cert.rustls {
|
||||
tls.root_store.add(&rustls_cert).map_err(error::tls)?;
|
||||
}
|
||||
}
|
||||
if self.accept_invalid_certs {
|
||||
tls.dangerous()
|
||||
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {}));
|
||||
}
|
||||
|
||||
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
||||
domain: self.domain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum InnerTlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsConnector),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(ClientConfig),
|
||||
RustlsTls(Arc<ClientConfig>),
|
||||
}
|
||||
|
||||
impl TlsParameters {
|
||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
pub fn new(domain: String) -> Result<Self, Error> {
|
||||
#[cfg(feature = "native-tls")]
|
||||
return Self::new_native(domain);
|
||||
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
return Self::new_rustls(domain);
|
||||
TlsParametersBuilder::new(domain).build()
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
pub(crate) fn new_tokio02(domain: String) -> Result<Self, Error> {
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
return Self::new_native(domain);
|
||||
|
||||
#[cfg(not(feature = "tokio02-native-tls"))]
|
||||
return Self::new_rustls(domain);
|
||||
pub fn builder(domain: String) -> TlsParametersBuilder {
|
||||
TlsParametersBuilder::new(domain)
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn new_native(domain: String) -> Result<Self, Error> {
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
|
||||
let connector = tls_builder.build()?;
|
||||
Ok(Self {
|
||||
connector: InnerTlsParameters::NativeTls(connector),
|
||||
domain,
|
||||
})
|
||||
TlsParametersBuilder::new(domain).build_native()
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
pub fn new_rustls(domain: String) -> Result<Self, Error> {
|
||||
use webpki_roots::TLS_SERVER_ROOTS;
|
||||
|
||||
let mut tls = ClientConfig::new();
|
||||
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
|
||||
Ok(Self {
|
||||
connector: InnerTlsParameters::RustlsTls(tls),
|
||||
domain,
|
||||
})
|
||||
TlsParametersBuilder::new(domain).build_rustls()
|
||||
}
|
||||
|
||||
pub fn domain(&self) -> &str {
|
||||
&self.domain
|
||||
}
|
||||
}
|
||||
|
||||
/// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
pub struct Certificate {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls::Certificate,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: Vec<rustls::Certificate>,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
impl Certificate {
|
||||
/// Create a `Certificate` from a DER encoded certificate
|
||||
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
|
||||
#[cfg(feature = "native-tls")]
|
||||
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: vec![rustls::Certificate(der)],
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a `Certificate` from a PEM encoded certificate
|
||||
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
|
||||
#[cfg(feature = "native-tls")]
|
||||
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let rustls_cert = {
|
||||
use rustls::internal::pemfile;
|
||||
use std::io::Cursor;
|
||||
|
||||
let mut pem = Cursor::new(pem);
|
||||
pemfile::certs(&mut pem).map_err(|_| error::tls("invalid certificates"))?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: rustls_cert,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Certificate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Certificate").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
struct InvalidCertsVerifier;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_roots: &RootCertStore,
|
||||
_presented_certs: &[rustls::Certificate],
|
||||
_dns_name: DNSNameRef<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
) -> Result<ServerCertVerified, TLSError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
//! SMTP commands
|
||||
|
||||
use crate::{
|
||||
address::Address,
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
error::Error,
|
||||
error::{self, Error},
|
||||
extension::{ClientId, MailParameter, RcptParameter},
|
||||
response::Response,
|
||||
},
|
||||
Address,
|
||||
};
|
||||
use std::{
|
||||
convert::AsRef,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
@@ -22,7 +19,7 @@ pub struct Ehlo {
|
||||
}
|
||||
|
||||
impl Display for Ehlo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "EHLO {}\r\n", self.client_id)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +37,7 @@ impl Ehlo {
|
||||
pub struct Starttls;
|
||||
|
||||
impl Display for Starttls {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("STARTTLS\r\n")
|
||||
}
|
||||
}
|
||||
@@ -54,11 +51,11 @@ pub struct Mail {
|
||||
}
|
||||
|
||||
impl Display for Mail {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
|
||||
self.sender.as_ref().map_or("", |s| s.as_ref())
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
@@ -83,7 +80,7 @@ pub struct Rcpt {
|
||||
}
|
||||
|
||||
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)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
@@ -108,7 +105,7 @@ impl Rcpt {
|
||||
pub struct Data;
|
||||
|
||||
impl Display for Data {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("DATA\r\n")
|
||||
}
|
||||
}
|
||||
@@ -119,7 +116,7 @@ impl Display for Data {
|
||||
pub struct Quit;
|
||||
|
||||
impl Display for Quit {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("QUIT\r\n")
|
||||
}
|
||||
}
|
||||
@@ -130,7 +127,7 @@ impl Display for Quit {
|
||||
pub struct Noop;
|
||||
|
||||
impl Display for Noop {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("NOOP\r\n")
|
||||
}
|
||||
}
|
||||
@@ -143,7 +140,7 @@ pub struct Help {
|
||||
}
|
||||
|
||||
impl Display for Help {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("HELP")?;
|
||||
if let Some(argument) = &self.argument {
|
||||
write!(f, " {}", argument)?;
|
||||
@@ -167,7 +164,7 @@ pub struct Vrfy {
|
||||
}
|
||||
|
||||
impl Display for Vrfy {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "VRFY {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
@@ -187,7 +184,7 @@ pub struct Expn {
|
||||
}
|
||||
|
||||
impl Display for Expn {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "EXPN {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
@@ -205,7 +202,7 @@ impl Expn {
|
||||
pub struct Rset;
|
||||
|
||||
impl Display for Rset {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("RSET\r\n")
|
||||
}
|
||||
}
|
||||
@@ -221,7 +218,7 @@ pub struct 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);
|
||||
|
||||
if self.mechanism.supports_initial_response() {
|
||||
@@ -264,16 +261,17 @@ impl Auth {
|
||||
response: &Response,
|
||||
) -> Result<Auth, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
return Err(error::response("Expecting a challenge"));
|
||||
}
|
||||
|
||||
let encoded_challenge = response
|
||||
.first_word()
|
||||
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
|
||||
.ok_or_else(|| error::response("Could not read auth challenge"))?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
||||
|
||||
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
|
||||
let decoded_base64 = base64::decode(&encoded_challenge).map_err(error::response)?;
|
||||
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
@@ -338,7 +336,7 @@ mod test {
|
||||
"RCPT TO:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Rcpt::new(email.clone(), vec![rcpt_parameter])),
|
||||
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Quit), "QUIT\r\n");
|
||||
@@ -369,7 +367,7 @@ mod test {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||
Auth::new(Mechanism::Login, credentials, None).unwrap()
|
||||
),
|
||||
"AUTH LOGIN\r\n"
|
||||
);
|
||||
|
||||
@@ -1,154 +1,184 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use self::Error::*;
|
||||
use crate::transport::smtp::response::{Response, Severity};
|
||||
use base64::DecodeError;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
use crate::{
|
||||
transport::smtp::response::{Code, Severity},
|
||||
BoxError,
|
||||
};
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Transient(Response),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Permanent(Response),
|
||||
/// Error parsing a response
|
||||
ResponseParsing(&'static str),
|
||||
/// Error parsing a base64 string in response
|
||||
ChallengeParsing(DecodeError),
|
||||
/// Error parsing UTF8in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// DNS resolution error
|
||||
Resolution,
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// TLS error
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(native_tls::Error),
|
||||
/// Parsing error
|
||||
Parsing(nom::error::ErrorKind),
|
||||
/// Invalid hostname
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InvalidDNSName(webpki::InvalidDNSNameError),
|
||||
#[cfg(feature = "r2d2")]
|
||||
Pool(r2d2::Error),
|
||||
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
|
||||
|
||||
/// The Errors that may occur when sending an email over SMTP
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
// Try to display the first line of the server's response that usually
|
||||
// contains a short humanly readable error message
|
||||
Transient(ref err) => fmt.write_str(match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "transient error during SMTP transaction",
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
Permanent(ref err) => fmt.write_str(match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "permanent error during SMTP transaction",
|
||||
}),
|
||||
ResponseParsing(err) => fmt.write_str(err),
|
||||
ChallengeParsing(ref err) => err.fmt(fmt),
|
||||
Utf8Parsing(ref err) => err.fmt(fmt),
|
||||
Resolution => fmt.write_str("could not resolve hostname"),
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(ref err) => err.fmt(fmt),
|
||||
Parsing(ref err) => fmt.write_str(err.description()),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InvalidDNSName(ref err) => err.fmt(fmt),
|
||||
#[cfg(feature = "r2d2")]
|
||||
Pool(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
ChallengeParsing(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
Io(ref err) => Some(&*err),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(ref err) => Some(&*err),
|
||||
/// Returns true if the error is from response
|
||||
pub fn is_response(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Response)
|
||||
}
|
||||
|
||||
/// Returns true if the error is from client
|
||||
pub fn is_client(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Client)
|
||||
}
|
||||
|
||||
/// Returns true if the error is a transient SMTP error
|
||||
pub fn is_transient(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Transient(_))
|
||||
}
|
||||
|
||||
/// Returns true if the error is a permanent SMTP error
|
||||
pub fn is_permanent(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Permanent(_))
|
||||
}
|
||||
|
||||
/// Returns true if the error is caused by a timeout
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
let mut source = self.source();
|
||||
|
||||
while let Some(err) = source {
|
||||
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
|
||||
return io_err.kind() == std::io::ErrorKind::TimedOut;
|
||||
}
|
||||
|
||||
source = err.source();
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if the error is from TLS
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
pub fn is_tls(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Tls)
|
||||
}
|
||||
|
||||
/// Returns the status code, if the error was generated from a response.
|
||||
pub fn status(&self) -> Option<Code> {
|
||||
match self.inner.kind {
|
||||
Kind::Transient(code) | Kind::Permanent(code) => Some(code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Transient(Code),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Permanent(Code),
|
||||
/// Error parsing a response
|
||||
Response,
|
||||
/// Internal client error
|
||||
Client,
|
||||
/// Connection error
|
||||
Connection,
|
||||
/// Underlying network i/o error
|
||||
Network,
|
||||
/// TLS error
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Tls,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::smtp::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(ref source) = self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
impl From<native_tls::Error> for Error {
|
||||
fn from(err: native_tls::Error) -> Error {
|
||||
Tls(err)
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.inner.kind {
|
||||
Kind::Response => f.write_str("response error")?,
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
Kind::Network => f.write_str("network error")?,
|
||||
Kind::Connection => f.write_str("Connection error")?,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Kind::Tls => f.write_str("tls error")?,
|
||||
Kind::Transient(ref code) => {
|
||||
write!(f, "transient error ({})", code)?;
|
||||
}
|
||||
Kind::Permanent(ref code) => {
|
||||
write!(f, "permanent error ({})", code)?;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref e) = self.inner.source {
|
||||
write!(f, ": {}", e)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<(&str, nom::error::ErrorKind)>> for Error {
|
||||
fn from(err: nom::Err<(&str, nom::error::ErrorKind)>) -> Error {
|
||||
Parsing(match err {
|
||||
nom::Err::Incomplete(_) => nom::error::ErrorKind::Complete,
|
||||
nom::Err::Failure((_, k)) => k,
|
||||
nom::Err::Error((_, k)) => k,
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for Error {
|
||||
fn from(err: DecodeError) -> Error {
|
||||
ChallengeParsing(err)
|
||||
pub(crate) fn code(c: Code) -> Error {
|
||||
match c.severity {
|
||||
Severity::TransientNegativeCompletion => Error::new::<Error>(Kind::Transient(c), None),
|
||||
Severity::PermanentNegativeCompletion => Error::new::<Error>(Kind::Permanent(c), None),
|
||||
_ => client("Unknown error code"),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Response, Some(e))
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
impl From<webpki::InvalidDNSNameError> for Error {
|
||||
fn from(err: webpki::InvalidDNSNameError) -> Error {
|
||||
InvalidDNSName(err)
|
||||
}
|
||||
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Client, Some(e))
|
||||
}
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
impl From<r2d2::Error> for Error {
|
||||
fn from(err: r2d2::Error) -> Error {
|
||||
Pool(err)
|
||||
}
|
||||
pub(crate) fn network<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Network, Some(e))
|
||||
}
|
||||
|
||||
impl From<Response> for Error {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.code.severity {
|
||||
Severity::TransientNegativeCompletion => Transient(response),
|
||||
Severity::PermanentNegativeCompletion => Permanent(response),
|
||||
_ => Client("Unknown error code"),
|
||||
}
|
||||
}
|
||||
pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Connection, Some(e))
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Client(string)
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Tls, Some(e))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! ESMTP features
|
||||
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism, error::Error, response::Response, util::XText,
|
||||
authentication::Mechanism,
|
||||
error::{self, Error},
|
||||
response::Response,
|
||||
util::XText,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
@@ -48,7 +51,7 @@ impl Default for ClientId {
|
||||
}
|
||||
|
||||
impl Display for ClientId {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Domain(ref value) => f.write_str(value),
|
||||
Self::Ipv4(ref value) => write!(f, "[{}]", value),
|
||||
@@ -58,6 +61,7 @@ impl Display for ClientId {
|
||||
}
|
||||
|
||||
impl ClientId {
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
|
||||
/// Creates a new `ClientId` from a fully qualified domain name
|
||||
pub fn new(domain: String) -> Self {
|
||||
@@ -71,22 +75,22 @@ impl ClientId {
|
||||
pub enum Extension {
|
||||
/// 8BITMIME keyword
|
||||
///
|
||||
/// RFC 6152: https://tools.ietf.org/html/rfc6152
|
||||
/// Defined in [RFC 6152](https://tools.ietf.org/html/rfc6152)
|
||||
EightBitMime,
|
||||
/// SMTPUTF8 keyword
|
||||
///
|
||||
/// RFC 6531: https://tools.ietf.org/html/rfc6531
|
||||
/// Defined in [RFC 6531](https://tools.ietf.org/html/rfc6531)
|
||||
SmtpUtfEight,
|
||||
/// STARTTLS keyword
|
||||
///
|
||||
/// RFC 2487: https://tools.ietf.org/html/rfc2487
|
||||
/// Defined in [RFC 2487](https://tools.ietf.org/html/rfc2487)
|
||||
StartTls,
|
||||
/// AUTH mechanism
|
||||
Authentication(Mechanism),
|
||||
}
|
||||
|
||||
impl Display for Extension {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
@@ -111,7 +115,7 @@ pub struct 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() {
|
||||
"no supported features".to_string()
|
||||
} else {
|
||||
@@ -126,7 +130,7 @@ impl ServerInfo {
|
||||
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||
let name = match response.first_word() {
|
||||
Some(name) => name,
|
||||
None => return Err(Error::ResponseParsing("Could not read server name")),
|
||||
None => return Err(error::response("Could not read server name")),
|
||||
};
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
@@ -215,7 +219,7 @@ pub enum MailParameter {
|
||||
}
|
||||
|
||||
impl Display for MailParameter {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
|
||||
MailParameter::Size(size) => write!(f, "SIZE={}", size),
|
||||
@@ -243,7 +247,7 @@ pub enum MailBodyParameter {
|
||||
}
|
||||
|
||||
impl Display for MailBodyParameter {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
MailBodyParameter::SevenBit => f.write_str("7BIT"),
|
||||
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
|
||||
@@ -265,7 +269,7 @@ pub enum RcptParameter {
|
||||
}
|
||||
|
||||
impl Display for RcptParameter {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
@@ -320,7 +324,7 @@ mod test {
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: eightbitmime.clone(),
|
||||
features: eightbitmime,
|
||||
}
|
||||
),
|
||||
"name with {EightBitMime}".to_string()
|
||||
@@ -347,7 +351,7 @@ mod test {
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: plain.clone(),
|
||||
features: plain,
|
||||
}
|
||||
),
|
||||
"name with {Authentication(Plain)}".to_string()
|
||||
|
||||
@@ -31,133 +31,96 @@
|
||||
//! This is the most basic example of usage:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(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>> {
|
||||
//! use lettre::{Message, Transport, SmtpTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Create TLS transport on port 465
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")
|
||||
//! .expect("relay valid")
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! .build();
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
//! #### Complete example
|
||||
//!
|
||||
//! ```todo
|
||||
//! # #[cfg(feature = "smtp-transport")]
|
||||
//! # {
|
||||
//! use lettre::transport::smtp::authentication::{Credentials, Mechanism};
|
||||
//! use lettre::{Email, Envelope, Transport, SmtpClient};
|
||||
//! use lettre::transport::smtp::extension::ClientId;
|
||||
//! #### Authentication
|
||||
//!
|
||||
//! let email_1 = Email::new(
|
||||
//! Envelope::new(
|
||||
//! Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
//! vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
//! ).unwrap(),
|
||||
//! "id1".to_string(),
|
||||
//! "Hello world".to_string().into_bytes(),
|
||||
//! );
|
||||
//! Example with authentication and connection pool:
|
||||
//!
|
||||
//! let email_2 = Email::new(
|
||||
//! Envelope::new(
|
||||
//! Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
//! vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
//! ).unwrap(),
|
||||
//! "id2".to_string(),
|
||||
//! "Hello world a second time".to_string().into_bytes(),
|
||||
//! );
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::{PoolConfig, authentication::{Credentials, Mechanism}}};
|
||||
//!
|
||||
//! // Connect to a remote server on a custom port
|
||||
//! let mut mailer = SmtpClient::new_simple("server.tld").unwrap()
|
||||
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
||||
//! .hello_name(ClientId::Domain("my.hostname.tld".to_string()))
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials(Credentials::new("username".to_string(), "password".to_string()))
|
||||
//! // Enable SMTPUTF8 if the server supports it
|
||||
//! .smtp_utf8(true)
|
||||
//! // Configure expected authentication mechanism
|
||||
//! .authentication_mechanism(Mechanism::Plain)
|
||||
//! // Enable connection reuse
|
||||
//! .connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result_1 = mailer.send(&email_1);
|
||||
//! assert!(result_1.is_ok());
|
||||
//! // Create TLS transport on port 587 with STARTTLS
|
||||
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials(Credentials::new("username".to_string(), "password".to_string()))
|
||||
//! // Configure expected authentication mechanism
|
||||
//! .authentication(vec![Mechanism::Plain])
|
||||
//! // Connection pool settings
|
||||
//! .pool_config( PoolConfig::new().max_size(20))
|
||||
//! .build();
|
||||
//!
|
||||
//! // The second email will use the same connection
|
||||
//! let result_2 = mailer.send(&email_2);
|
||||
//! assert!(result_2.is_ok());
|
||||
//!
|
||||
//! // Explicitly close the SMTP transaction as we enabled connection reuse
|
||||
//! mailer.close();
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! You can specify custom TLS settings:
|
||||
//!
|
||||
//! ```todo
|
||||
//! # #[cfg(feature = "native-tls")]
|
||||
//! # {
|
||||
//! use lettre::{
|
||||
//! ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
|
||||
//! Email, SmtpClient, Transport,
|
||||
//! };
|
||||
//! use lettre::transport::smtp::authentication::{Credentials, Mechanism};
|
||||
//! use lettre::transport::smtp::ConnectionReuseParameters;
|
||||
//! use native_tls::{Protocol, TlsConnector};
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::client::{TlsParameters, Tls}};
|
||||
//!
|
||||
//! let email = Email::new(
|
||||
//! Envelope::new(
|
||||
//! Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
//! vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
//! ).unwrap(),
|
||||
//! "message_id".to_string(),
|
||||
//! "Hello world".to_string().into_bytes(),
|
||||
//! );
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let mut tls_builder = TlsConnector::builder();
|
||||
//! tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
|
||||
//! let tls_parameters =
|
||||
//! ClientTlsParameters::new(
|
||||
//! "smtp.example.com".to_string(),
|
||||
//! tls_builder.build().unwrap()
|
||||
//! );
|
||||
//! // Custom TLS configuration
|
||||
//! let tls = TlsParameters::builder("smtp.example.com".to_string())
|
||||
//! .dangerous_accept_invalid_certs(true).build()?;
|
||||
//!
|
||||
//! let mut mailer = SmtpClient::new(
|
||||
//! ("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
|
||||
//! ).unwrap()
|
||||
//! .authentication_mechanism(Mechanism::Login)
|
||||
//! .credentials(Credentials::new(
|
||||
//! "example_username".to_string(), "example_password".to_string()
|
||||
//! ))
|
||||
//! .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||
//! .transport();
|
||||
//! // Create TLS transport on port 465
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! // Custom TLS configuration
|
||||
//! .tls(Tls::Required(tls))
|
||||
//! .build();
|
||||
//!
|
||||
//! let result = mailer.send(&email);
|
||||
//!
|
||||
//! assert!(result.is_ok());
|
||||
//!
|
||||
//! mailer.close();
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub use self::async_transport::{
|
||||
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder, Tokio02Connector,
|
||||
};
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub use self::pool::PoolConfig;
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub(crate) use self::transport::SmtpClient;
|
||||
pub use self::{
|
||||
error::Error,
|
||||
@@ -174,7 +137,7 @@ use crate::transport::smtp::{
|
||||
use client::Tls;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_transport;
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
@@ -182,7 +145,7 @@ pub mod commands;
|
||||
mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub mod pool;
|
||||
mod pool;
|
||||
pub mod response;
|
||||
mod transport;
|
||||
pub mod util;
|
||||
@@ -197,14 +160,13 @@ pub const SMTP_PORT: u16 = 25;
|
||||
pub const SUBMISSION_PORT: u16 = 587;
|
||||
/// Default submission over TLS port
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc8314
|
||||
/// Defined in [RFC8314](https://tools.ietf.org/html/rfc8314)
|
||||
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
/// Default timeout
|
||||
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct SmtpInfo {
|
||||
/// Name sent during EHLO
|
||||
hello_name: ClientId,
|
||||
|
||||
@@ -1,5 +1,82 @@
|
||||
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
|
||||
use r2d2::ManageConnection;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::transport::smtp::{client::SmtpConnection, error, error::Error, SmtpClient};
|
||||
|
||||
use r2d2::{CustomizeConnection, ManageConnection, Pool};
|
||||
|
||||
/// Configuration for a connection pool
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
pub struct PoolConfig {
|
||||
min_idle: u32,
|
||||
max_size: u32,
|
||||
connection_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl PoolConfig {
|
||||
/// Create a new pool configuration with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Minimum number of idle connections
|
||||
///
|
||||
/// Defaults to `0`
|
||||
pub fn min_idle(mut self, min_idle: u32) -> Self {
|
||||
self.min_idle = min_idle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of pooled connections
|
||||
///
|
||||
/// Defaults to `10`
|
||||
pub fn max_size(mut self, max_size: u32) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection timeout
|
||||
///
|
||||
/// Defaults to `30 seconds`
|
||||
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
|
||||
self.connection_timeout = connection_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection idle timeout
|
||||
///
|
||||
/// Defaults to `60 seconds`
|
||||
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
|
||||
self.idle_timeout = idle_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build<C: ManageConnection<Connection = SmtpConnection, Error = Error>>(
|
||||
&self,
|
||||
client: C,
|
||||
) -> Pool<C> {
|
||||
Pool::builder()
|
||||
.min_idle(Some(self.min_idle))
|
||||
.max_size(self.max_size)
|
||||
.connection_timeout(self.connection_timeout)
|
||||
.idle_timeout(Some(self.idle_timeout))
|
||||
.connection_customizer(Box::new(SmtpConnectionQuitter))
|
||||
.build_unchecked(client)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_idle: 0,
|
||||
max_size: 10,
|
||||
connection_timeout: Duration::from_secs(30),
|
||||
idle_timeout: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageConnection for SmtpClient {
|
||||
type Connection = SmtpConnection;
|
||||
@@ -13,10 +90,22 @@ impl ManageConnection for SmtpClient {
|
||||
if conn.test_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::Client("is not connected anymore"))
|
||||
Err(error::network("is not connected anymore"))
|
||||
}
|
||||
|
||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||
conn.has_broken()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct SmtpConnectionQuitter;
|
||||
|
||||
impl CustomizeConnection<SmtpConnection, Error> for SmtpConnectionQuitter {
|
||||
fn on_release(&self, conn: SmtpConnection) {
|
||||
let mut conn = conn;
|
||||
if !conn.has_broken() {
|
||||
let _quit = conn.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text
|
||||
//! message
|
||||
|
||||
use crate::transport::smtp::Error;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::streaming::{tag, take_until},
|
||||
@@ -32,7 +32,7 @@ pub enum Severity {
|
||||
}
|
||||
|
||||
impl Display for Severity {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}", *self as u8)
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ pub enum Category {
|
||||
}
|
||||
|
||||
impl Display for Category {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}", *self as u8)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ pub enum Detail {
|
||||
}
|
||||
|
||||
impl Display for Detail {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}", *self as u8)
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ pub struct Code {
|
||||
}
|
||||
|
||||
impl Display for Code {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}{}{}", self.severity, self.category, self.detail)
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,14 @@ impl Code {
|
||||
detail,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells if the response is positive
|
||||
pub fn is_positive(self) -> bool {
|
||||
matches!(
|
||||
self.severity,
|
||||
Severity::PositiveCompletion | Severity::PositiveIntermediate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an SMTP reply, with separated code and message
|
||||
@@ -139,7 +147,9 @@ impl FromStr for Response {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> result::Result<Response, Error> {
|
||||
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
|
||||
parse_response(s)
|
||||
.map(|(_, r)| r)
|
||||
.map_err(|e| error::response(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +161,7 @@ impl Response {
|
||||
|
||||
/// Tells if the response is positive
|
||||
pub fn is_positive(&self) -> bool {
|
||||
match self.code.severity {
|
||||
Severity::PositiveCompletion | Severity::PositiveIntermediate => true,
|
||||
_ => false,
|
||||
}
|
||||
self.code.is_positive()
|
||||
}
|
||||
|
||||
/// Tests code equality
|
||||
@@ -238,7 +245,10 @@ pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
|
||||
|
||||
// Check that all codes are equal.
|
||||
if !lines.iter().all(|&(code, _, _)| code == last_code) {
|
||||
return Err(nom::Err::Failure(("", nom::error::ErrorKind::Not)));
|
||||
return Err(nom::Err::Failure(nom::error::Error::new(
|
||||
"",
|
||||
nom::error::ErrorKind::Not,
|
||||
)));
|
||||
}
|
||||
|
||||
// Extract text from lines, and append last line.
|
||||
|
||||
@@ -3,12 +3,15 @@ use std::time::Duration;
|
||||
#[cfg(feature = "r2d2")]
|
||||
use r2d2::Pool;
|
||||
|
||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||
#[cfg(feature = "r2d2")]
|
||||
use super::PoolConfig;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use crate::{Envelope, Transport};
|
||||
use super::{error, Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||
use crate::{address::Envelope, Transport};
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
/// Sends emails using the SMTP protocol
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
@@ -24,7 +27,7 @@ impl Transport for SmtpTransport {
|
||||
/// Sends an email
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
#[cfg(feature = "r2d2")]
|
||||
let mut conn = self.inner.get()?;
|
||||
let mut conn = self.inner.get().map_err(error::client)?;
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
@@ -38,13 +41,14 @@ impl Transport for SmtpTransport {
|
||||
}
|
||||
|
||||
impl SmtpTransport {
|
||||
/// Simple and secure transport, using TLS connections to comunicate with the SMTP server
|
||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||
///
|
||||
/// The right option for most SMTP servers.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
@@ -65,6 +69,7 @@ impl SmtpTransport {
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
@@ -93,17 +98,26 @@ impl SmtpTransport {
|
||||
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
|
||||
let mut new = SmtpInfo::default();
|
||||
new.server = server.into();
|
||||
SmtpTransportBuilder { info: new }
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
SmtpTransportBuilder {
|
||||
info: new,
|
||||
#[cfg(feature = "r2d2")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`SmtpTransport`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
#[cfg(feature = "r2d2")]
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
@@ -140,32 +154,31 @@ impl SmtpTransportBuilder {
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the client
|
||||
fn build_client(self) -> SmtpClient {
|
||||
SmtpClient { info: self.info }
|
||||
/// Use a custom configuration for the connection pool
|
||||
///
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
|
||||
self.pool_config = pool_config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport
|
||||
///
|
||||
/// If the `r2d2` feature is enabled an `Arc` wrapped pool is be created.
|
||||
/// Defaults:
|
||||
///
|
||||
/// * 60 seconds idle timeout
|
||||
/// * 30 minutes max connection lifetime
|
||||
/// * max pool size of 10 connections
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
let client = self.build_client();
|
||||
let client = SmtpClient { info: self.info };
|
||||
SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: Pool::builder()
|
||||
.min_idle(Some(0))
|
||||
.idle_timeout(Some(Duration::from_secs(60)))
|
||||
.build_unchecked(client),
|
||||
inner: self.pool_config.build(client),
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
inner: client,
|
||||
}
|
||||
@@ -173,7 +186,7 @@ impl SmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Build client
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpClient {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
@@ -190,6 +203,7 @@ impl SmtpClient {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut conn = SmtpConnection::connect::<(&str, u16)>(
|
||||
(self.info.server.as_ref(), self.info.port),
|
||||
self.info.timeout,
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
pub struct XText<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Display for XText<'a> {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut rest = self.0;
|
||||
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
|
||||
let (start, end) = rest.split_at(idx);
|
||||
|
||||
@@ -7,27 +7,31 @@
|
||||
//! testing purposes.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::{Message, Envelope, Transport, StubTransport};
|
||||
//! # #[cfg(feature = "builder")]
|
||||
//! # {
|
||||
//! use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
//! .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let mut sender = StubTransport::new_ok();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Transport;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Transport;
|
||||
use crate::{Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use async_trait::async_trait;
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
@@ -49,7 +53,7 @@ pub struct StubTransport {
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
/// Creates aResult new transport that always returns the given response
|
||||
/// Creates a new transport that always returns the given Result
|
||||
pub fn new(response: Result<(), Error>) -> StubTransport {
|
||||
StubTransport { response }
|
||||
}
|
||||
@@ -76,20 +80,9 @@ impl Transport for StubTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for StubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for StubTransport {
|
||||
impl AsyncTransport for StubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
|
||||
234
testdata/email_with_png.eml
vendored
Normal file
234
testdata/email_with_png.eml
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
Date: Tue, 15 Nov 1994 08:12:31 -0000
|
||||
From: NoBody <nobody@domain.tld>
|
||||
Reply-To: Yuin <yuin@domain.tld>
|
||||
To: Hei <hei@domain.tld>
|
||||
Subject: Happy new year
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/related;
|
||||
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
|
||||
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
Content-Type: image/png
|
||||
Content-Disposition: inline
|
||||
Content-ID: <123>
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAACXBIWXMAASdGAAEnRgHWSSfaAAAA
|
||||
GXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJzs3WmMlfXd//HvzLAO
|
||||
izAORCmKgKmKKQa1taUWl1C1cWm1rVvdSls01bbGpcsDTWOrMcabdNNiXdqkrSamMbY2hkTbOiyD
|
||||
0HFBiRSLE6WFKgoFFIRBztwP7M2//gEVnJnf95zzeiUmLjDziYlnztvrd12noaOjozsAAACAohpL
|
||||
DwAAAAAEOgAAAKQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ9Cs9ADL6wx/+EHfffXfpGQB7ZNiw
|
||||
YfGb3/ym9AwAYC8JdNiFN954I1atWlV6BsAeGT58eOkJAMAH4Ig7AAAAJCDQAQAAIAGBDgAAAAkI
|
||||
dAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhA
|
||||
oAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEAC
|
||||
Ah0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAAS
|
||||
EOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQ
|
||||
gEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACA
|
||||
BAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAA
|
||||
JCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAA
|
||||
IAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAA
|
||||
AAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAA
|
||||
AEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMA
|
||||
AEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0A
|
||||
AAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgA
|
||||
AACQgEAHAACABAQ6AAAAJCDQAQAAIIF+pQdARieeeGIcfPDBpWdQJ55//vn40Y9+VHoGAACFCXTY
|
||||
hf322y/222+/0jOoE01NTaUnAACQgCPuAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADFNbY6KUYAACBDlBcU1NT6QkAACQg0AEK69evX+kJAAAkINABCnMFHQCACIEOUJxABwAgQqAD
|
||||
FOeIOwAAEQIdoDhPcaenbN++vfQEAOAD8K4QoDBX0Okpb731VukJAMAHINABChPo9BSBDgDVTaAD
|
||||
FOYhcfSU7du3R3d3d+kZAMBeEugAhQl0epL70AGgegl0gML69+9fegI1pKurq/QEAGAvCXSAwgYP
|
||||
HhwNDQ2lZ1AjXn/99dITAIC9JNABCmtsbIyBAweWnkGNEOgAUL0EOkACzc3NpSdQIwQ6AFQvgQ6Q
|
||||
gECnpwh0AKheAh0ggSFDhpSeQI144403Sk8AAPaSQAdIwBV0esprr71WegIAsJcEOkACAp2esmbN
|
||||
mtITAIC9JNABEnDEnZ4i0AGgegl0gAQGDx5cegI1QqADQPUS6AAJuIJOTxHoAFC9BDpAAgKdnvLa
|
||||
a6/F1q1bS88AAPaCQAdIYPjw4aUnUCMqlUqsXLmy9AwAYC8IdIAEWlpaSk+ghrz44oulJwAAe0Gg
|
||||
AyQwcuTI0hOoIQIdAKqTQAdIQKDTkwQ6AFQngQ6QgCPu9KTly5eXngAA7AWBDpDAyJEjo7HRSzI9
|
||||
46WXXoo33nij9AwAYA95NwiQQGNjoye502O6u7tdRQeAKiTQAZJwzJ2e9Nxzz5WeAADsIYEOkMSI
|
||||
ESNKT6CGLF26tPQEAGAPCXSAJFxBpyd1dHREpVIpPQMA2AMCHSAJgU5P2rBhQ6xYsaL0DABgDwh0
|
||||
gCRaW1tLT6DGdHR0lJ4AAOwBgQ6QxJgxY0pPoMYsXry49AQAYA8IdIAk9t9//9ITqDEdHR3x5ptv
|
||||
lp4BALxPAh0gCYFOT9uyZUssWrSo9AwA4H0S6ABJtLa2Rv/+/UvPoMb85S9/KT0BAHifBDpAEo2N
|
||||
jTF69OjSM6gxc+fOjW3btpWeAQC8DwIdIBEPiqOnvf766x4WBwBVQqADJOI+dHrDQw89VHoCAPA+
|
||||
CHSARPbbb7/SE6hBbW1tsX79+tIzAID3INABEnEFnd6wbdu2mDNnTukZAMB7EOgAibgHnd7ywAMP
|
||||
RHd3d+kZAMC7EOgAiRx44IGlJ1CjOjs7Y+HChaVnAADvQqADJDJq1KgYOnRo6RnUqHvvvbf0BADg
|
||||
XQh0gGQOOuig0hOoUY8//ng8//zzpWcAALsh0AGSmTBhQukJ1LBf/epXpScAALsh0AGScQWd3vTI
|
||||
I4+4ig4ASQl0gGTGjx9fegI1rLu7O2bPnl16BgCwCwIdIBlH3Oltc+fOjWeffbb0DADg/yPQAZLZ
|
||||
f//9Y+DAgaVnUONuueWWqFQqpWcAAP9FoAMk09jYGOPGjSs9gxq3bNmyePDBB0vPAAD+i0AHSMiD
|
||||
4ugLP/vZz2L9+vWlZwAA/yHQARI6+OCDS0+gDmzcuDFmzZpVegYA8B8CHSChQw89tPQE6sTDDz8c
|
||||
jz76aOkZAEAIdICUJk2aVHoCdeTmm2+OtWvXlp4BAHVPoAMkNGLEiBgzZkzpGdSJ9evXxw033OCp
|
||||
7gBQmEAHSOqwww4rPYE6smDBgrjnnntKzwCAuibQAZIS6PS1X/ziF9He3l56BgDULYEOkJT70Olr
|
||||
lUolrrvuuli5cmXpKQBQlwQ6QFKTJk2KhoaG0jOoMxs2bIgrrrgi1q1bV3oKANQdgQ6Q1NChQ+OA
|
||||
Aw4oPYM6tHr16rjyyivjzTffLD0FAOqKQAdIzOehU8pzzz0X3/72t6Orq6v0FACoGwIdILHDDz+8
|
||||
9ATq2MKFC+Oaa64R6QDQRwQ6QGJTpkwpPYE6197eHtdee61IB4A+INABEjvkkENiyJAhpWdQ5xYs
|
||||
WBCXXnpprF+/vvQUAKhpAh0gsaampjjiiCNKz4B49tlnY+bMmfHyyy+XngIANUugAyR31FFHlZ4A
|
||||
ERHR2dkZM2bMiKVLl5aeAgA1SaADJHfkkUeWngA7rFmzJmbOnBkPPPBA6Skk0dXV5RkFAD1EoAMk
|
||||
d9hhh8XgwYNLz4Adurq64qabboof/OAHPiu9jj333HNxyy23xCmnnBJz584tPQegJgh0gOT69esX
|
||||
kydPLj0DdvL73/8+zjnnnFiyZEnpKfSRtWvXxn333Rfnn39+XHTRRXH//ffHxo0bBTpAD+lXegAA
|
||||
7+2oo46KRYsWlZ4BO1m9enVceuml8ZWvfCUuueSS6N+/f+lJ9LCurq6YP39+PPTQQ9He3h7bt2/f
|
||||
6dfMnz8/tm/fHk1NTQUWAtQOgQ5QBdyHTmZvvfVW3HHHHfHII4/E9773vZgyZUrpSfSAZcuWxR//
|
||||
+MeYM2dObNiw4V1/7caNG2PJkiVeqwA+IIEOUAUmTZoUgwYNii1btpSeArvV2dkZM2fOjNNPPz0u
|
||||
v/zy2HfffUtPYg8tW7Ys/vznP8ef/vSnWLly5R793ra2NoEO8AE1dHR0dJceAcB7u+qqq9znSdUY
|
||||
PHhwnH322TFjxowYMmRI6Tm8i87Oznj00Udjzpw5exzl/23s2LHx4IMP9uAygPoj0AGqxAMPPBA3
|
||||
3XRT6RmwR1pbW+Piiy+OM888MwYNGlR6DhFRqVTimWee2XGl/JVXXumxr33//ffHhAkTeuzrAdQb
|
||||
gQ5QJdasWROnnnpqdHd72ab6tLS0xAUXXBBf+MIXorm5ufScurN58+b461//GgsXLoy2trZ49dVX
|
||||
e+X7XHHFFXHJJZf0ytcGqAcCHaCKnH/++fH888+XngF7rbm5OU455ZQ499xzXWntRZVKJZYvXx6L
|
||||
Fy+ORYsWxVNPPRXbtm3r9e87efLkuOeee3r9+wDUKoEOUEVuv/12b36pCQ0NDXHMMcfE6aefHscf
|
||||
f3wMHDiw9KSq9+qrr8bjjz8eCxcujMWLF8f69ev7fENjY2PMmTMnWlpa+vx7A9QCgQ5QRZ555pmY
|
||||
MWNG6RnQo4YOHRrTp0+Pk08+OY466qhobGwsPSm97u7ueOmll+LZZ5+NZ555JpYsWRKdnZ2lZ0VE
|
||||
xHXXXRef/exnS88AqEoCHaCKVCqVOOmkk4pcGYO+MGLEiJg2bVqccMIJcfTRR8fgwYNLT0rhzTff
|
||||
jOeeey6WLFmyI8rf67PJS5k2bVrMmjWr9AyAqiTQAarM9ddfHw8//HDpGdDrBgwYEJMnT46Pf/zj
|
||||
cfTRR8ehhx4a/fr1Kz2r161bty5WrFgRnZ2d8cILL8SyZcvi73//e2zfvr30tPdl0KBB8eijj3pq
|
||||
P8BeqP2fcgA15thjjxXo1IWurq7o6OiIjo6OiHg7/CZNmhRHHHFEHHLIITFx4sQ44IADqjbaN27c
|
||||
GC+++GKsWLEiXnjhhejs7IwVK1bEv//979LTPpAtW7bE4sWLY9q0aaWnAFSd6vyJBlDHpk6dGgMG
|
||||
DIiurq7SU6BPbdmyJZ588sl48sknd/y9/v37x7hx42LChAkxceLEGD9+fIwfPz5Gjx4dQ4YMKba1
|
||||
UqnE2rVr41//+le8/PLL8fLLL+/489WrV8fLL78cmzZtKravt7W1tQl0gL0g0AGqzNChQ2Pq1Knx
|
||||
2GOPlZ4CxW3bti1WrFgRK1as2OmfDRw4MFpbW6O1tTX23XffaG1tjZaWlmhpaYmmpqYYMmRINDQ0
|
||||
xLBhwyLi7f+2Ghoa3vG1t2zZEhERW7duja1bt0bE2/eDb9q0KTZu3Ljjjw0bNsTrr78eGzZs2PHX
|
||||
lUqlD/4N5DRv3ryoVCoe+AewhwQ6QBU66aSTBDq8h61bt8aqVati1apVpafUnXXr1sXSpUtj8uTJ
|
||||
pacAVBX/WxOgCk2bNi2am5tLzwDYrXnz5pWeAFB1BDpAFRo0aFAce+yxpWcA7FZbW1vpCQBVR6AD
|
||||
VKmTTjqp9ASA3ers7IyVK1eWngFQVQQ6QJWaOnXqjodbAWTkmDvAnhHoAFVqwIABcfzxx5eeAbBb
|
||||
c+fOLT0BoKoIdIAq9ulPf7r0BIDdeuqpp2LDhg2lZwBUDYEOUMU+9rGPRUtLS+kZALtUqVRiwYIF
|
||||
pWcAVA2BDlDF+vXrF6eddlrpGQC75Zg7wPsn0AGq3FlnnRUNDQ2lZwDsUnt7e3R1dZWeAVAVBDpA
|
||||
lRs7dmwcddRRpWcA7NLmzZvjiSeeKD0DoCoIdIAacOaZZ5aeALBbPm4N4P0R6AA14MQTT4yRI0eW
|
||||
ngGwS4899lh0d3eXngGQnkAHqAH9+/ePU089tfQMgF1as2ZNLF++vPQMgPQEOkCNOPPMMz0sDkjL
|
||||
09wB3ptAB6gR48aNiylTppSeAbBLAh3gvQl0gBry+c9/vvQEgF1avnx5vPLKK6VnAKQm0AFqyPTp
|
||||
02P//fcvPQNgJ93d3a6iA7wHgQ5QQ5qamuK8884rPQNglwQ6wLsT6AA15nOf+1wMGzas9AyAnXR0
|
||||
dMSmTZtKzwBIS6AD1Jjm5uY466yzSs8A2Mm2bdti4cKFpWcApCXQAWrQl770pRgwYEDpGQA7ccwd
|
||||
YPcEOkANamlpiVNOOaX0DICdzJ8/P7Zv3156BkBKAh2gRl100UXR2OhlHshl48aN8fTTT5eeAZCS
|
||||
d24ANeqggw6KT3ziE6VnAOzEMXeAXRPoADVsxowZpScA7KStra30BICUBDpADTviiCNi6tSppWcA
|
||||
vMM///nP6OzsLD0DIB2BDlDjvv71r0dDQ0PpGQDv4Co6wM4EOkCNO/TQQ+OEE04oPQPgHebNm1d6
|
||||
AkA6Ah2gDlx++eXR1NRUegbADkuXLo21a9eWngGQikAHqAPjxo2Lk08+ufQMgB0qlUrMnz+/9AyA
|
||||
VAQ6QJ247LLLon///qVnAOzgPnSAdxLoAHVizJgxcfrpp5eeAbDDokWLYsuWLaVnAKQh0AHqyFe/
|
||||
+tUYPHhw6RkAERGxdevWWLx4cekZAGkIdIA6Mnr06Pjyl79cegbADo65A/w/Ah2gzlx44YVx4IEH
|
||||
lp4BEBFvf9xapVIpPQMgBYEOUGf69+8f3/rWt0rPAIiIiHXr1sXSpUtLzwBIQaAD1KHjjjsupk6d
|
||||
WnoGQES8fRUdAIEOULeuvfbaGDBgQOkZAO5DB/gPgQ5Qpw444IA455xzSs8AiM7Ozli5cmXpGQDF
|
||||
CXSAOva1r30tRo0aVXoGgGPuACHQAepac3NzfPOb3yw9AyDmzp1begJAcQIdoM595jOfiRNOOKH0
|
||||
DKDOPfXUU7Fhw4bSMwCKEugAxHe+850YPnwtwJqJAAAO10lEQVR46RlAHatUKrFgwYLSMwCKEugA
|
||||
RGtra1x11VWlZwB1zjF3oN4JdAAiIuK0006L4447rvQMoI61t7dHV1dX6RkAxQh0AHb47ne/66g7
|
||||
UMzmzZvjiSeeKD0DoBiBDsAOo0aNiiuvvLL0DKCOOeYO1DOBDsA7nHHGGTF16tTSM4A61dbWFt3d
|
||||
3aVnABQh0AHYyfe///3Yd999S88A6tCaNWti+fLlpWcAFCHQAdhJS0tL/PCHP4zGRj8mgL7nmDtQ
|
||||
r7zzAmCXPvrRj8bFF19cegZQhwQ6UK8EOgC7ddlll8WUKVNKzwDqzN/+9rdYvXp16RkAfU6gA7Bb
|
||||
TU1NceONN8aIESNKTwHqzPz580tPAOhzAh2AdzV69Oi44YYboqGhofQUoI7Mmzev9ASAPifQAXhP
|
||||
U6dOjQsuuKD0DKCOdHR0xKZNm0rPAOhTAh2A9+Xyyy+PI488svQMoE5s27YtFi5cWHoGQJ8S6AC8
|
||||
L/369Ytbb701DjzwwNJTgDrhae5AvRHoALxvw4cPj1mzZsWwYcNKTwHqwPz582P79u2lZwD0GYEO
|
||||
wB456KCD4uabb46mpqbSU4Aat3Hjxnj66adLzwDoMwIdgD12zDHHxNVXX116BlAH2traSk8A6DMC
|
||||
HYC9cvbZZ8cXv/jF0jOAGifQgXoi0AHYa9dee2188pOfLD0DqGGrVq2Kzs7O0jMA+oRAB2CvNTY2
|
||||
xo033hgf/vCHS08Bapir6EC9EOgAfCBDhw6N2267LcaPH196ClCj5s2bV3oCQJ8Q6AB8YCNHjozb
|
||||
brstxowZU3oKUIOWLl0aa9euLT0DoNcJdAB6xOjRo+P222+P1tbW0lOAGlOpVFxFB+qCQAegx4wd
|
||||
OzZuu+222GeffUpPAWrM3LlzS08A6HUCHYAeNXHixPjJT34Szc3NpacANWTRokWxZcuW0jMAepVA
|
||||
B6DHHX744TFr1qwYNGhQ6SlAjdi6dWssXry49AyAXiXQAegVRx99dPz0pz+NIUOGlJ4C1AgftwbU
|
||||
OoEOQK+ZMmVKzJ49O0aMGFF6ClAD5s2bF5VKpfQMgF4j0AHoVYcddljceeedMXr06NJTgCq3bt26
|
||||
WLp0aekZAL1GoAPQ68aPHx933XVXjB07tvQUoMr5uDWglgl0APrEmDFj4q677oqJEyeWngJUMfeh
|
||||
A7VMoAPQZ1pbW+PnP/95HHLIIaWnAFWqs7Mz/vGPf5SeAdArBDoAfaqlpSXuvPPOOO6440pPAarU
|
||||
3LlzS08A6BUCHYA+19zcHLfeemvMnDmz9BSgCgl0oFYJdACKaGhoiJkzZ8b1118f/fv3Lz0HqCJP
|
||||
PfVUrF+/vvQMgB4n0AEo6owzzojZs2fHyJEjS08BqkSlUon29vbSMwB6nEAHoLgjjjgifvnLX8b4
|
||||
8eNLTwGqhGPuQC0S6ACkMHbs2Ljnnnti6tSppacAVaC9vT26urpKzwDoUQIdgDSGDRsWP/7xj+Pq
|
||||
q6+Ofv36lZ4DJLZ58+Z44oknSs8A6FECHYBUGhoa4rzzzou77747xowZU3oOkJhj7kCtEegApHT4
|
||||
4YfHr3/96/jUpz5VegqQVFtbW3R3d5eeAdBjBDoAae2zzz4xa9asuPrqq30UG7CTNWvWxPLly0vP
|
||||
AOgxAh2A1P77yPuHPvSh0nOAZBxzB2qJQAegKkyaNCnuvffeOPfcc6Ox0Y8v4G1tbW2lJwD0GO9w
|
||||
AKgaQ4YMiWuuuSbuuOOOOPDAA0vPARJYvnx5rF69uvQMgB4h0AGoOlOmTIn77rsvLr74YlfTgZg/
|
||||
f37pCQA9wrsaAKrSwIED4xvf+EbcddddcdBBB5WeAxQ0b9680hMAeoRAB6CqTZ48OX7729/GhRde
|
||||
6Go61KmOjo7YvHlz6RkAH1hDR0eHD48EoCa8+OKLMWvWrGhvby89BegDEyZMiOnTp8f06dNjwoQJ
|
||||
pecAfGACHYCaM3fu3Pif//mfWLVqVekpQA9qbGyMj3zkIzFt2rQ44YQTPCwSqDkCHYCatG3btvjd
|
||||
734Xs2fPjk2bNpWeA+yl/4vy/7tSPmrUqNKTAHqNQAegpr322mvxi1/8b3t391J1usZx+M6VQrqW
|
||||
tbSyLEgtCUtIijRNI6K/sKPaMOcdVVBBUVTUQVRSIpLlQfRGlNILaEYWug82IxMje2aY6nfbui74
|
||||
4WKh+D3TDz74/CfOnTsXi4uLRc8B/oaGhoYYGBiIkZGROHr0aLS0tBQ9CeCnEOgA1ISJiYk4ceJE
|
||||
3L9/v+gpwAqamppieHg4jh07FkNDQ7Fu3bqiJwH8dAIdgJoyNjYWp06dinv37hU9BWpec3NzDA8P
|
||||
x/Hjx+PQoUPR0NBQ9CSAQgl0AGrS2NhYnDx5MkZHR4ueAjVl27ZtMTIyEkeOHIn9+/fH2rVri54E
|
||||
kIZAB6CmCXX4serq6mL37t0xMjLiOjSAvyDQASAibt++Hb/99luMjY0VPQVWvXK5HENDQ3HkyJEY
|
||||
GhqK5ubmoicBrAoCHQD+4NGjR3HmzJm4cOFCLCwsFD0HVo329vY4dOhQDA8Px+DgYNTX1xc9CWDV
|
||||
EegAsIJ3797F+fPn4/Tp0zEzM1P0HEjnj0fXR0ZGoqenp+hJAKueQAeA/2NhYSEuXboUp0+fjqmp
|
||||
qaLnQKE2bNgQ/f39MTQ0FIcPH45qtVr0JIBfikAHgL9pdHQ0zpw5Ezdu3HD8nZpQKpWit7c3BgcH
|
||||
Y3BwMHp6eqKurq7oWQC/LIEOAP/Q7OxsXLlyJS5evBjj4+OxtORHKb+O9vb2GBgYiMHBwTh48GBU
|
||||
KpWiJwHUDIEOAP/C69ev4/Lly3H27Nl4+fJl0XPgH6tWq3HgwIHo7++Pvr4+16ABFEigA8B3sLi4
|
||||
GA8ePIgLFy7E9evXY25uruhJsKJyuRz79++PgwcPRn9/f3R1dcWaNWuKngVACHQA+O4WFxdjfHw8
|
||||
rl69GteuXYvp6emiJ1HDWlpaYu/evdHX1xf79u2L3t7eWLt2bdGzAFiBQAeAH+zJkydx9erVuHXr
|
||||
VkxOThY9h1/cxo0bo6+vb/nIemdnp7+QA6wSAh0AfqLnz5/HjRs34ubNmzExMRFfv34tehKrWKVS
|
||||
ib1790Zvb+/yR1efAaxeAh0ACjI/Px9jY2MxOjoao6Oj8ejRo1hcXCx6FknV19fHrl27vonxHTt2
|
||||
+Os4wC9EoANAEvPz8zExMRF37tyJu3fvxuPHjwV7jSqXy7Fz587o6emJrq6u6Orqij179kRDQ0PR
|
||||
0wD4gQQ6ACT1/v37GB8fj4cPH8bk5GQ8fPgwPnz4UPQsvqOGhobo6OiIjo6O2LlzZ3R3d0d3d3ds
|
||||
3bq16GkAFECgA8Aq8urVq+VYn5ycjMnJyZidnS16Fn+hUqnEjh07oqurKzo6OqKzszM6Ozujvb09
|
||||
6urqip4HQBICHQBWsaWlpXjx4kVMTU3F8+fP4+nTp/H06dN49uxZfPz4seh5NaNUKkVbW1ts3749
|
||||
tm3b9s2zffv2aG5uLnoiAKuAQAeAX9TMzEw8e/Zs+fk93Kenp+PLly9Fz1s1SqVStLa2xpYtW2Lj
|
||||
xo3R1tYWmzdvjk2bNi2/bmtrc7c4AP+aQAeAGvT27duYmZmJ6enpmJ6ejpmZmXjz5s03r+fn54ue
|
||||
+UPU19dHpVKJDRs2LD/VajWq1WqsX79++b3W1taoVqvR2trqGDoAP4VABwBWND8/H7Ozs988c3Nz
|
||||
MTc3t+L7S0tLMTc3t/z1s7OzsbT0v18zPn36tHzn++fPn+PLly9RLpdX/L7lcvlPV4f9fkS8Uqks
|
||||
f05dXV00NTVFqVSKxsbGKJVK0dTU9KenUqlEuVyOpqamKJfL/hM6AGk5iwUArKixsTEaGxujra2t
|
||||
6CkAUBOc1wIAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCA
|
||||
QAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAE
|
||||
BDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAk
|
||||
INABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAg
|
||||
AYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAA
|
||||
CQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAA
|
||||
SECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAA
|
||||
QAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAA
|
||||
ABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAA
|
||||
AJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcA
|
||||
AIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoA
|
||||
AAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINAB
|
||||
AAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEO
|
||||
AAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0
|
||||
AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECg
|
||||
AwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAIC
|
||||
HQAAABIQ6AAAAJDAfwHNjj3TR6+CggAAAABJRU5ErkJggg==
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
|
||||
@@ -1,98 +1,198 @@
|
||||
#[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
fn default_date() -> std::time::SystemTime {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
mod test {
|
||||
use lettre::{transport::file::FileTransport, Message};
|
||||
#[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
mod sync {
|
||||
use crate::default_date;
|
||||
use lettre::{FileTransport, Message, Transport};
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{remove_file, File},
|
||||
io::Read,
|
||||
fs::{read_to_string, remove_file},
|
||||
};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
use lettre::Transport;
|
||||
let sender = FileTransport::new(temp_dir());
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(&email);
|
||||
let id = result.unwrap();
|
||||
|
||||
let file = temp_dir().join(format!("{}.json", id));
|
||||
let mut f = File::open(file.clone()).unwrap();
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer,
|
||||
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
|
||||
remove_file(file).unwrap();
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_attributes::test]
|
||||
async fn file_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
|
||||
let sender = FileTransport::new(temp_dir());
|
||||
#[test]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
fn file_transport_with_envelope() {
|
||||
let sender = FileTransport::with_envelope(temp_dir());
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let result = sender.send(&email);
|
||||
let id = result.unwrap();
|
||||
|
||||
let file = temp_dir().join(format!("{}.json", id));
|
||||
let mut f = File::open(file.clone()).unwrap();
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
let json_file = temp_dir().join(format!("{}.json", id));
|
||||
let json = read_to_string(&json_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer,
|
||||
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
|
||||
remove_file(file).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn file_transport_tokio02() {
|
||||
use lettre::Tokio02Transport;
|
||||
|
||||
let sender = FileTransport::new(temp_dir());
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let id = result.unwrap();
|
||||
|
||||
let file = temp_dir().join(format!("{}.json", id));
|
||||
let mut f = File::open(file.clone()).unwrap();
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
buffer,
|
||||
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
|
||||
remove_file(file).unwrap();
|
||||
json,
|
||||
"{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"}"
|
||||
);
|
||||
|
||||
let (e, m) = sender.read(&id).unwrap();
|
||||
|
||||
assert_eq!(&e, email.envelope());
|
||||
assert_eq!(m, email.formatted());
|
||||
|
||||
remove_file(eml_file).unwrap();
|
||||
remove_file(json_file).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
use crate::default_date;
|
||||
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{read_to_string, remove_file},
|
||||
};
|
||||
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_transport_tokio1() {
|
||||
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let id = result.unwrap();
|
||||
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "file-transport",
|
||||
feature = "builder",
|
||||
feature = "async-std1"
|
||||
))]
|
||||
mod asyncstd_1 {
|
||||
use crate::default_date;
|
||||
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{read_to_string, remove_file},
|
||||
};
|
||||
|
||||
#[async_std::test]
|
||||
async fn file_transport_asyncstd1() {
|
||||
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let id = result.unwrap();
|
||||
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,74 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
mod test {
|
||||
use lettre::{transport::sendmail::SendmailTransport, Message};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
#[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
mod sync {
|
||||
use lettre::{Message, SendmailTransport, Transport};
|
||||
|
||||
#[test]
|
||||
fn sendmail_transport() {
|
||||
use lettre::Transport;
|
||||
let sender = SendmailTransport::new();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(&email);
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_attributes::test]
|
||||
async fn sendmail_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
feature = "builder",
|
||||
feature = "tokio1"
|
||||
))]
|
||||
mod tokio_1 {
|
||||
use lettre::{AsyncSendmailTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
let sender = SendmailTransport::new();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn sendmail_transport_tokio02() {
|
||||
use lettre::Tokio02Transport;
|
||||
|
||||
let sender = SendmailTransport::new();
|
||||
async fn sendmail_transport_tokio1() {
|
||||
let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
feature = "builder",
|
||||
feature = "async-std1"
|
||||
))]
|
||||
mod asyncstd_1 {
|
||||
use lettre::{AsyncSendmailTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
|
||||
#[async_std::test]
|
||||
async fn sendmail_transport_asyncstd1() {
|
||||
let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
mod test {
|
||||
#[cfg(all(feature = "smtp-transport", feature = "builder"))]
|
||||
mod sync {
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
#[test]
|
||||
@@ -10,12 +10,64 @@ mod test {
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build()
|
||||
.send(&email)
|
||||
.unwrap();
|
||||
.build();
|
||||
sender.send(&email).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn smtp_transport_simple_tokio1() {
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let sender: AsyncSmtpTransport<Tokio1Executor> =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
sender.send(email).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
feature = "builder",
|
||||
feature = "async-std1"
|
||||
))]
|
||||
mod asyncstd_1 {
|
||||
use lettre::{AsyncSmtpTransport, AsyncStd1Executor, AsyncTransport, Message};
|
||||
|
||||
#[async_std::test]
|
||||
async fn smtp_transport_simple_asyncstd1() {
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let sender: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
AsyncSmtpTransport::<AsyncStd1Executor>::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
sender.send(email).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
|
||||
mod test {
|
||||
use lettre::{Envelope, SmtpTransport, Transport};
|
||||
mod sync {
|
||||
use lettre::{address::Envelope, SmtpTransport, Transport};
|
||||
use std::{sync::mpsc, thread};
|
||||
|
||||
fn envelope() -> Envelope {
|
||||
|
||||
@@ -1,61 +1,67 @@
|
||||
use lettre::{transport::stub::StubTransport, Message};
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "builder")]
|
||||
mod sync {
|
||||
use lettre::{transport::stub::StubTransport, Message, Transport};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
use lettre::Transport;
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(&email).unwrap();
|
||||
sender_ko.send(&email).unwrap_err();
|
||||
sender_ok.send(&email).unwrap();
|
||||
sender_ko.send(&email).unwrap_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_attributes::test]
|
||||
async fn stub_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "builder", feature = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
|
||||
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
#[tokio::test]
|
||||
async fn stub_transport_tokio1() {
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn stub_transport_tokio02() {
|
||||
use lettre::Tokio02Transport;
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "builder", feature = "async-std1"))]
|
||||
mod asyncstd_1 {
|
||||
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
|
||||
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
#[async_std::test]
|
||||
async fn stub_transport_asyncstd1() {
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user