Compare commits
216 Commits
v0.9.x
...
v0.10.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68bceaa9b5 | ||
|
|
31a3be1cba | ||
|
|
47dfdf7ee8 | ||
|
|
47c4077b14 | ||
|
|
8869c7fdb4 | ||
|
|
3cf89935af | ||
|
|
ce957ee346 | ||
|
|
f87c80e05c | ||
|
|
1c4a3f0fb3 | ||
|
|
393d414700 | ||
|
|
a83f927109 | ||
|
|
c59f67d808 | ||
|
|
087e1e9c31 | ||
|
|
69d48c4be7 | ||
|
|
04b42879b0 | ||
|
|
d5c1ab8dd1 | ||
|
|
42a34175ac | ||
|
|
542ea4ffd2 | ||
|
|
41d68616e0 | ||
|
|
36aab20086 | ||
|
|
98f09117f7 | ||
|
|
c0ef9a38a1 | ||
|
|
6b6f130070 | ||
|
|
694a6d2852 | ||
|
|
f865fc1bce | ||
|
|
60e3a0b7cb | ||
|
|
c8ec8984b8 | ||
|
|
1b45c6dd58 | ||
|
|
470b8c3ca7 | ||
|
|
c8d73dd940 | ||
|
|
bcbdbecd95 | ||
|
|
d75fb5956b | ||
|
|
211aa389d7 | ||
|
|
49787e0c41 | ||
|
|
3e62efb46a | ||
|
|
6c440bda73 | ||
|
|
cedfd8bfbb | ||
|
|
889ef0ba6a | ||
|
|
e7f07c5ce8 | ||
|
|
4b238829c7 | ||
|
|
fbbd015109 | ||
|
|
8fa66c1e0f | ||
|
|
72015da467 | ||
|
|
4b693e2ae3 | ||
|
|
ff2baacc3d | ||
|
|
2173bc5f43 | ||
|
|
df6169bc98 | ||
|
|
598abcc589 | ||
|
|
213fe1dc4e | ||
|
|
75e309731e | ||
|
|
95bc3e6745 | ||
|
|
e2b641ae89 | ||
|
|
b3ad137691 | ||
|
|
f86c544792 | ||
|
|
f8ea0c384d | ||
|
|
9e24786f67 | ||
|
|
8a5dc32578 | ||
|
|
f4a580bb90 | ||
|
|
cbef830df9 | ||
|
|
fba900daa5 | ||
|
|
f39f0d1527 | ||
|
|
c41948ccd8 | ||
|
|
43adb0fb11 | ||
|
|
7765a97e7d | ||
|
|
03cbed9b05 | ||
|
|
427fb4e35c | ||
|
|
e87f9950af | ||
|
|
8c8aa770bf | ||
|
|
0d063873fc | ||
|
|
ce08d9e8aa | ||
|
|
83a0310c8c | ||
|
|
c43e205212 | ||
|
|
33b0a9e27d | ||
|
|
4f0ea6366c | ||
|
|
2ade0b8846 | ||
|
|
6499bfe3d4 | ||
|
|
4c7086968f | ||
|
|
6afdb0cf3a | ||
|
|
ed9f38d2c8 | ||
|
|
88df2a502d | ||
|
|
7dd392401c | ||
|
|
8425f1d7c4 | ||
|
|
70c33882cc | ||
|
|
349b349518 | ||
|
|
5acfa398f6 | ||
|
|
e609c6bb72 | ||
|
|
0b98ccad45 | ||
|
|
c352efcb86 | ||
|
|
e640b6fe19 | ||
|
|
4c2f40dcb6 | ||
|
|
18a89d4407 | ||
|
|
4115652695 | ||
|
|
b3414bd1ff | ||
|
|
0604030b91 | ||
|
|
dfbe6e9ba2 | ||
|
|
123794a00d | ||
|
|
1404cc8010 | ||
|
|
a661de16c9 | ||
|
|
0ac3438d32 | ||
|
|
7f22a98f2f | ||
|
|
4e500ded50 | ||
|
|
edc22e842f | ||
|
|
8b1399261f | ||
|
|
de277a2ee2 | ||
|
|
7d29d778ac | ||
|
|
8f31fb9804 | ||
|
|
482d74f7bc | ||
|
|
2fdafcd573 | ||
|
|
53aa5b4df6 | ||
|
|
a440ae5c79 | ||
|
|
16ac97e9d0 | ||
|
|
245c600c82 | ||
|
|
3995ea2983 | ||
|
|
8ed030a476 | ||
|
|
d1b54dc990 | ||
|
|
22eced1dbf | ||
|
|
f0fd4556b5 | ||
|
|
715c169167 | ||
|
|
c4d4413242 | ||
|
|
5667da9174 | ||
|
|
174791532d | ||
|
|
99df787e24 | ||
|
|
ce37464050 | ||
|
|
5e521b0c82 | ||
|
|
55ceb8f85d | ||
|
|
a4a3f33180 | ||
|
|
b20e7a0964 | ||
|
|
c4d91177dc | ||
|
|
f67d32ab86 | ||
|
|
21ae091f42 | ||
|
|
39a06862d4 | ||
|
|
6014f5c3f4 | ||
|
|
947af0acdd | ||
|
|
a90b548b4f | ||
|
|
b379adec28 | ||
|
|
29e4829f69 | ||
|
|
d2675fab82 | ||
|
|
aac3e00f8d | ||
|
|
0a450e64a8 | ||
|
|
536b4451d7 | ||
|
|
0f3f27fdb6 | ||
|
|
eb026838e3 | ||
|
|
14fac980ed | ||
|
|
ff6a4ff910 | ||
|
|
9b432aff7a | ||
|
|
92ee714f9a | ||
|
|
ec184ca5ee | ||
|
|
f306c14575 | ||
|
|
4dc4dd29c5 | ||
|
|
61b4087a40 | ||
|
|
2200cf407d | ||
|
|
afc11951a6 | ||
|
|
4b6ea72aac | ||
|
|
6deeb02139 | ||
|
|
97f60f111e | ||
|
|
4601e0f8c8 | ||
|
|
1c879718cf | ||
|
|
da7b701e60 | ||
|
|
fe79b27b44 | ||
|
|
bc60857ce4 | ||
|
|
e0910ad351 | ||
|
|
ff6408f099 | ||
|
|
7ba7560fbb | ||
|
|
32e2a551b0 | ||
|
|
bdd2076eec | ||
|
|
3eef024f77 | ||
|
|
d227cd4384 | ||
|
|
a4627f139a | ||
|
|
9b825de617 | ||
|
|
05735deb56 | ||
|
|
e5a1248a55 | ||
|
|
0e05e0e792 | ||
|
|
86e51813ca | ||
|
|
83a0185a83 | ||
|
|
ebfd00b146 | ||
|
|
4fbe7004cc | ||
|
|
da8bf9a040 | ||
|
|
6b6eadf134 | ||
|
|
75c640b4a9 | ||
|
|
24d694db3b | ||
|
|
eda7fc1501 | ||
|
|
5bc1cba2eb | ||
|
|
bf2adcabed | ||
|
|
e927d0b2e5 | ||
|
|
6eff9d3bee | ||
|
|
8336528f09 | ||
|
|
e86e33214f | ||
|
|
089b811bbc | ||
|
|
50d96ad8df | ||
|
|
657f2cd5ad | ||
|
|
e900b59008 | ||
|
|
0313576fe1 | ||
|
|
5f75afe05c | ||
|
|
0ead3cde09 | ||
|
|
4597884d31 | ||
|
|
2ad5e81e60 | ||
|
|
574afb0c9b | ||
|
|
e17c1b8754 | ||
|
|
93066e4ca0 | ||
|
|
56b718f04d | ||
|
|
629b4b0501 | ||
|
|
c638650d0a | ||
|
|
1cbcbbb11f | ||
|
|
c9bd7ed852 | ||
|
|
334ce235ff | ||
|
|
10d362f509 | ||
|
|
2e7bd5708f | ||
|
|
cfe4ebf8cb | ||
|
|
8f1c9dbec5 | ||
|
|
0c055b50d1 | ||
|
|
139c07ca10 | ||
|
|
5bb7316722 | ||
|
|
54f4cfcdab | ||
|
|
9db66ce8e8 | ||
|
|
d5e9ebc0db | ||
|
|
cabc625009 |
@@ -1,14 +0,0 @@
|
||||
environment:
|
||||
matrix:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
install:
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
|
||||
- rustup-init.exe -y --default-host %TARGET%
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -vV
|
||||
- cargo -vV
|
||||
build: false
|
||||
test_script:
|
||||
- cargo build --verbose --manifest-path lettre/Cargo.toml
|
||||
- cargo test --verbose --manifest-path lettre_email/Cargo.toml
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz
|
||||
tar xzf master.tar.gz
|
||||
cd kcov-master
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install DESTDIR=../../kcov-build
|
||||
cd ../..
|
||||
rm -rf kcov-master
|
||||
for file in target/debug/lettre*[^\.d]; do
|
||||
mkdir -p "target/cov/$(basename $file)"
|
||||
./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"
|
||||
done
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
echo "Uploaded code coverage"
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
cd website
|
||||
make clean && make
|
||||
echo "lettre.at" > _book/html/CNAME
|
||||
sudo pip install ghp-import
|
||||
ghp-import -n _book/html
|
||||
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: amousset
|
||||
12
.github/workflows/audit.yml
vendored
Normal file
12
.github/workflows/audit.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Security audit
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
121
.github/workflows/test.yml
vendored
Normal file
121
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
name: Continuous integration
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
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
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
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
|
||||
|
||||
# coverage:
|
||||
# name: Coverage
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v1
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# toolchain: nightly
|
||||
# override: true
|
||||
# - 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-fail-fast
|
||||
# env:
|
||||
# CARGO_INCREMENTAL: "0"
|
||||
# RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads"
|
||||
# - id: coverage
|
||||
# uses: actions-rs/grcov@v0.1
|
||||
# - name: Coveralls upload
|
||||
# uses: coverallsapp/github-action@master
|
||||
# with:
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# path-to-lcov: ${{ steps.coverage.outputs.report }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
.vscode/
|
||||
.project/
|
||||
.idea/
|
||||
lettre.sublime-*
|
||||
lettre.iml
|
||||
target/
|
||||
/Cargo.lock
|
||||
|
||||
29
.travis.yml
29
.travis.yml
@@ -1,29 +0,0 @@
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- 1.32.0
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
sudo: required
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- postfix
|
||||
- libcurl4-openssl-dev
|
||||
- libelf-dev
|
||||
- libdw-dev
|
||||
- cmake
|
||||
- gcc
|
||||
- binutils-dev
|
||||
- libiberty-dev
|
||||
before_script:
|
||||
- smtp-sink 2525 1000&
|
||||
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
|
||||
script:
|
||||
- cargo test --verbose --all --all-features
|
||||
after_success:
|
||||
- ./.build-scripts/codecov.sh
|
||||
- '[ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_BRANCH" = "v0.9.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'
|
||||
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,37 +1,45 @@
|
||||
<a name="v0.9.6"></a>
|
||||
### v0.9.6 (2021-05-22)
|
||||
<a name="v0.10.0"></a>
|
||||
### v0.10.0 (unreleased)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
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`
|
||||
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`.
|
||||
* The `serde-impls` feature has been renamed to `serde`. To migrate, rename the feature.
|
||||
|
||||
#### 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))
|
||||
|
||||
#### 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)
|
||||
* The `new` method of `ClientId` is deprecated
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **transport**
|
||||
* **SECURITY**: Prevent SMTP command injection in smtp transport
|
||||
|
||||
<a name="v0.9.5"></a>
|
||||
### v0.9.5 (2020-11-11)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **transport**
|
||||
* **SECURITY**: Prevent argument injection in sendmail transport
|
||||
|
||||
<a name="v0.9.4"></a>
|
||||
### v0.9.4 (2020-04-21)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **email**
|
||||
* Go back to `rust-email` 0.0.20 as upgrade broke message formatting ([6a40f4a](https://github.com/lettre/lettre/commit/6a40f4a)
|
||||
|
||||
<a name="v0.9.3"></a>
|
||||
### v0.9.3 (2020-04-19)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **all:**
|
||||
* Fix compilation warnings ([9b591ff](https://github.com/lettre/lettre/commit/9b591ff932e35947f793aaaeec0e3f06e8818449))
|
||||
|
||||
* **email**
|
||||
* Update `rust-email` to 0.0.21 ([eff4e169](https://github.com/lettre/lettre/commit/eff4e1693f5e65430b851707fdfd18046bc796e3))
|
||||
* 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))
|
||||
|
||||
<a name="v0.9.2"></a>
|
||||
### v0.9.2 (2019-06-11)
|
||||
|
||||
@@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[version]: https://www.contributor-covenant.org/version/1/4/
|
||||
|
||||
@@ -33,3 +33,11 @@ Any line of the commit message cannot be longer 72 characters.
|
||||
all
|
||||
|
||||
The body explains the change, and the footer contains relevant changelog notes and references to fixed issues.
|
||||
|
||||
### Release process
|
||||
|
||||
Releases are made using `cargo-release`:
|
||||
|
||||
```bash
|
||||
cargo release --dry-run 0.10.0 --prev-tag-name v0.9.2 -v
|
||||
```
|
||||
|
||||
98
Cargo.toml
98
Cargo.toml
@@ -1,5 +1,93 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"lettre",
|
||||
"lettre_email",
|
||||
]
|
||||
[package]
|
||||
name = "lettre"
|
||||
version = "0.10.0-alpha.2" # remember to update html_root_url and README.md
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "https://lettre.at"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
|
||||
categories = ["email", "network-programming"]
|
||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||
edition = "2018"
|
||||
|
||||
[badges]
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
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"
|
||||
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 }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
webpki = { version = "0.21", optional = true }
|
||||
webpki-roots = { version = "0.20", 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"] }
|
||||
|
||||
[[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"]
|
||||
sendmail-transport = []
|
||||
smtp-transport = ["base64", "nom"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[[example]]
|
||||
name = "smtp"
|
||||
required-features = ["smtp-transport"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_tls"
|
||||
required-features = ["smtp-transport", "native-tls"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_starttls"
|
||||
required-features = ["smtp-transport", "native-tls"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio02_smtp_tls"
|
||||
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio02_smtp_starttls"
|
||||
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -1,4 +1,6 @@
|
||||
Copyright (c) 2014-2018 Alexis Mousset
|
||||
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me>
|
||||
Copyright (c) 2019-2020 Paolo Barbolini <paolo@paolo565.org>
|
||||
Copyright (c) 2018 K. <kayo@illumium.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
||||
112
README.md
112
README.md
@@ -1,25 +1,39 @@
|
||||
# lettre
|
||||
<h1 align="center">lettre</h1>
|
||||
<div align="center">
|
||||
<strong>
|
||||
A mailer library for Rust
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
**Lettre is a mailer library for Rust.**
|
||||
<br />
|
||||
|
||||
[](https://travis-ci.org/lettre/lettre)
|
||||
[](https://ci.appveyor.com/project/amousset/lettre/branch/master)
|
||||
[](https://codecov.io/gh/lettre/lettre)
|
||||
<div align="center">
|
||||
<a href="https://docs.rs/lettre">
|
||||
<img src="https://docs.rs/lettre/badge.svg"
|
||||
alt="docs" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/lettre">
|
||||
<img src="https://img.shields.io/crates/d/lettre.svg"
|
||||
alt="downloads" />
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://gitter.im/lettre/lettre">
|
||||
<img src="https://badges.gitter.im/lettre/lettre.svg"
|
||||
alt="chat on gitter" />
|
||||
</a>
|
||||
<a href="https://lettre.at">
|
||||
<img src="https://img.shields.io/badge/visit-website-blueviolet"
|
||||
alt="website" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
[](https://crates.io/crates/lettre)
|
||||
[](https://docs.rs/lettre/)
|
||||
[]()
|
||||
[](./LICENSE)
|
||||
---
|
||||
|
||||
[](https://gitter.im/lettre/lettre?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](http://isitmaintained.com/project/lettre/lettre "Average time to resolve an issue")
|
||||
[](http://isitmaintained.com/project/lettre/lettre "Percentage of issues still open")
|
||||
|
||||
Useful links:
|
||||
|
||||
* [User documentation](http://lettre.at/)
|
||||
* [API documentation](https://docs.rs/lettre/)
|
||||
* [Changelog](https://github.com/lettre/lettre/blob/master/CHANGELOG.md)
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -31,50 +45,47 @@ 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)
|
||||
|
||||
Lettre does not provide (for now):
|
||||
|
||||
* Email parsing
|
||||
|
||||
## Example
|
||||
|
||||
This library requires Rust 1.32 or newer.
|
||||
This library requires Rust 1.40 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.9"
|
||||
lettre_email = "0.9"
|
||||
lettre = "0.10.0-alpha.2"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
use lettre::{EmailTransport, SmtpTransport};
|
||||
use lettre_email::EmailBuilder;
|
||||
use std::path::Path;
|
||||
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();
|
||||
|
||||
fn main() {
|
||||
let email = EmailBuilder::new()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.build()
|
||||
.unwrap();
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap()
|
||||
.build();
|
||||
// Send the email
|
||||
let result = mailer.send(&email);
|
||||
// Open a remote connection to gmail
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
```
|
||||
|
||||
@@ -82,6 +93,8 @@ fn main() {
|
||||
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
|
||||
|
||||
Alternatively only unit tests can be run by doing `cargo test --lib`.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Anyone who interacts with Lettre in any space, including but not limited to
|
||||
@@ -91,4 +104,7 @@ this GitHub repository, must follow our [code of conduct](https://github.com/let
|
||||
|
||||
This program is distributed under the terms of the MIT license.
|
||||
|
||||
The builder comes from [emailmessage-rs](https://github.com/katyo/emailmessage-rs) by
|
||||
Kayo, under MIT license.
|
||||
|
||||
See [LICENSE](./LICENSE) for details.
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 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
|
||||
should not be reported via the public Github Issue tracker.
|
||||
|
||||
## Security advisories
|
||||
|
||||
Security issues will be announced via the [RustSec advisory database](https://github.com/RustSec/advisory-db).
|
||||
40
benches/transport_smtp.rs
Normal file
40
benches/transport_smtp.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
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();
|
||||
|
||||
c.bench_function("send email", move |b| {
|
||||
b.iter(|| {
|
||||
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();
|
||||
let result = black_box(sender.send(&email));
|
||||
assert!(result.is_ok());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_reuse_send(c: &mut Criterion) {
|
||||
let sender = SmtpTransport::builder("127.0.0.1").port(2525).build();
|
||||
c.bench_function("send email with connection reuse", move |b| {
|
||||
b.iter(|| {
|
||||
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();
|
||||
let result = black_box(sender.send(&email));
|
||||
assert!(result.is_ok());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_simple_send, bench_reuse_send);
|
||||
criterion_main!(benches);
|
||||
22
examples/smtp.rs
Normal file
22
examples/smtp.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use lettre::{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("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mailer = SmtpTransport::unencrypted_localhost();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
27
examples/smtp_starttls.rs
Normal file
27
examples/smtp_starttls.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use lettre::{transport::smtp::authentication::Credentials, 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("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
27
examples/smtp_tls.rs
Normal file
27
examples/smtp_tls.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use lettre::{transport::smtp::authentication::Credentials, 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("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
36
examples/tokio02_smtp_starttls.rs
Normal file
36
examples/tokio02_smtp_starttls.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
|
||||
Tokio02Transport,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.body("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();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
36
examples/tokio02_smtp_tls.rs
Normal file
36
examples/tokio02_smtp_tls.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
|
||||
Tokio02Transport,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.body("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();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../CHANGELOG.md
|
||||
@@ -1,53 +0,0 @@
|
||||
[package]
|
||||
|
||||
name = "lettre"
|
||||
version = "0.9.6" # remember to update html_root_url
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "http://lettre.at"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
categories = ["email"]
|
||||
keywords = ["email", "smtp", "mailer"]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "lettre/lettre" }
|
||||
appveyor = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
|
||||
[dependencies]
|
||||
log = "^0.4"
|
||||
nom = { version = "^4.0", optional = true }
|
||||
bufstream = { version = "^0.1", optional = true }
|
||||
native-tls = { version = "^0.2", optional = true }
|
||||
base64 = { version = "^0.10", optional = true }
|
||||
hostname = { version = "^0.1", optional = true }
|
||||
serde = { version = "^1.0", optional = true }
|
||||
serde_json = { version = "^1.0", optional = true }
|
||||
serde_derive = { version = "^1.0", optional = true }
|
||||
fast_chemail = "^0.9"
|
||||
r2d2 = { version = "^0.8", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "^0.6"
|
||||
glob = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["file-transport", "smtp-transport", "sendmail-transport"]
|
||||
unstable = []
|
||||
serde-impls = ["serde", "serde_derive"]
|
||||
file-transport = ["serde-impls", "serde_json"]
|
||||
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
|
||||
sendmail-transport = []
|
||||
connection-pool = [ "r2d2" ]
|
||||
|
||||
[[example]]
|
||||
name = "smtp"
|
||||
required-features = ["smtp-transport"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_gmail"
|
||||
required-features = ["smtp-transport"]
|
||||
@@ -1 +0,0 @@
|
||||
../LICENSE
|
||||
@@ -1 +0,0 @@
|
||||
../README.md
|
||||
@@ -1,50 +0,0 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate lettre;
|
||||
extern crate test;
|
||||
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::{ClientSecurity, Envelope, SmtpTransport};
|
||||
use lettre::{EmailAddress, SendableEmail, Transport};
|
||||
|
||||
#[bench]
|
||||
fn bench_simple_send(b: &mut test::Bencher) {
|
||||
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_reuse_send(b: &mut test::Bencher) {
|
||||
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
sender.close()
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
extern crate env_logger;
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::smtp::authentication::Credentials;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("from@gmail.com".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("to@example.com".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let creds = Credentials::new(
|
||||
"example_username".to_string(),
|
||||
"example_password".to_string(),
|
||||
);
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.transport();
|
||||
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
/// Error type for email content
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Error {
|
||||
/// Missing from in envelope
|
||||
MissingFrom,
|
||||
/// Missing to in envelope
|
||||
MissingTo,
|
||||
/// Invalid email
|
||||
InvalidEmailAddress,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(&match *self {
|
||||
MissingFrom => "missing source address, invalid envelope".to_owned(),
|
||||
MissingTo => "missing destination address, invalid envelope".to_owned(),
|
||||
InvalidEmailAddress => "invalid email address".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Email result type
|
||||
pub type EmailResult<T> = Result<T, Error>;
|
||||
@@ -1,60 +0,0 @@
|
||||
//! The file transport writes the emails to the given directory. The name of the file will be
|
||||
//! `message_id.txt`.
|
||||
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
|
||||
//!
|
||||
|
||||
use file::error::FileResult;
|
||||
use serde_json;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use Envelope;
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
/// Creates a new transport to the given directory
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
struct SerializableEmail {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for FileTransport {
|
||||
type Result = FileResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> FileResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
let envelope = email.envelope().clone();
|
||||
|
||||
let mut file = self.path.clone();
|
||||
file.push(format!("{}.json", message_id));
|
||||
|
||||
let serialized = serde_json::to_string(&SerializableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: email.message_to_string()?.as_bytes().to_vec(),
|
||||
})?;
|
||||
|
||||
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
|
||||
//!
|
||||
//! This mailer contains the available transports for your emails.
|
||||
//!
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/lettre/0.9.6")]
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces
|
||||
)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate base64;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate bufstream;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate hostname;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate native_tls;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[macro_use]
|
||||
extern crate nom;
|
||||
#[cfg(feature = "serde-impls")]
|
||||
extern crate serde;
|
||||
#[cfg(feature = "serde-impls")]
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate fast_chemail;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
extern crate r2d2;
|
||||
#[cfg(feature = "file-transport")]
|
||||
extern crate serde_json;
|
||||
|
||||
pub mod error;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
|
||||
use error::EmailResult;
|
||||
use error::Error;
|
||||
use fast_chemail::is_valid_email;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub use file::FileTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub use sendmail::SendmailTransport;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::client::net::ClientTlsParameters;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
||||
pub use smtp::r2d2::SmtpConnectionManager;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::{ClientSecurity, SmtpClient, SmtpTransport};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Email address
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct EmailAddress(String);
|
||||
|
||||
impl EmailAddress {
|
||||
pub fn new(address: String) -> EmailResult<EmailAddress> {
|
||||
if !is_valid_email(&address) && !address.ends_with("localhost") {
|
||||
return Err(Error::InvalidEmailAddress);
|
||||
}
|
||||
Ok(EmailAddress(address))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EmailAddress {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
EmailAddress::new(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EmailAddress {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for EmailAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for EmailAddress {
|
||||
fn as_ref(&self) -> &OsStr {
|
||||
&self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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-impls", derive(Serialize, Deserialize))]
|
||||
pub struct Envelope {
|
||||
/// The envelope recipients' addresses
|
||||
///
|
||||
/// This can not be empty.
|
||||
forward_path: Vec<EmailAddress>,
|
||||
/// The envelope sender address
|
||||
reverse_path: Option<EmailAddress>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
/// Creates a new envelope, which may fail if `to` is empty.
|
||||
pub fn new(from: Option<EmailAddress>, to: Vec<EmailAddress>) -> EmailResult<Envelope> {
|
||||
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) -> &[EmailAddress] {
|
||||
self.forward_path.as_slice()
|
||||
}
|
||||
|
||||
/// Source address of the envelope
|
||||
pub fn from(&self) -> Option<&EmailAddress> {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
Reader(Box<dyn Read + Send>),
|
||||
Bytes(Cursor<Vec<u8>>),
|
||||
}
|
||||
|
||||
impl Read for Message {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
Message::Reader(ref mut rdr) => rdr.read(buf),
|
||||
Message::Bytes(ref mut rdr) => rdr.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sendable email structure
|
||||
pub struct SendableEmail {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Message,
|
||||
}
|
||||
|
||||
impl SendableEmail {
|
||||
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
|
||||
SendableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Bytes(Cursor::new(message)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_reader(
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Box<dyn Read + Send>,
|
||||
) -> SendableEmail {
|
||||
SendableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Reader(message),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
}
|
||||
|
||||
pub fn message_id(&self) -> &str {
|
||||
&self.message_id
|
||||
}
|
||||
|
||||
pub fn message(self) -> Message {
|
||||
self.message
|
||||
}
|
||||
|
||||
pub fn message_to_string(mut self) -> Result<String, io::Error> {
|
||||
let mut message_content = String::new();
|
||||
self.message.read_to_string(&mut message_content)?;
|
||||
Ok(message_content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transport method for emails
|
||||
pub trait Transport<'a> {
|
||||
/// Result type for the transport
|
||||
type Result;
|
||||
|
||||
/// Sends the email
|
||||
fn send(&mut self, email: SendableEmail) -> Self::Result;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//!
|
||||
|
||||
use sendmail::error::SendmailResult;
|
||||
use std::io::prelude::*;
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Sends an email using the `sendmail` command
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct SendmailTransport {
|
||||
command: String,
|
||||
}
|
||||
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: "/usr/sbin/sendmail".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given sendmail command
|
||||
pub fn new_with_command<S: Into<String>>(command: S) -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: command.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for SendmailTransport {
|
||||
type Result = SendmailResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> SendmailResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = Command::new(&self.command)
|
||||
.arg("-i")
|
||||
.arg("-f")
|
||||
.arg(
|
||||
email
|
||||
.envelope()
|
||||
.from()
|
||||
.map(|x| x.as_ref())
|
||||
.unwrap_or("\"\""),
|
||||
)
|
||||
.arg("--")
|
||||
.args(email.envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let mut message_content = String::new();
|
||||
let _ = email.message().read_to_string(&mut message_content);
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(message_content.as_bytes())?;
|
||||
|
||||
info!("Wrote {} message to stdin", message_id);
|
||||
|
||||
let output = process.wait_with_output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
// TODO display stderr
|
||||
Err(error::Error::Client("The message could not be sent"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
//! SMTP client
|
||||
|
||||
use bufstream::BufStream;
|
||||
use nom::ErrorKind as NomErrorKind;
|
||||
use smtp::authentication::{Credentials, Mechanism};
|
||||
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
|
||||
use smtp::commands::*;
|
||||
use smtp::error::{Error, SmtpResult};
|
||||
use smtp::response::Response;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::string::String;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod mock;
|
||||
pub mod net;
|
||||
|
||||
/// The codec used for transparency
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct ClientCodec {
|
||||
escape_count: u8,
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub fn new() -> Self {
|
||||
ClientCodec::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Adds transparency
|
||||
/// TODO: replace CR and LF by CRLF
|
||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) -> Result<(), Error> {
|
||||
match frame.len() {
|
||||
0 => {
|
||||
match self.escape_count {
|
||||
0 => buf.write_all(b"\r\n.\r\n")?,
|
||||
1 => buf.write_all(b"\n.\r\n")?,
|
||||
2 => buf.write_all(b".\r\n")?,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.escape_count = 0;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
let mut start = 0;
|
||||
for (idx, byte) in frame.iter().enumerate() {
|
||||
match self.escape_count {
|
||||
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
|
||||
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
|
||||
2 => {
|
||||
self.escape_count = if *byte == b'.' {
|
||||
3
|
||||
} else if *byte == b'\r' {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if self.escape_count == 3 {
|
||||
self.escape_count = 0;
|
||||
buf.write_all(&frame[start..idx])?;
|
||||
buf.write_all(b".")?;
|
||||
start = idx;
|
||||
}
|
||||
}
|
||||
buf.write_all(&frame[start..])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||
/// Used for debug displays
|
||||
fn escape_crlf(string: &str) -> String {
|
||||
string.replace("\r\n", "<CRLF>")
|
||||
}
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InnerClient<S: Write + Read = NetworkStream> {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: Option<BufStream<S>>,
|
||||
}
|
||||
|
||||
macro_rules! return_err (
|
||||
($err: expr, $client: ident) => ({
|
||||
return Err(From::from($err))
|
||||
})
|
||||
);
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
|
||||
impl<S: Write + Read> InnerClient<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Client`
|
||||
pub fn new() -> InnerClient<S> {
|
||||
InnerClient { stream: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.command(QuitCommand);
|
||||
self.stream = None;
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: S) {
|
||||
self.stream = Some(BufStream::new(stream));
|
||||
}
|
||||
|
||||
/// Upgrades the underlying connection to SSL/TLS
|
||||
pub fn upgrade_tls_stream(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
|
||||
match self.stream {
|
||||
Some(ref mut stream) => stream.get_mut().upgrade_tls(tls_parameters),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.stream {
|
||||
Some(ref stream) => stream.get_ref().is_encrypted(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set timeout
|
||||
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.stream {
|
||||
Some(ref mut stream) => {
|
||||
stream.get_mut().set_read_timeout(duration)?;
|
||||
stream.get_mut().set_write_timeout(duration)?;
|
||||
Ok(())
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
&mut self,
|
||||
addr: &A,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> SmtpResult {
|
||||
// Connect should not be called when the client is already connected
|
||||
if self.stream.is_some() {
|
||||
return_err!("The connection is already established", self);
|
||||
}
|
||||
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
|
||||
let server_addr = match addresses.next() {
|
||||
Some(addr) => addr,
|
||||
None => return_err!("Could not resolve hostname", self),
|
||||
};
|
||||
|
||||
debug!("connecting to {}", server_addr);
|
||||
|
||||
// Try to connect
|
||||
self.set_stream(Connector::connect(&server_addr, tls_parameters)?);
|
||||
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.stream.is_some() && self.command(NoopCommand).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
|
||||
// TODO
|
||||
let mut challenges = 10;
|
||||
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = self.command(AuthCommand::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)?;
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message: Box<dyn Read>) -> SmtpResult {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
|
||||
let mut message_reader = BufReader::new(message);
|
||||
|
||||
loop {
|
||||
out_buf.clear();
|
||||
|
||||
let consumed = match message_reader.fill_buf() {
|
||||
Ok(bytes) => {
|
||||
codec.encode(bytes, &mut out_buf)?;
|
||||
bytes.len()
|
||||
}
|
||||
Err(ref err) => panic!("Failed with: {}", err),
|
||||
};
|
||||
message_reader.consume(consumed);
|
||||
|
||||
if consumed == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
self.write(out_buf.as_slice())?;
|
||||
}
|
||||
|
||||
self.write(b"\r\n.\r\n")?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn command<C: Display>(&mut self, command: C) -> SmtpResult {
|
||||
self.write(command.to_string().as_bytes())?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
if self.stream.is_none() {
|
||||
return Err(From::from("Connection closed"));
|
||||
}
|
||||
|
||||
self.stream.as_mut().unwrap().write_all(string)?;
|
||||
self.stream.as_mut().unwrap().flush()?;
|
||||
|
||||
debug!(
|
||||
"Wrote: {}",
|
||||
escape_crlf(String::from_utf8_lossy(string).as_ref())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
fn read_response(&mut self) -> SmtpResult {
|
||||
let mut raw_response = String::new();
|
||||
let mut response = raw_response.parse::<Response>();
|
||||
|
||||
while response.is_err() {
|
||||
if response.as_ref().err().unwrap() != &NomErrorKind::Complete {
|
||||
break;
|
||||
}
|
||||
// TODO read more than one line
|
||||
let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
|
||||
|
||||
// EOF is reached
|
||||
if read_count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
response = raw_response.parse::<Response>();
|
||||
}
|
||||
|
||||
debug!("Read: {}", escape_crlf(raw_response.as_ref()));
|
||||
|
||||
let final_response = response?;
|
||||
|
||||
if final_response.is_positive() {
|
||||
Ok(final_response)
|
||||
} else {
|
||||
Err(From::from(final_response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{escape_crlf, ClientCodec};
|
||||
|
||||
#[test]
|
||||
fn test_codec() {
|
||||
let mut codec = ClientCodec::new();
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
|
||||
assert!(codec.encode(b"test\r\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test\r\n\r\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b".\r\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"\r\ntest", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"te\r\n.\r\nst", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test.", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b".test\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test", &mut buf).is_ok());
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
"test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_crlf() {
|
||||
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
|
||||
assert_eq!(
|
||||
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CRLF>SIZE 42<CRLF>"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
//! A trait to represent a stream
|
||||
|
||||
use native_tls::{Protocol, TlsConnector, TlsStream};
|
||||
use smtp::client::mock::MockStream;
|
||||
use std::io::{self, ErrorKind, Read, Write};
|
||||
use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ClientTlsParameters {
|
||||
/// A connector from `native-tls`
|
||||
pub connector: TlsConnector,
|
||||
/// The domain to send during the TLS handshake
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
impl ClientTlsParameters {
|
||||
/// Creates a `ClientTlsParameters`
|
||||
pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters {
|
||||
ClientTlsParameters { connector, domain }
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv12];
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Represents the different types of underlying network streams
|
||||
pub enum NetworkStream {
|
||||
/// Plain TCP stream
|
||||
Tcp(TcpStream),
|
||||
/// Encrypted TCP stream
|
||||
Tls(TlsStream<TcpStream>),
|
||||
/// Mock stream
|
||||
Mock(MockStream),
|
||||
}
|
||||
|
||||
impl NetworkStream {
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
|
||||
NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
80,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdowns the connection
|
||||
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref s) => s.shutdown(how),
|
||||
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.read(buf),
|
||||
NetworkStream::Tls(ref mut s) => s.read(buf),
|
||||
NetworkStream::Mock(ref mut s) => s.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.write(buf),
|
||||
NetworkStream::Tls(ref mut s) => s.write(buf),
|
||||
NetworkStream::Mock(ref mut s) => s.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.flush(),
|
||||
NetworkStream::Tls(ref mut s) => s.flush(),
|
||||
NetworkStream::Mock(ref mut s) => s.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for the concept of opening a stream
|
||||
pub trait Connector: Sized {
|
||||
/// Opens a connection to the given IP socket
|
||||
fn connect(addr: &SocketAddr, tls_parameters: Option<&ClientTlsParameters>)
|
||||
-> io::Result<Self>;
|
||||
/// Upgrades to TLS connection
|
||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()>;
|
||||
/// Is the NetworkStream encrypted
|
||||
fn is_encrypted(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Connector for NetworkStream {
|
||||
fn connect(
|
||||
addr: &SocketAddr,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> io::Result<NetworkStream> {
|
||||
let tcp_stream = TcpStream::connect(addr)?;
|
||||
|
||||
match tls_parameters {
|
||||
Some(context) => context
|
||||
.connector
|
||||
.connect(context.domain.as_ref(), tcp_stream)
|
||||
.map(NetworkStream::Tls)
|
||||
.map_err(|e| io::Error::new(ErrorKind::Other, e)),
|
||||
None => Ok(NetworkStream::Tcp(tcp_stream)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
|
||||
*self = match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => match tls_parameters
|
||||
.connector
|
||||
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
|
||||
{
|
||||
Ok(tls_stream) => NetworkStream::Tls(tls_stream),
|
||||
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
NetworkStream::Tls(_) => return Ok(()),
|
||||
NetworkStream::Mock(_) => return Ok(()),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn is_encrypted(&self) -> bool {
|
||||
match *self {
|
||||
NetworkStream::Tcp(_) => false,
|
||||
NetworkStream::Tls(_) => true,
|
||||
NetworkStream::Mock(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for read and write timeout support
|
||||
pub trait Timeout: Sized {
|
||||
/// Set read timeout for IO calls
|
||||
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
|
||||
/// Set write timeout for IO calls
|
||||
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl Timeout for NetworkStream {
|
||||
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set write timeout for IO calls
|
||||
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
||||
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,479 +0,0 @@
|
||||
//! The SMTP transport sends emails using the SMTP protocol.
|
||||
//!
|
||||
//! This SMTP client follows [RFC
|
||||
//! 5321](https://tools.ietf.org/html/rfc5321), and is designed to efficiently send emails from an
|
||||
//! application to a relay email server, as it relies as much as possible on the relay server
|
||||
//! for sanity and RFC compliance checks.
|
||||
//!
|
||||
//! It implements the following extensions:
|
||||
//!
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//!
|
||||
|
||||
use native_tls::TlsConnector;
|
||||
use smtp::authentication::{
|
||||
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
||||
};
|
||||
use smtp::client::net::ClientTlsParameters;
|
||||
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
|
||||
use smtp::client::InnerClient;
|
||||
use smtp::commands::*;
|
||||
use smtp::error::{Error, SmtpResult};
|
||||
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
use {SendableEmail, Transport};
|
||||
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
pub mod r2d2;
|
||||
pub mod response;
|
||||
pub mod util;
|
||||
|
||||
// Registered port numbers:
|
||||
// https://www.iana.
|
||||
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
|
||||
/// Default smtp port
|
||||
pub const SMTP_PORT: u16 = 25;
|
||||
/// Default submission port
|
||||
pub const SUBMISSION_PORT: u16 = 587;
|
||||
/// Default submission over TLS port
|
||||
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub enum ClientSecurity {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
None,
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
Opportunistic(ClientTlsParameters),
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
Required(ClientTlsParameters),
|
||||
/// Use TLS wrapped connection
|
||||
Wrapper(ClientTlsParameters),
|
||||
}
|
||||
|
||||
/// Configures connection reuse behavior
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub enum ConnectionReuseParameters {
|
||||
/// Unlimited connection reuse
|
||||
ReuseUnlimited,
|
||||
/// Maximum number of connection reuse
|
||||
ReuseLimited(u16),
|
||||
/// Disable connection reuse, close connection after each transaction
|
||||
NoReuse,
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpClient {
|
||||
/// Enable connection reuse
|
||||
connection_reuse: ConnectionReuseParameters,
|
||||
/// Name sent during EHLO
|
||||
hello_name: ClientId,
|
||||
/// Credentials
|
||||
credentials: Option<Credentials>,
|
||||
/// Socket we are connecting to
|
||||
server_addr: SocketAddr,
|
||||
/// TLS security configuration
|
||||
security: ClientSecurity,
|
||||
/// Enable UTF8 mailboxes in envelope or headers
|
||||
smtp_utf8: bool,
|
||||
/// Optional enforced authentication mechanism
|
||||
authentication_mechanism: Option<Mechanism>,
|
||||
/// Define network timeout
|
||||
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
impl SmtpClient {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No connection reuse
|
||||
/// * No authentication
|
||||
/// * No SMTPUTF8 support
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
///
|
||||
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
|
||||
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
|
||||
match addresses.next() {
|
||||
Some(addr) => Ok(SmtpClient {
|
||||
server_addr: addr,
|
||||
security,
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse: ConnectionReuseParameters::NoReuse,
|
||||
hello_name: ClientId::hostname(),
|
||||
authentication_mechanism: None,
|
||||
timeout: Some(Duration::new(60, 0)),
|
||||
}),
|
||||
None => Err(Error::Resolution),
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple and secure transport, should be used when possible.
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
|
||||
|
||||
let tls_parameters =
|
||||
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
|
||||
|
||||
SmtpClient::new(
|
||||
(domain, SUBMISSIONS_PORT),
|
||||
ClientSecurity::Wrapper(tls_parameters),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
|
||||
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
|
||||
}
|
||||
|
||||
/// Enable SMTPUTF8 if the server supports it
|
||||
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
|
||||
self.smtp_utf8 = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
|
||||
self.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable connection reuse
|
||||
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
|
||||
self.connection_reuse = parameters;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client credentials
|
||||
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
|
||||
self.credentials = Some(credentials.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
|
||||
self.authentication_mechanism = Some(mechanism);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn transport(self) -> SmtpTransport {
|
||||
SmtpTransport::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of a client
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
/// Panic state
|
||||
pub panic: bool,
|
||||
/// Connection reuse counter
|
||||
pub connection_reuse_count: u16,
|
||||
}
|
||||
|
||||
/// Structure that implements the high level SMTP client
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct SmtpTransport {
|
||||
/// Information about the server
|
||||
/// Value is None before HELO/EHLO
|
||||
server_info: Option<ServerInfo>,
|
||||
/// SmtpTransport variable states
|
||||
state: State,
|
||||
/// Information about the client
|
||||
client_info: SmtpClient,
|
||||
/// Low level client
|
||||
client: InnerClient,
|
||||
}
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
if !$client.state.panic {
|
||||
$client.state.panic = true;
|
||||
$client.close();
|
||||
}
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
impl<'a> SmtpTransport {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn new(builder: SmtpClient) -> SmtpTransport {
|
||||
let client = InnerClient::new();
|
||||
|
||||
SmtpTransport {
|
||||
client,
|
||||
server_info: None,
|
||||
client_info: builder,
|
||||
state: State {
|
||||
panic: false,
|
||||
connection_reuse_count: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(&mut self) -> Result<(), Error> {
|
||||
// Check if the connection is still available
|
||||
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
|
||||
self.close();
|
||||
}
|
||||
|
||||
if self.state.connection_reuse_count > 0 {
|
||||
info!(
|
||||
"connection already established to {}",
|
||||
self.client_info.server_addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.client.connect(
|
||||
&self.client_info.server_addr,
|
||||
match self.client_info.security {
|
||||
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
},
|
||||
)?;
|
||||
|
||||
self.client.set_timeout(self.client_info.timeout)?;
|
||||
|
||||
// Log the connection
|
||||
info!("connection established to {}", self.client_info.server_addr);
|
||||
|
||||
self.ehlo()?;
|
||||
|
||||
match (
|
||||
&self.client_info.security.clone(),
|
||||
self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::StartTls),
|
||||
) {
|
||||
(&ClientSecurity::Required(_), false) => {
|
||||
return Err(From::from("Could not encrypt connection, aborting"));
|
||||
}
|
||||
(&ClientSecurity::Opportunistic(_), false) => (),
|
||||
(&ClientSecurity::None, _) => (),
|
||||
(&ClientSecurity::Wrapper(_), _) => (),
|
||||
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
|
||||
| (&ClientSecurity::Required(ref tls_parameters), true) => {
|
||||
try_smtp!(self.client.command(StarttlsCommand), self);
|
||||
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
|
||||
|
||||
debug!("connection encrypted");
|
||||
|
||||
// Send EHLO again
|
||||
self.ehlo()?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.client_info.credentials.is_some() {
|
||||
let mut found = false;
|
||||
|
||||
// Compute accepted mechanism
|
||||
let accepted_mechanisms = match self.client_info.authentication_mechanism {
|
||||
Some(mechanism) => vec![mechanism],
|
||||
None => {
|
||||
if self.client.is_encrypted() {
|
||||
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
|
||||
} else {
|
||||
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for mechanism in accepted_mechanisms {
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_auth_mechanism(mechanism)
|
||||
{
|
||||
found = true;
|
||||
try_smtp!(
|
||||
self.client
|
||||
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
|
||||
self
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
info!("No supported authentication mechanisms available");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the EHLO response and updates server information
|
||||
fn ehlo(&mut self) -> SmtpResult {
|
||||
// Extended Hello
|
||||
let ehlo_response = try_smtp!(
|
||||
self.client.command(EhloCommand::new(ClientId::new(
|
||||
self.client_info.hello_name.to_string()
|
||||
),)),
|
||||
self
|
||||
);
|
||||
|
||||
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
|
||||
|
||||
// Print server information
|
||||
debug!("server {}", self.server_info.as_ref().unwrap());
|
||||
|
||||
Ok(ehlo_response)
|
||||
}
|
||||
|
||||
/// Reset the client state
|
||||
pub fn close(&mut self) {
|
||||
// Close the SMTP transaction if needed
|
||||
self.client.close();
|
||||
|
||||
// Reset the client state
|
||||
self.server_info = None;
|
||||
self.state.panic = false;
|
||||
self.state.connection_reuse_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for SmtpTransport {
|
||||
type Result = SmtpResult;
|
||||
|
||||
/// Sends an email
|
||||
#[cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
|
||||
)]
|
||||
fn send(&mut self, email: SendableEmail) -> SmtpResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
if !self.client.is_connected() {
|
||||
self.connect()?;
|
||||
}
|
||||
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::EightBitMime)
|
||||
{
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::SmtpUtfEight)
|
||||
&& self.client_info.smtp_utf8
|
||||
{
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.client.command(MailCommand::new(
|
||||
email.envelope().from().cloned(),
|
||||
mail_options,
|
||||
)),
|
||||
self
|
||||
);
|
||||
|
||||
// Log the mail command
|
||||
info!(
|
||||
"{}: from=<{}>",
|
||||
message_id,
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in email.envelope().to() {
|
||||
try_smtp!(
|
||||
self.client
|
||||
.command(RcptCommand::new(to_address.clone(), vec![])),
|
||||
self
|
||||
);
|
||||
// Log the rcpt command
|
||||
info!("{}: to=<{}>", message_id, to_address);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.client.command(DataCommand), self);
|
||||
|
||||
// Message content
|
||||
let result = self.client.message(Box::new(email.message()));
|
||||
|
||||
if result.is_ok() {
|
||||
// Increment the connection reuse counter
|
||||
self.state.connection_reuse_count += 1;
|
||||
|
||||
// Log the message
|
||||
info!(
|
||||
"{}: conn_use={}, status=sent ({})",
|
||||
message_id,
|
||||
self.state.connection_reuse_count,
|
||||
result
|
||||
.as_ref()
|
||||
.ok()
|
||||
.unwrap()
|
||||
.message
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&"no response".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
// Test if we can reuse the existing connection
|
||||
match self.client_info.connection_reuse {
|
||||
ConnectionReuseParameters::ReuseLimited(limit)
|
||||
if self.state.connection_reuse_count >= limit =>
|
||||
{
|
||||
self.close()
|
||||
}
|
||||
ConnectionReuseParameters::NoReuse => self.close(),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use r2d2::ManageConnection;
|
||||
use smtp::error::Error;
|
||||
use smtp::{ConnectionReuseParameters, SmtpClient, SmtpTransport};
|
||||
|
||||
pub struct SmtpConnectionManager {
|
||||
transport_builder: SmtpClient,
|
||||
}
|
||||
|
||||
impl SmtpConnectionManager {
|
||||
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
|
||||
Ok(SmtpConnectionManager {
|
||||
transport_builder: transport_builder
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageConnection for SmtpConnectionManager {
|
||||
type Connection = SmtpTransport;
|
||||
type Error = Error;
|
||||
|
||||
fn connect(&self) -> Result<Self::Connection, Error> {
|
||||
let mut transport = SmtpTransport::new(self.transport_builder.clone());
|
||||
transport.connect()?;
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
||||
if conn.client.is_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::Client("is not connected anymore"))
|
||||
}
|
||||
|
||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||
conn.state.panic
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//! The stub transport only logs message envelope and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//!
|
||||
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
/// This transport logs the message envelope and returns the given response
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct StubTransport {
|
||||
response: StubResult,
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
/// Creates a new transport that always returns the given response
|
||||
pub fn new(response: StubResult) -> StubTransport {
|
||||
StubTransport { response }
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_positive() -> StubTransport {
|
||||
StubTransport { response: Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type StubResult = Result<(), ()>;
|
||||
|
||||
impl<'a> Transport<'a> for StubTransport {
|
||||
type Result = StubResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> StubResult {
|
||||
info!(
|
||||
"{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
},
|
||||
email.envelope().to()
|
||||
);
|
||||
self.response
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
|
||||
mod test {
|
||||
extern crate lettre;
|
||||
extern crate r2d2;
|
||||
|
||||
use self::lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient};
|
||||
use self::lettre::{SmtpConnectionManager, Transport};
|
||||
use self::r2d2::Pool;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
fn email(message: &str) -> SendableEmail {
|
||||
SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
message.to_string().into_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_one() {
|
||||
let client = SmtpClient::new("localhost:2525", ClientSecurity::None).unwrap();
|
||||
let manager = SmtpConnectionManager::new(client).unwrap();
|
||||
let pool = Pool::builder().max_size(1).build(manager).unwrap();
|
||||
|
||||
let mut mailer = pool.get().unwrap();
|
||||
let result = (*mailer).send(email("send one"));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_from_thread() {
|
||||
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap();
|
||||
let manager = SmtpConnectionManager::new(client).unwrap();
|
||||
let pool = Pool::builder().max_size(2).build(manager).unwrap();
|
||||
|
||||
let (s1, r1) = mpsc::channel();
|
||||
let (s2, r2) = mpsc::channel();
|
||||
|
||||
let pool1 = pool.clone();
|
||||
let t1 = thread::spawn(move || {
|
||||
let mut conn = pool1.get().unwrap();
|
||||
s1.send(()).unwrap();
|
||||
r2.recv().unwrap();
|
||||
(*conn)
|
||||
.send(email("send from thread 1"))
|
||||
.expect("Send failed from thread 1");
|
||||
drop(conn);
|
||||
});
|
||||
|
||||
let pool2 = pool.clone();
|
||||
let t2 = thread::spawn(move || {
|
||||
let mut conn = pool2.get().unwrap();
|
||||
s2.send(()).unwrap();
|
||||
r1.recv().unwrap();
|
||||
(*conn)
|
||||
.send(email("send from thread 2"))
|
||||
.expect("Send failed from thread 2");
|
||||
drop(conn);
|
||||
});
|
||||
|
||||
t1.join().unwrap();
|
||||
t2.join().unwrap();
|
||||
|
||||
let mut mailer = pool.get().unwrap();
|
||||
(*mailer)
|
||||
.send(email("send from main thread"))
|
||||
.expect("Send failed from main thread");
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
extern crate glob;
|
||||
|
||||
use self::glob::glob;
|
||||
use std::env;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn test_readme() {
|
||||
let readme = Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("README.md");
|
||||
|
||||
skeptic_test(&readme);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn book_test() {
|
||||
let mut book_path = env::current_dir().unwrap();
|
||||
book_path.push(
|
||||
Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("../website/content/sending-messages"),
|
||||
); // For some reasons, calling .parent() once more gives us None...
|
||||
|
||||
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
|
||||
skeptic_test(&md.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn skeptic_test(path: &Path) {
|
||||
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
|
||||
let exe = env::current_exe().unwrap();
|
||||
let depdir = exe.parent().unwrap();
|
||||
|
||||
let mut cmd = Command::new(rustdoc);
|
||||
cmd.args(&["--verbose", "--test"])
|
||||
.arg("-L")
|
||||
.arg(&depdir)
|
||||
.arg(path);
|
||||
|
||||
let result = cmd
|
||||
.spawn()
|
||||
.expect("Failed to spawn process")
|
||||
.wait()
|
||||
.expect("Failed to run process");
|
||||
|
||||
assert!(
|
||||
result.success(),
|
||||
format!("Failed to run rustdoc tests on {:?}", path)
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
mod test {
|
||||
|
||||
use lettre::file::FileTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
use std::env::temp_dir;
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
let mut sender = FileTransport::new(temp_dir());
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
|
||||
let mut f = File::open(file.clone()).unwrap();
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
|
||||
assert_eq!(
|
||||
buffer,
|
||||
"{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"id\",\"message\":[72,101,108,108,111,32,195,159,226,152,186,32,101,120,97,109,112,108,101]}"
|
||||
);
|
||||
|
||||
remove_file(file).unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
mod test {
|
||||
use lettre::sendmail::SendmailTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
|
||||
#[test]
|
||||
fn sendmail_transport_simple() {
|
||||
let mut sender = SendmailTransport::new();
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let result = sender.send(email);
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
mod test {
|
||||
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
#[test]
|
||||
fn smtp_transport_simple() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.transport()
|
||||
.send(email)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::stub::StubTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
let mut sender_ok = StubTransport::new_positive();
|
||||
let mut sender_ko = StubTransport::new(Err(()));
|
||||
let email_ok = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email_ko = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
sender_ok.send(email_ok).unwrap();
|
||||
sender_ko.send(email_ko).unwrap_err();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../CHANGELOG.md
|
||||
@@ -1,31 +0,0 @@
|
||||
[package]
|
||||
|
||||
name = "lettre_email"
|
||||
version = "0.9.4" # remember to update html_root_url
|
||||
description = "Email builder"
|
||||
readme = "README.md"
|
||||
homepage = "http://lettre.at"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
categories = ["email"]
|
||||
keywords = ["email", "mailer"]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "lettre/lettre_email" }
|
||||
appveyor = { repository = "lettre/lettre_email" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre_email" }
|
||||
is-it-maintained-open-issues = { repository = "lettre/lettre_email" }
|
||||
|
||||
[dev-dependencies]
|
||||
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
|
||||
glob = "0.3"
|
||||
|
||||
[dependencies]
|
||||
email = "^0.0.20"
|
||||
mime = "^0.3"
|
||||
time = "^0.1"
|
||||
uuid = { version = "^0.7", features = ["v4"] }
|
||||
lettre = { version = "^0.9", path = "../lettre", default-features = false }
|
||||
base64 = "^0.10"
|
||||
@@ -1 +0,0 @@
|
||||
../LICENSE
|
||||
@@ -1 +0,0 @@
|
||||
../README.md
|
||||
@@ -1,34 +0,0 @@
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
extern crate mime;
|
||||
|
||||
use lettre::{SmtpClient, Transport};
|
||||
use lettre_email::Email;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let email = Email::builder()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.attachment_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email.into());
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
//! Error and result type for emails
|
||||
|
||||
use lettre;
|
||||
use std::io;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
use self::Error::*;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Envelope error
|
||||
Envelope(lettre::error::Error),
|
||||
/// Unparseable filename for attachment
|
||||
CannotParseFilename,
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(&match *self {
|
||||
CannotParseFilename => "Could not parse attachment filename".to_owned(),
|
||||
Io(ref err) => err.to_string(),
|
||||
Envelope(ref err) => err.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
match *self {
|
||||
Envelope(ref err) => Some(err),
|
||||
Io(ref err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre::error::Error> for Error {
|
||||
fn from(err: lettre::error::Error) -> Error {
|
||||
Error::Envelope(err)
|
||||
}
|
||||
}
|
||||
@@ -1,588 +0,0 @@
|
||||
//! Lettre is a mailer written in Rust. lettre_email provides a simple email builder.
|
||||
//!
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/lettre_email/0.9.4")]
|
||||
#![deny(
|
||||
missing_docs,
|
||||
missing_debug_implementations,
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces
|
||||
)]
|
||||
|
||||
extern crate base64;
|
||||
extern crate email as email_format;
|
||||
extern crate lettre;
|
||||
pub extern crate mime;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub use email_format::{Address, Header, Mailbox, MimeMessage, MimeMultipartType};
|
||||
use error::Error;
|
||||
use lettre::{error::Error as LettreError, EmailAddress, Envelope, SendableEmail};
|
||||
use mime::Mime;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use time::{now, Tm};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Builds a `MimeMessage` structure
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct PartBuilder {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
}
|
||||
|
||||
impl Default for PartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a message id
|
||||
pub type MessageId = String;
|
||||
|
||||
/// Builds an `Email` structure
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default)]
|
||||
pub struct EmailBuilder {
|
||||
/// Message
|
||||
message: PartBuilder,
|
||||
/// The recipients' addresses for the mail header
|
||||
to: Vec<Address>,
|
||||
/// The sender addresses for the mail header
|
||||
from: Vec<Address>,
|
||||
/// The Cc addresses for the mail header
|
||||
cc: Vec<Address>,
|
||||
/// The Bcc addresses for the mail header
|
||||
bcc: Vec<Address>,
|
||||
/// The Reply-To addresses for the mail header
|
||||
reply_to: Vec<Address>,
|
||||
/// The In-Reply-To ids for the mail header
|
||||
in_reply_to: Vec<MessageId>,
|
||||
/// The References ids for the mail header
|
||||
references: Vec<MessageId>,
|
||||
/// The sender address for the mail header
|
||||
sender: Option<Mailbox>,
|
||||
/// The envelope
|
||||
envelope: Option<Envelope>,
|
||||
/// Date issued
|
||||
date_issued: bool,
|
||||
}
|
||||
|
||||
/// Simple email representation
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Email {
|
||||
/// Message
|
||||
message: Vec<u8>,
|
||||
/// Envelope
|
||||
envelope: Envelope,
|
||||
/// Message-ID
|
||||
message_id: Uuid,
|
||||
}
|
||||
|
||||
impl Into<SendableEmail> for Email {
|
||||
fn into(self) -> SendableEmail {
|
||||
SendableEmail::new(
|
||||
self.envelope.clone(),
|
||||
self.message_id.to_string(),
|
||||
self.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Email {
|
||||
/// TODO
|
||||
pub fn builder() -> EmailBuilder {
|
||||
EmailBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartBuilder {
|
||||
/// Creates a new empty part
|
||||
pub fn new() -> PartBuilder {
|
||||
PartBuilder {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a generic header
|
||||
pub fn header<A: Into<Header>>(mut self, header: A) -> PartBuilder {
|
||||
self.message.headers.insert(header.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the body
|
||||
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
|
||||
self.message.body = body.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines a `MimeMultipartType` value
|
||||
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
|
||||
self.message.message_type = Some(mime_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `ContentType` header with the given MIME type
|
||||
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
|
||||
self.header(("Content-Type", content_type.to_string()))
|
||||
}
|
||||
|
||||
/// Adds a child part
|
||||
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
|
||||
self.message.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets built `MimeMessage`
|
||||
pub fn build(mut self) -> MimeMessage {
|
||||
self.message.update_headers();
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
EmailBuilder {
|
||||
message: PartBuilder::new(),
|
||||
to: vec![],
|
||||
from: vec![],
|
||||
cc: vec![],
|
||||
bcc: vec![],
|
||||
reply_to: vec![],
|
||||
in_reply_to: vec![],
|
||||
references: vec![],
|
||||
sender: None,
|
||||
envelope: None,
|
||||
date_issued: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
|
||||
self.message = self.message.body(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn header<A: Into<Header>>(mut self, header: A) -> EmailBuilder {
|
||||
self.message = self.message.header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `From` header and stores the sender address
|
||||
pub fn from<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.from.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and stores the recipient address
|
||||
pub fn to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.to.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and stores the recipient address
|
||||
pub fn cc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.cc.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Bcc` header and stores the recipient address
|
||||
pub fn bcc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.bcc.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.reply_to.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `In-Reply-To` header
|
||||
pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder {
|
||||
self.in_reply_to.push(message_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `References` header
|
||||
pub fn references(mut self, message_id: MessageId) -> EmailBuilder {
|
||||
self.references.push(message_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
pub fn sender<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.sender = Some(mailbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
|
||||
self.message = self.message.header(("Subject".to_string(), subject.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
||||
self.message = self.message.header(("Date", Tm::rfc822z(date).to_string()));
|
||||
self.date_issued = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email from a file
|
||||
///
|
||||
/// If not specified, the filename will be extracted from the file path.
|
||||
pub fn attachment_from_file(
|
||||
self,
|
||||
path: &Path,
|
||||
filename: Option<&str>,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
self.attachment(
|
||||
fs::read(path)?.as_slice(),
|
||||
filename.unwrap_or(
|
||||
path.file_name()
|
||||
.and_then(|x| x.to_str())
|
||||
.ok_or(Error::CannotParseFilename)?,
|
||||
),
|
||||
content_type,
|
||||
)
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email from a vector of bytes.
|
||||
pub fn attachment(
|
||||
self,
|
||||
body: &[u8],
|
||||
filename: &str,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
let encoded_body = base64::encode(&body)
|
||||
.as_bytes()
|
||||
.chunks(72)
|
||||
// base64 encoding is guaranteed to return utf-8, so this won't panic
|
||||
.map(|s| std::str::from_utf8(s).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\r\n");
|
||||
let content = PartBuilder::new()
|
||||
.body(encoded_body)
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"", filename),
|
||||
))
|
||||
.header(("Content-Type", content_type.to_string()))
|
||||
.header(("Content-Transfer-Encoding", "base64"))
|
||||
.build();
|
||||
|
||||
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
|
||||
}
|
||||
|
||||
/// Set the message type
|
||||
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
|
||||
self.message = self.message.message_type(message_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a child
|
||||
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
|
||||
self.message = self.message.child(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body to plain text content
|
||||
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
|
||||
let text = PartBuilder::new()
|
||||
.body(body)
|
||||
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
|
||||
.build();
|
||||
self.child(text)
|
||||
}
|
||||
|
||||
/// Sets the email body to HTML content
|
||||
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
|
||||
let html = PartBuilder::new()
|
||||
.body(body)
|
||||
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
|
||||
.build();
|
||||
self.child(html)
|
||||
}
|
||||
|
||||
/// Sets the email content
|
||||
pub fn alternative<S: Into<String>, T: Into<String>>(
|
||||
self,
|
||||
body_html: S,
|
||||
body_text: T,
|
||||
) -> EmailBuilder {
|
||||
let text = PartBuilder::new()
|
||||
.body(body_text)
|
||||
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
|
||||
.build();
|
||||
|
||||
let html = PartBuilder::new()
|
||||
.body(body_html)
|
||||
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
|
||||
.build();
|
||||
|
||||
let alternate = PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Alternative)
|
||||
.child(text)
|
||||
.child(html);
|
||||
|
||||
self.message_type(MimeMultipartType::Mixed)
|
||||
.child(alternate.build())
|
||||
}
|
||||
|
||||
/// Sets the envelope for manual destination control
|
||||
/// If this function is not called, the envelope will be calculated
|
||||
/// from the "to" and "cc" addresses you set.
|
||||
pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder {
|
||||
self.envelope = Some(envelope);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the Email
|
||||
pub fn build(mut self) -> Result<Email, Error> {
|
||||
// If there are multiple addresses in "From", the "Sender" is required.
|
||||
if self.from.len() >= 2 && self.sender.is_none() {
|
||||
// So, we must find something to put as Sender.
|
||||
for possible_sender in &self.from {
|
||||
// Only a mailbox can be used as sender, not Address::Group.
|
||||
if let Address::Mailbox(ref mbx) = *possible_sender {
|
||||
self.sender = Some(mbx.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Address::Group is not yet supported, so the line below will never panic.
|
||||
// If groups are supported one day, add another Error for this case
|
||||
// and return it here, if sender_header is still None at this point.
|
||||
assert!(self.sender.is_some());
|
||||
}
|
||||
// Add the sender header, if any.
|
||||
if let Some(ref v) = self.sender {
|
||||
self.message = self.message.header(("Sender", v.to_string()));
|
||||
}
|
||||
// Calculate the envelope
|
||||
let envelope = match self.envelope {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
// we need to generate the envelope
|
||||
let mut to = vec![];
|
||||
// add all receivers in to_header and cc_header
|
||||
for receiver in self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter()) {
|
||||
match *receiver {
|
||||
Address::Mailbox(ref m) => to.push(EmailAddress::from_str(&m.address)?),
|
||||
Address::Group(_, ref ms) => {
|
||||
for m in ms.iter() {
|
||||
to.push(EmailAddress::from_str(&m.address.clone())?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let from = Some(EmailAddress::from_str(&match self.sender {
|
||||
Some(x) => Ok(x.address), // if we have a sender_header, use it
|
||||
None => {
|
||||
// use a from header
|
||||
debug_assert!(self.from.len() <= 1); // else we'd have sender_header
|
||||
match self.from.first() {
|
||||
Some(a) => match *a {
|
||||
// if we have a from header
|
||||
Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()), // use it
|
||||
Address::Group(_, ref mailbox_list) => match mailbox_list.first() {
|
||||
// if it's an author group, use the first author
|
||||
Some(mailbox) => Ok(mailbox.address.clone()),
|
||||
// for an empty author group (the rarest of the rare cases)
|
||||
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
|
||||
},
|
||||
},
|
||||
// if we don't have a from header
|
||||
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
|
||||
}
|
||||
}
|
||||
}?)?);
|
||||
Envelope::new(from, to)?
|
||||
}
|
||||
};
|
||||
// Add the collected addresses as mailbox-list all at once.
|
||||
// The unwraps are fine because the conversions for Vec<Address> never errs.
|
||||
if !self.to.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("To".into(), self.to).unwrap());
|
||||
}
|
||||
if !self.from.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("From".into(), self.from).unwrap());
|
||||
} else if let Some(from) = envelope.from() {
|
||||
let from = vec![Address::new_mailbox(from.to_string())];
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("From".into(), from).unwrap());
|
||||
} else {
|
||||
return Err(Error::Envelope(LettreError::MissingFrom));
|
||||
}
|
||||
if !self.cc.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("Cc".into(), self.cc).unwrap());
|
||||
}
|
||||
if !self.reply_to.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("Reply-To".into(), self.reply_to).unwrap());
|
||||
}
|
||||
if !self.in_reply_to.is_empty() {
|
||||
self.message = self.message.header(
|
||||
Header::new_with_value("In-Reply-To".into(), self.in_reply_to.join(" ")).unwrap(),
|
||||
);
|
||||
}
|
||||
if !self.references.is_empty() {
|
||||
self.message = self.message.header(
|
||||
Header::new_with_value("References".into(), self.references.join(" ")).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.date_issued {
|
||||
self.message = self
|
||||
.message
|
||||
.header(("Date", Tm::rfc822z(&now()).to_string()));
|
||||
}
|
||||
|
||||
self.message = self.message.header(("MIME-Version", "1.0"));
|
||||
|
||||
let message_id = Uuid::new_v4();
|
||||
|
||||
if let Ok(header) = Header::new_with_value(
|
||||
"Message-ID".to_string(),
|
||||
format!("<{}.lettre@localhost>", message_id),
|
||||
) {
|
||||
self.message = self.message.header(header)
|
||||
}
|
||||
|
||||
Ok(Email {
|
||||
message: self.message.build().as_string().into_bytes(),
|
||||
envelope,
|
||||
message_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{EmailBuilder, SendableEmail};
|
||||
use lettre::EmailAddress;
|
||||
use time::now;
|
||||
|
||||
#[test]
|
||||
fn test_multiple_from() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
let email: SendableEmail = email_builder
|
||||
.to("anna@example.com")
|
||||
.from("dieter@example.com")
|
||||
.from("joachim@example.com")
|
||||
.date(&date_now)
|
||||
.subject("Invitation")
|
||||
.body("We invite you!")
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
let id = email.message_id().to_string();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Invitation\r\nSender: \
|
||||
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
|
||||
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
|
||||
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
|
||||
date_now.rfc822z(),
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_builder() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email: SendableEmail = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.in_reply_to("original".to_string())
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
let id = email.message_id().to_string();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
|
||||
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\n\
|
||||
Reply-To: <reply@localhost>\r\nIn-Reply-To: original\r\n\
|
||||
MIME-Version: 1.0\r\nMessage-ID: \
|
||||
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
|
||||
date_now.rfc822z(),
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_sendable() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email: SendableEmail = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
assert_eq!(
|
||||
email.envelope().from().unwrap().to_string(),
|
||||
"sender@localhost".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
email.envelope().to(),
|
||||
vec![
|
||||
EmailAddress::new("user@localhost".to_string()).unwrap(),
|
||||
EmailAddress::new("cc@localhost".to_string()).unwrap(),
|
||||
EmailAddress::new("bcc@localhost".to_string()).unwrap(),
|
||||
]
|
||||
.as_slice()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
use lettre::{EmailAddress, Envelope};
|
||||
use lettre_email::EmailBuilder;
|
||||
|
||||
#[test]
|
||||
fn build_with_envelope_test() {
|
||||
let e = Envelope::new(
|
||||
Some(EmailAddress::new("from@example.org".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let _email = EmailBuilder::new()
|
||||
.envelope(e)
|
||||
.subject("subject")
|
||||
.text("message")
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_with_envelope_without_from_test() {
|
||||
let e = Envelope::new(
|
||||
None,
|
||||
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let _email = EmailBuilder::new()
|
||||
.envelope(e)
|
||||
.subject("subject")
|
||||
.text("message")
|
||||
.build()
|
||||
.unwrap_err();
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
extern crate glob;
|
||||
|
||||
use self::glob::glob;
|
||||
use std::env;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn book_test() {
|
||||
let mut book_path = env::current_dir().unwrap();
|
||||
book_path.push(
|
||||
Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("../website/content/creating-messages"),
|
||||
); // For some reasons, calling .parent() once more gives us None...
|
||||
|
||||
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
|
||||
skeptic_test(&md.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn skeptic_test(path: &Path) {
|
||||
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
|
||||
let exe = env::current_exe().unwrap();
|
||||
let depdir = exe.parent().unwrap();
|
||||
|
||||
let mut cmd = Command::new(rustdoc);
|
||||
cmd.args(&["--verbose", "--test"])
|
||||
.arg("-L")
|
||||
.arg(&depdir)
|
||||
.arg(path);
|
||||
|
||||
let result = cmd
|
||||
.spawn()
|
||||
.expect("Failed to spawn process")
|
||||
.wait()
|
||||
.expect("Failed to run process");
|
||||
|
||||
assert!(
|
||||
result.success(),
|
||||
format!("Failed to run rustdoc tests on {:?}!", path)
|
||||
);
|
||||
}
|
||||
300
src/address.rs
Normal file
300
src/address.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
//! Representation of an email address
|
||||
|
||||
use idna::domain_to_ascii;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
net::IpAddr,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Email address
|
||||
///
|
||||
/// 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/).
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Address {
|
||||
/// Complete address
|
||||
serialized: String,
|
||||
/// Index into `serialized` before the '@'
|
||||
at_start: usize,
|
||||
}
|
||||
|
||||
impl<U, D> TryFrom<(U, D)> for Address
|
||||
where
|
||||
U: AsRef<str>,
|
||||
D: AsRef<str>,
|
||||
{
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
|
||||
let user = user.as_ref();
|
||||
Address::check_user(user)?;
|
||||
|
||||
let domain = domain.as_ref();
|
||||
Address::check_domain(domain)?;
|
||||
|
||||
let serialized = format!("{}@{}", user, domain);
|
||||
Ok(Address {
|
||||
serialized,
|
||||
at_start: user.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Regex from the specs
|
||||
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
|
||||
// It will mark esoteric email addresses like quoted string as invalid
|
||||
static USER_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
|
||||
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
|
||||
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
|
||||
|
||||
impl Address {
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for Address {
|
||||
fn as_ref(&self) -> &OsStr {
|
||||
self.serialized.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum AddressError {
|
||||
MissingParts,
|
||||
Unbalanced,
|
||||
InvalidUser,
|
||||
InvalidDomain,
|
||||
InvalidUtf8b,
|
||||
}
|
||||
|
||||
impl Error for AddressError {}
|
||||
|
||||
impl Display for AddressError {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_address() {
|
||||
let addr_str = "something@example.com";
|
||||
let addr = Address::from_str(addr_str).unwrap();
|
||||
let addr2 = Address::new("something", "example.com").unwrap();
|
||||
assert_eq!(addr, addr2);
|
||||
assert_eq!(addr.user(), "something");
|
||||
assert_eq!(addr.domain(), "example.com");
|
||||
assert_eq!(addr2.user(), "something");
|
||||
assert_eq!(addr2.domain(), "example.com");
|
||||
}
|
||||
}
|
||||
52
src/error.rs
Normal file
52
src/error.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
// FIXME message-specific errors
|
||||
/// Error type for email content
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Missing from in envelope
|
||||
MissingFrom,
|
||||
/// Missing to in envelope
|
||||
MissingTo,
|
||||
/// Can only be one from in envelope
|
||||
TooManyFrom,
|
||||
/// Invalid email: missing at
|
||||
EmailMissingAt,
|
||||
/// Invalid email: missing local part
|
||||
EmailMissingLocalPart,
|
||||
/// Invalid email: missing domain
|
||||
EmailMissingDomain,
|
||||
/// Cannot parse filename for attachment
|
||||
CannotParseFilename,
|
||||
/// IO error
|
||||
Io(std::io::Error),
|
||||
/// Non-ASCII chars
|
||||
NonAsciiChars,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
Error::MissingFrom => f.write_str("missing source address, invalid envelope"),
|
||||
Error::MissingTo => f.write_str("missing destination address, invalid envelope"),
|
||||
Error::TooManyFrom => f.write_str("there can only be one source address"),
|
||||
Error::EmailMissingAt => f.write_str("missing @ in email address"),
|
||||
Error::EmailMissingLocalPart => f.write_str("missing local part in email address"),
|
||||
Error::EmailMissingDomain => f.write_str("missing domain in email address"),
|
||||
Error::CannotParseFilename => f.write_str("could not parse attachment filename"),
|
||||
Error::NonAsciiChars => f.write_str("contains non-ASCII chars"),
|
||||
Error::Io(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
259
src/lib.rs
Normal file
259
src/lib.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
//! Lettre is an email library that allows creating and sending messages. It provides:
|
||||
//!
|
||||
//! * An easy to use email builder
|
||||
//! * Pluggable email transports
|
||||
//! * Unicode support
|
||||
//! * Secure defaults
|
||||
//!
|
||||
//! Lettre requires Rust 1.40 or newer.
|
||||
//!
|
||||
//! ## Optional 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
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-alpha.2")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unstable_features,
|
||||
unused_import_braces,
|
||||
unsafe_code
|
||||
)]
|
||||
|
||||
pub mod address;
|
||||
pub mod error;
|
||||
#[cfg(feature = "builder")]
|
||||
pub mod message;
|
||||
pub mod transport;
|
||||
|
||||
use crate::error::Error;
|
||||
#[cfg(feature = "builder")]
|
||||
pub use crate::message::{
|
||||
header::{self, Headers},
|
||||
EmailFormat, Mailbox, Mailboxes, Message,
|
||||
};
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub use crate::transport::file::FileTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub use crate::transport::sendmail::SendmailTransport;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
||||
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
|
||||
#[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;
|
||||
|
||||
/// 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>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::message::{header, Mailbox, Mailboxes};
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::To(to));
|
||||
|
||||
assert_eq!(
|
||||
Envelope::try_from(&headers).unwrap(),
|
||||
Envelope::new(
|
||||
Some(Address::new("kayo", "example.com").unwrap()),
|
||||
vec![Address::new("amousset", "example.com").unwrap()]
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers_sender() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
|
||||
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));
|
||||
|
||||
assert_eq!(
|
||||
Envelope::try_from(&headers).unwrap(),
|
||||
Envelope::new(
|
||||
Some(Address::new("kayo2", "example.com").unwrap()),
|
||||
vec![Address::new("amousset", "example.com").unwrap()]
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers_no_to() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
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));
|
||||
|
||||
assert!(Envelope::try_from(&headers).is_err(),);
|
||||
}
|
||||
}
|
||||
250
src/message/encoder.rs
Normal file
250
src/message/encoder.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
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, мир!"
|
||||
);
|
||||
}
|
||||
}
|
||||
121
src/message/header/content.rs
Normal file
121
src/message/header/content.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
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},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ContentTransferEncoding {
|
||||
SevenBit,
|
||||
QuotedPrintable,
|
||||
Base64,
|
||||
// 8BITMIME
|
||||
EightBit,
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::SevenBit
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTransferEncoding {
|
||||
fn fmt(&self, f: &mut FmtFormatter) -> FmtResult {
|
||||
use self::ContentTransferEncoding::*;
|
||||
f.write_str(match *self {
|
||||
SevenBit => "7bit",
|
||||
QuotedPrintable => "quoted-printable",
|
||||
Base64 => "base64",
|
||||
EightBit => "8bit",
|
||||
Binary => "binary",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
_ => 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))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ContentTransferEncoding;
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn format_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentTransferEncoding::SevenBit);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: 7bit\r\n"
|
||||
);
|
||||
|
||||
headers.set(ContentTransferEncoding::Base64);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"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.set_raw("Content-Transfer-Encoding", "base64");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::Base64)
|
||||
);
|
||||
}
|
||||
}
|
||||
289
src/message/header/mailbox.rs
Normal file
289
src/message/header/mailbox.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use crate::message::{
|
||||
mailbox::{Mailbox, Mailboxes},
|
||||
utf8_b,
|
||||
};
|
||||
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 {
|
||||
fn join_mailboxes(&mut self, other: Self);
|
||||
}
|
||||
|
||||
macro_rules! mailbox_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailbox);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static 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 fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&self.0.recode_name(utf8_b::encode))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! mailboxes_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailboxes);
|
||||
|
||||
impl MailboxesHeader for $type_name {
|
||||
fn join_mailboxes(&mut self, other: Self) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static 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 fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
format_mailboxes(self.0.iter(), f)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mailbox_header! {
|
||||
/**
|
||||
|
||||
`Sender` header
|
||||
|
||||
This header contains [`Mailbox`][self::Mailbox] associated with sender.
|
||||
|
||||
```no_test
|
||||
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
|
||||
```
|
||||
*/
|
||||
(Sender, "Sender")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`From` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(From, "From")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Reply-To` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(ReplyTo, "Reply-To")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`To` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(To, "To")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Cc` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(Cc, "Cc")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Bcc` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(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;
|
||||
|
||||
#[test]
|
||||
fn format_single_without_name() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_single_with_name() {
|
||||
let from = Mailboxes::new().with("K. <kayo@example.com>".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_multi_without_name() {
|
||||
let from = Mailboxes::new()
|
||||
.with("kayo@example.com".parse().unwrap())
|
||||
.with("pony@domain.tld".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"From: kayo@example.com, pony@domain.tld\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_multi_with_name() {
|
||||
let from = vec![
|
||||
"K. <kayo@example.com>".parse().unwrap(),
|
||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_single_with_utf8_name() {
|
||||
let from = vec!["Кайо <kayo@example.com>".parse().unwrap()];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_without_name() {
|
||||
let from = vec!["kayo@example.com".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "kayo@example.com");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_with_name() {
|
||||
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "K. <kayo@example.com>");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_without_name() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"kayo@example.com".parse().unwrap(),
|
||||
"pony@domain.tld".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "kayo@example.com, pony@domain.tld");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_with_name() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"K. <kayo@example.com>".parse().unwrap(),
|
||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
|
||||
|
||||
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())));
|
||||
}
|
||||
}
|
||||
17
src/message/header/mod.rs
Normal file
17
src/message/header/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
|
||||
## Headers widely used in email messages
|
||||
|
||||
*/
|
||||
|
||||
mod content;
|
||||
mod mailbox;
|
||||
mod special;
|
||||
mod textual;
|
||||
|
||||
pub use self::{content::*, mailbox::*, special::*, textual::*};
|
||||
|
||||
pub use hyperx::header::{
|
||||
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
|
||||
DispositionType, Header, Headers, HttpDate as EmailDate,
|
||||
};
|
||||
83
src/message/header/special.rs
Normal file
83
src/message/header/special.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MimeVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
}
|
||||
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
|
||||
|
||||
impl MimeVersion {
|
||||
pub fn new(major: u8, minor: u8) -> Self {
|
||||
MimeVersion { major, minor }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MimeVersion {
|
||||
fn default() -> Self {
|
||||
MIME_VERSION_1_0
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
#[test]
|
||||
fn format_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(MIME_VERSION_1_0);
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
|
||||
|
||||
headers.set(MimeVersion::new(0, 1));
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("MIME-Version", "1.0");
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0));
|
||||
|
||||
headers.set_raw("MIME-Version", "0.1");
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
|
||||
}
|
||||
}
|
||||
105
src/message/header/textual.rs
Normal file
105
src/message/header/textual.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
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};
|
||||
|
||||
macro_rules! text_header {
|
||||
( $type_name: ident, $header_name: expr ) => {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub String);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static 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 fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
|
||||
fmt_text(&self.0, f)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn fmt_text(s: &str, f: &mut HeaderFormatter) -> FmtResult {
|
||||
f.fmt_line(&utf8_b::encode(s))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Subject;
|
||||
use hyperx::header::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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_utf8() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Тема сообщения".into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[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?=",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Тема сообщения".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
5
src/message/mailbox/mod.rs
Normal file
5
src/message/mailbox/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
mod types;
|
||||
|
||||
pub use self::types::*;
|
||||
215
src/message/mailbox/serde.rs
Normal file
215
src/message/mailbox/serde.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use crate::message::{Mailbox, Mailboxes};
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
impl Serialize for Mailbox {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Mailbox {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
Name,
|
||||
Email,
|
||||
};
|
||||
|
||||
const FIELDS: &[&str] = &["name", "email"];
|
||||
|
||||
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("'name' or 'email'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Field, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
match value {
|
||||
"name" => Ok(Field::Name),
|
||||
"email" => Ok(Field::Email),
|
||||
_ => Err(DeError::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_identifier(FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct MailboxVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MailboxVisitor {
|
||||
type Value = Mailbox;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("mailbox 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 name = None;
|
||||
let mut addr = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::Name => {
|
||||
if name.is_some() {
|
||||
return Err(DeError::duplicate_field("name"));
|
||||
}
|
||||
name = Some(map.next_value()?);
|
||||
}
|
||||
Field::Email => {
|
||||
if addr.is_some() {
|
||||
return Err(DeError::duplicate_field("email"));
|
||||
}
|
||||
addr = Some(map.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
let addr = addr.ok_or_else(|| DeError::missing_field("email"))?;
|
||||
Ok(Mailbox::new(name, addr))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(MailboxVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Mailboxes {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Mailboxes {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct MailboxesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MailboxesVisitor {
|
||||
type Value = Mailboxes;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
|
||||
formatter.write_str("mailboxes string or sequence")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: SeqAccess<'de>,
|
||||
{
|
||||
let mut mboxes = Mailboxes::new();
|
||||
while let Some(mbox) = seq.next_element()? {
|
||||
mboxes.push(mbox);
|
||||
}
|
||||
Ok(mboxes)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(MailboxesVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::address::Address;
|
||||
use serde_json::from_str;
|
||||
|
||||
#[test]
|
||||
fn parse_address_string() {
|
||||
let m: Address = from_str(r#""kayo@example.com""#).unwrap();
|
||||
assert_eq!(m, "kayo@example.com".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_object() {
|
||||
let m: Address = from_str(r#"{ "user": "kayo", "domain": "example.com" }"#).unwrap();
|
||||
assert_eq!(m, "kayo@example.com".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_string() {
|
||||
let m: Mailbox = from_str(r#""Kai <kayo@example.com>""#).unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_object_address_stirng() {
|
||||
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_object_address_object() {
|
||||
let m: Mailbox =
|
||||
from_str(r#"{ "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }"#)
|
||||
.unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailboxes_string() {
|
||||
let m: Mailboxes =
|
||||
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailboxes_array() {
|
||||
let m: Mailboxes =
|
||||
from_str(r#"["yin@dtb.com", { "name": "Hei", "email": "hei@dtb.com" }, { "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }]"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"<yin@dtb.com>, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
319
src/message/mailbox/types.rs
Normal file
319
src/message/mailbox/types.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use crate::{
|
||||
address::{Address, AddressError},
|
||||
message::utf8_b,
|
||||
};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||
slice::Iter,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// Email address with optional addressee name
|
||||
///
|
||||
/// 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/).
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Mailbox {
|
||||
/// User name part
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Email address part
|
||||
pub email: Address,
|
||||
}
|
||||
|
||||
impl Mailbox {
|
||||
/// Create new mailbox using email address and addressee name
|
||||
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 {
|
||||
if let Some(ref name) = self.name {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
f.write_str(&name)?;
|
||||
f.write_str(" <")?;
|
||||
self.email.fmt(f)?;
|
||||
return f.write_char('>');
|
||||
}
|
||||
}
|
||||
self.email.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
|
||||
let (name, address) = header;
|
||||
Ok(Mailbox::new(Some(name.into()), address.into().parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl<S: AsRef<&str>, T: AsRef<&str>> TryFrom<(S, T)> for Mailbox {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
|
||||
let (name, address) = header;
|
||||
Ok(Mailbox::new(Some(name.as_ref()), address.as_ref().parse()?))
|
||||
}
|
||||
}*/
|
||||
|
||||
impl FromStr for Mailbox {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||
match (src.find('<'), src.find('>')) {
|
||||
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => {
|
||||
let name = src.split_at(addr_open).0;
|
||||
let addr_open = addr_open + 1;
|
||||
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
|
||||
let addr = addr.parse()?;
|
||||
let name = name.trim();
|
||||
let name = if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.into())
|
||||
};
|
||||
Ok(Mailbox::new(name, addr))
|
||||
}
|
||||
(Some(_), _) => Err(AddressError::Unbalanced),
|
||||
_ => {
|
||||
let addr = src.parse()?;
|
||||
Ok(Mailbox::new(None, addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List or email mailboxes
|
||||
///
|
||||
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Mailboxes(Vec<Mailbox>);
|
||||
|
||||
impl Mailboxes {
|
||||
/// Create mailboxes list
|
||||
pub fn new() -> Self {
|
||||
Mailboxes(Vec::new())
|
||||
}
|
||||
|
||||
/// Add mailbox to a list
|
||||
pub fn with(mut self, mbox: Mailbox) -> Self {
|
||||
self.0.push(mbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add mailbox to a list
|
||||
pub fn push(&mut self, mbox: Mailbox) {
|
||||
self.0.push(mbox);
|
||||
}
|
||||
|
||||
/// Extract first mailbox
|
||||
pub fn into_single(self) -> Option<Mailbox> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Iterate over mailboxes
|
||||
pub fn iter(&self) -> Iter<Mailbox> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mailboxes {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mailbox> for Mailboxes {
|
||||
fn from(single: Mailbox) -> Self {
|
||||
Mailboxes(vec![single])
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Option<Mailbox>> for Mailboxes {
|
||||
fn into(self) -> Option<Mailbox> {
|
||||
self.into_iter().next()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Mailbox>> for Mailboxes {
|
||||
fn from(list: Vec<Mailbox>) -> Self {
|
||||
Mailboxes(list)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Vec<Mailbox>> for Mailboxes {
|
||||
fn into(self) -> Vec<Mailbox> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Mailboxes {
|
||||
type Item = Mailbox;
|
||||
type IntoIter = ::std::vec::IntoIter<Mailbox>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<Mailbox> for Mailboxes {
|
||||
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
|
||||
for elem in iter {
|
||||
self.0.push(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mailboxes {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
let mut iter = self.iter();
|
||||
|
||||
if let Some(mbox) = iter.next() {
|
||||
mbox.fmt(f)?;
|
||||
|
||||
for mbox in iter {
|
||||
f.write_str(", ")?;
|
||||
mbox.fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mailboxes {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||
src.split(',')
|
||||
.map(|m| {
|
||||
m.trim().parse().and_then(|Mailbox { name, email }| {
|
||||
if let Some(name) = name {
|
||||
if let Some(name) = utf8_b::decode(&name) {
|
||||
Ok(Mailbox::new(Some(name), email))
|
||||
} else {
|
||||
Err(AddressError::InvalidUtf8b)
|
||||
}
|
||||
} else {
|
||||
Ok(Mailbox::new(None, email))
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(Mailboxes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Mailbox;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_only() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(None, "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"kayo@example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_name() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"K. <kayo@example.com>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_address_with_empty_name() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"kayo@example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_address_with_name_trim() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"K. <kayo@example.com>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_only() {
|
||||
assert_eq!(
|
||||
"kayo@example.com".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_name() {
|
||||
assert_eq!(
|
||||
"K. <kayo@example.com>".parse(),
|
||||
Ok(Mailbox::new(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_empty_name() {
|
||||
assert_eq!(
|
||||
"<kayo@example.com>".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_empty_name_trim() {
|
||||
assert_eq!(
|
||||
" <kayo@example.com>".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_from_tuple() {
|
||||
assert_eq!(
|
||||
("K.".to_string(), "kayo@example.com".to_string()).try_into(),
|
||||
Ok(Mailbox::new(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
779
src/message/mimebody.rs
Normal file
779
src/message/mimebody.rs
Normal file
@@ -0,0 +1,779 @@
|
||||
use crate::message::{
|
||||
encoder::codec,
|
||||
header::{ContentTransferEncoding, ContentType, Header, Headers},
|
||||
EmailFormat,
|
||||
};
|
||||
use mime::Mime;
|
||||
use rand::Rng;
|
||||
|
||||
/// MIME part variants
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Part {
|
||||
/// Single part with content
|
||||
///
|
||||
Single(SinglePart),
|
||||
|
||||
/// Multiple parts of content
|
||||
///
|
||||
Multi(MultiPart),
|
||||
}
|
||||
|
||||
impl EmailFormat for Part {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
Part::Single(part) => part.format(out),
|
||||
Part::Multi(part) => part.format(out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl SinglePartBuilder {
|
||||
/// Creates a default singlepart builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the header to singlepart
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Content-Type header of the singlepart
|
||||
pub fn content_type(mut self, content_type: ContentType) -> Self {
|
||||
self.headers.set(content_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build singlepart using body
|
||||
pub fn body<T: Into<Vec<u8>>>(self, body: T) -> SinglePart {
|
||||
SinglePart {
|
||||
headers: self.headers,
|
||||
body: body.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SinglePartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Single part
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::message::{SinglePart, header};
|
||||
///
|
||||
/// let part = SinglePart::builder()
|
||||
/// .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
/// .header(header::ContentTransferEncoding::Binary)
|
||||
/// .body("Текст письма в уникоде");
|
||||
/// ```
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinglePart {
|
||||
headers: Headers,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SinglePart {
|
||||
/// Creates a default builder for singlepart
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Get the headers from singlepart
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Read the body from singlepart
|
||||
pub fn body_ref(&self) -> &[u8] {
|
||||
&self.body
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for SinglePart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
out.extend_from_slice(b"\r\n");
|
||||
|
||||
let encoding = self.headers.get::<ContentTransferEncoding>();
|
||||
let mut encoder = codec(encoding);
|
||||
|
||||
out.extend_from_slice(&encoder.encode(&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
|
||||
///
|
||||
/// For example this kind can be used to mix email message and attachments.
|
||||
Mixed,
|
||||
|
||||
/// Alternative kind to join several variants of same email contents.
|
||||
///
|
||||
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message.
|
||||
Alternative,
|
||||
|
||||
/// Related kind to mix content and related resources.
|
||||
///
|
||||
/// For example, you can include images into HTML content using that.
|
||||
Related,
|
||||
|
||||
/// Encrypted kind for encrypted messages
|
||||
Encrypted { protocol: String },
|
||||
|
||||
/// Signed kind for signed messages
|
||||
Signed { protocol: String, micalg: String },
|
||||
}
|
||||
|
||||
/// Create a random MIME boundary.
|
||||
fn make_boundary() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(68)
|
||||
.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());
|
||||
|
||||
use self::MultiPartKind::*;
|
||||
format!(
|
||||
"multipart/{}; boundary=\"{}\"{}",
|
||||
match self {
|
||||
Mixed => "mixed",
|
||||
Alternative => "alternative",
|
||||
Related => "related",
|
||||
Encrypted { .. } => "encrypted",
|
||||
Signed { .. } => "signed",
|
||||
},
|
||||
boundary,
|
||||
match self {
|
||||
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
|
||||
Signed { protocol, micalg } =>
|
||||
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
|
||||
_ => String::new(),
|
||||
}
|
||||
)
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn from_mime(m: &Mime) -> Option<Self> {
|
||||
use self::MultiPartKind::*;
|
||||
match m.subtype().as_ref() {
|
||||
"mixed" => Some(Mixed),
|
||||
"alternative" => Some(Alternative),
|
||||
"related" => Some(Related),
|
||||
"signed" => m.get_param("protocol").and_then(|p| {
|
||||
m.get_param("micalg").map(|micalg| Signed {
|
||||
protocol: p.as_str().to_owned(),
|
||||
micalg: micalg.as_str().to_owned(),
|
||||
})
|
||||
}),
|
||||
"encrypted" => m.get_param("protocol").map(|p| Encrypted {
|
||||
protocol: p.as_str().to_owned(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl MultiPartBuilder {
|
||||
/// Creates default multipart builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a header
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set `Content-Type` header using [`MultiPartKind`]
|
||||
pub fn kind(self, kind: MultiPartKind) -> Self {
|
||||
self.header(ContentType(kind.into()))
|
||||
}
|
||||
|
||||
/// Set custom boundary
|
||||
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
|
||||
let kind = {
|
||||
let mime = &self.headers.get::<ContentType>().unwrap().0;
|
||||
MultiPartKind::from_mime(mime).unwrap()
|
||||
};
|
||||
let mime = kind.to_mime(Some(boundary.as_ref()));
|
||||
self.header(ContentType(mime))
|
||||
}
|
||||
|
||||
/// Creates multipart without parts
|
||||
pub fn build(self) -> MultiPart {
|
||||
MultiPart {
|
||||
headers: self.headers,
|
||||
parts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Creates multipart using multipart
|
||||
pub fn multipart(self, part: MultiPart) -> MultiPart {
|
||||
self.build().multipart(part)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MultiPartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Multipart variant with parts
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPart {
|
||||
headers: Headers,
|
||||
parts: Parts,
|
||||
}
|
||||
|
||||
impl MultiPart {
|
||||
/// Creates multipart builder
|
||||
pub fn builder() -> MultiPartBuilder {
|
||||
MultiPartBuilder::new()
|
||||
}
|
||||
|
||||
/// Creates mixed multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)`
|
||||
pub fn mixed() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Mixed)
|
||||
}
|
||||
|
||||
/// Creates alternative multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)`
|
||||
pub fn alternative() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Alternative)
|
||||
}
|
||||
|
||||
/// Creates related multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)`
|
||||
pub fn related() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Related)
|
||||
}
|
||||
|
||||
/// Creates encrypted multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Encrypted{ protocol })`
|
||||
pub fn encrypted(protocol: String) -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Encrypted { protocol })
|
||||
}
|
||||
|
||||
/// Creates signed multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Signed{ protocol, micalg })`
|
||||
pub fn signed(protocol: String, micalg: String) -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
|
||||
}
|
||||
|
||||
/// Add part to multipart
|
||||
pub fn part(mut self, part: Part) -> Self {
|
||||
self.parts.push(part);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add single part to multipart
|
||||
pub fn singlepart(mut self, part: SinglePart) -> Self {
|
||||
self.parts.push(Part::Single(part));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multi part to multipart
|
||||
pub fn multipart(mut self, part: MultiPart) -> Self {
|
||||
self.parts.push(Part::Multi(part));
|
||||
self
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Get the headers from the multipart
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers
|
||||
pub fn headers_mut(&mut self) -> &mut Headers {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Get the parts from the multipart
|
||||
pub fn parts(&self) -> &Parts {
|
||||
&self.parts
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the parts
|
||||
pub fn parts_mut(&mut self) -> &mut Parts {
|
||||
&mut self.parts
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for MultiPart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
out.extend_from_slice(b"\r\n");
|
||||
|
||||
let boundary = self.boundary();
|
||||
|
||||
for part in &self.parts {
|
||||
out.extend_from_slice(b"--");
|
||||
out.extend_from_slice(boundary.as_bytes());
|
||||
out.extend_from_slice(b"\r\n");
|
||||
part.format(out);
|
||||
}
|
||||
|
||||
out.extend_from_slice(b"--");
|
||||
out.extend_from_slice(boundary.as_bytes());
|
||||
out.extend_from_slice(b"--\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::message::header;
|
||||
|
||||
#[test]
|
||||
fn single_part_binary() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_part_quoted_printable() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.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-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",
|
||||
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_part_base64() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.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-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[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("Текст письма в уникоде")),
|
||||
))
|
||||
.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::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"));
|
||||
}
|
||||
#[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")),
|
||||
))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType(
|
||||
"application/octet-stream; name=\"encrypted.asc\""
|
||||
.parse()
|
||||
.unwrap(),
|
||||
))
|
||||
.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",
|
||||
"...\r\n",
|
||||
"-----END PGP MESSAGE-----\r\n"
|
||||
))),
|
||||
);
|
||||
|
||||
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"));
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_signed() {
|
||||
let part = MultiPart::signed(
|
||||
"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")),
|
||||
))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType(
|
||||
"application/pgp-signature; name=\"signature.asc\""
|
||||
.parse()
|
||||
.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(),
|
||||
)],
|
||||
})
|
||||
.body(String::from(concat!(
|
||||
"-----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",
|
||||
))),
|
||||
);
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
#[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("Текст письма в уникоде"))))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.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",
|
||||
"\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/html; charset=utf8\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"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed_related() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.multipart(MultiPart::related()
|
||||
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.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::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::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: multipart/related;",
|
||||
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
|
||||
"\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
|
||||
"Content-Type: text/html; charset=utf8\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",
|
||||
"Content-Type: image/png\r\n",
|
||||
"Content-Location: /image.png\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"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",
|
||||
"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"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_boundary() {
|
||||
let mut boundaries = std::collections::HashSet::with_capacity(10);
|
||||
for _ in 0..1000 {
|
||||
boundaries.insert(make_boundary());
|
||||
}
|
||||
|
||||
// Ensure there are no duplicates
|
||||
assert_eq!(1000, boundaries.len());
|
||||
|
||||
// Ensure correct length
|
||||
for boundary in boundaries {
|
||||
assert_eq!(68, boundary.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
547
src/message/mod.rs
Normal file
547
src/message/mod.rs
Normal file
@@ -0,0 +1,547 @@
|
||||
//! Provides a strongly typed way to build emails
|
||||
//!
|
||||
//! ### Creating messages
|
||||
//!
|
||||
//! This section explains how to create emails.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ### Format email messages
|
||||
//!
|
||||
//! #### With string body
|
||||
//!
|
||||
//! The easiest way how we can create email message with simple string.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::Message;
|
||||
//!
|
||||
//! 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! Will produce:
|
||||
//!
|
||||
//! ```sh
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//!
|
||||
//! Be happy!
|
||||
//! ```
|
||||
//!
|
||||
//! The unicode header data will be encoded using _UTF8-Base64_ encoding.
|
||||
//!
|
||||
//! ### With MIME body
|
||||
//!
|
||||
//! ##### Single part
|
||||
//!
|
||||
//! The more complex way is using MIME contents.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::{header, Message, SinglePart, Part};
|
||||
//!
|
||||
//! 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType(
|
||||
//! "text/plain; charset=utf8".parse().unwrap(),
|
||||
//! )).header(header::ContentTransferEncoding::QuotedPrintable)
|
||||
//! .body("Привет, мир!"),
|
||||
//! )
|
||||
//! .unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! The body will be encoded using selected `Content-Transfer-Encoding`.
|
||||
//!
|
||||
//! ```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: text/plain; charset=utf8
|
||||
//! Content-Transfer-Encoding: quoted-printable
|
||||
//!
|
||||
//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
|
||||
//!
|
||||
//! ```
|
||||
//!
|
||||
//! ##### Multiple parts
|
||||
//!
|
||||
//! And more advanced way of building message by using multipart MIME contents.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::{header, Message, MultiPart, SinglePart, Part};
|
||||
//!
|
||||
//! 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())
|
||||
//! .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>")
|
||||
//! )
|
||||
//! )
|
||||
//! )
|
||||
//! .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();
|
||||
//! ```
|
||||
//!
|
||||
//! ```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"
|
||||
//!
|
||||
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
|
||||
//! Content-Type: multipart/alternative; boundary="qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy"
|
||||
//!
|
||||
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
|
||||
//! Content-Transfer-Encoding: quoted-printable
|
||||
//! Content-Type: text/plain; charset=utf8
|
||||
//!
|
||||
//! =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"
|
||||
//!
|
||||
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
|
||||
//! Content-Transfer-Encoding: 8bit
|
||||
//! Content-Type: text/html; charset=utf8
|
||||
//!
|
||||
//! <p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>
|
||||
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
|
||||
//! Content-Transfer-Encoding: base64
|
||||
//! Content-Type: image/png
|
||||
//! Content-Disposition: inline
|
||||
//!
|
||||
//! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg==
|
||||
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8--
|
||||
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy--
|
||||
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//! Content-Type: text/plain; charset=utf8
|
||||
//! Content-Disposition: attachment; filename="example.c"
|
||||
//!
|
||||
//! int main() { return 0; }
|
||||
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m--
|
||||
//!
|
||||
//! ```
|
||||
|
||||
pub use encoder::*;
|
||||
pub use mailbox::*;
|
||||
pub use mimebody::*;
|
||||
|
||||
pub use mime;
|
||||
|
||||
mod encoder;
|
||||
pub mod header;
|
||||
mod mailbox;
|
||||
mod mimebody;
|
||||
mod utf8_b;
|
||||
|
||||
use crate::{
|
||||
message::header::{EmailDate, Header, Headers, MailboxesHeader},
|
||||
Envelope, Error as EmailError,
|
||||
};
|
||||
use std::{convert::TryFrom, time::SystemTime};
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
|
||||
|
||||
pub trait EmailFormat {
|
||||
// Use a writer?
|
||||
fn format(&self, out: &mut Vec<u8>);
|
||||
}
|
||||
|
||||
/// A builder for messages
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessageBuilder {
|
||||
headers: Headers,
|
||||
envelope: Option<Envelope>,
|
||||
}
|
||||
|
||||
impl MessageBuilder {
|
||||
/// Creates a new default message builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
envelope: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set custom header to message
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add mailbox to header
|
||||
pub fn mailbox<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
|
||||
if self.headers.has::<H>() {
|
||||
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
|
||||
self
|
||||
} else {
|
||||
self.header(header)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add `Date` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Date(date))`.
|
||||
pub fn date(self, date: EmailDate) -> Self {
|
||||
self.header(header::Date(date))
|
||||
}
|
||||
|
||||
/// Set `Date` header using current date/time
|
||||
///
|
||||
/// Shortcut for `self.date(SystemTime::now())`.
|
||||
pub fn date_now(self) -> Self {
|
||||
self.date(SystemTime::now().into())
|
||||
}
|
||||
|
||||
/// Set `Subject` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Subject(subject.into()))`.
|
||||
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
|
||||
self.header(header::Subject(subject.into()))
|
||||
}
|
||||
|
||||
/// Set `Mime-Version` header to 1.0
|
||||
///
|
||||
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
|
||||
///
|
||||
/// Not exposed as it is set by body methods
|
||||
fn mime_1_0(self) -> Self {
|
||||
self.header(header::MIME_VERSION_1_0)
|
||||
}
|
||||
|
||||
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
|
||||
///
|
||||
/// Shortcut for `self.header(header::Sender(mbox))`.
|
||||
pub fn sender(self, mbox: Mailbox) -> Self {
|
||||
self.header(header::Sender(mbox))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `From` header
|
||||
///
|
||||
/// 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()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `ReplyTo` header
|
||||
///
|
||||
/// 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 {
|
||||
self.mailbox(header::ReplyTo(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `To` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::To(mbox))`.
|
||||
pub fn to(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::To(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `Cc` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::Cc(mbox))`.
|
||||
pub fn cc(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::Cc(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `Bcc` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::Bcc(mbox))`.
|
||||
pub fn bcc(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::Bcc(mbox.into()))
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// Set [Message-Id
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
///
|
||||
/// Should generally be inserted by the mail relay.
|
||||
///
|
||||
/// If `None` is provided, an id will be generated in the
|
||||
/// `<UUID@HOSTNAME>`.
|
||||
pub fn message_id(self, id: Option<String>) -> Self {
|
||||
match id {
|
||||
Some(i) => self.header(header::MessageId(i)),
|
||||
None => {
|
||||
#[cfg(feature = "hostname")]
|
||||
let hostname = hostname::get()
|
||||
.map_err(|_| ())
|
||||
.and_then(|s| s.into_string().map_err(|_| ()))
|
||||
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string());
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
|
||||
|
||||
self.header(header::MessageId(
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
||||
format!("<{}@{}>", Uuid::new_v4(), hostname),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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))
|
||||
}
|
||||
|
||||
/// Force specific envelope (by default it is derived from headers)
|
||||
pub fn envelope(mut self, envelope: Envelope) -> Self {
|
||||
self.envelope = Some(envelope);
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: High-level methods for attachments and embedded files
|
||||
|
||||
/// Create message from body
|
||||
fn build(self, body: Body) -> 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() {
|
||||
self.date_now()
|
||||
} else {
|
||||
self
|
||||
};
|
||||
|
||||
// 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();
|
||||
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
|
||||
return Err(EmailError::TooManyFrom);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(EmailError::MissingFrom);
|
||||
}
|
||||
}
|
||||
|
||||
let envelope = match res.envelope {
|
||||
Some(e) => e,
|
||||
None => Envelope::try_from(&res.headers)?,
|
||||
};
|
||||
Ok(Message {
|
||||
headers: res.headers,
|
||||
body,
|
||||
envelope,
|
||||
})
|
||||
}
|
||||
|
||||
// In theory having a body is optional
|
||||
|
||||
/// Plain ASCII 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();
|
||||
|
||||
if !&body.is_ascii() {
|
||||
return Err(EmailError::NonAsciiChars);
|
||||
}
|
||||
|
||||
self.build(Body::Raw(body))
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
|
||||
/// 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)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Email message which can be formatted
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Message {
|
||||
headers: Headers,
|
||||
body: Body,
|
||||
envelope: Envelope,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Body {
|
||||
Mime(Part),
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Create a new message builder without headers
|
||||
pub fn builder() -> MessageBuilder {
|
||||
MessageBuilder::new()
|
||||
}
|
||||
|
||||
/// Get the headers from the Message
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get `Message` envelope
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for Message {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
match &self.body {
|
||||
Body::Mime(p) => p.format(out),
|
||||
Body::Raw(r) => {
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend(r.as_bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MessageBuilder {
|
||||
fn default() -> Self {
|
||||
MessageBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::message::{header, mailbox::Mailbox, Message};
|
||||
|
||||
#[test]
|
||||
fn email_missing_originator() {
|
||||
assert!(Message::builder().body("Happy new year!").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_miminal_message() {
|
||||
assert!(Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.body("Happy new year!")
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_missing_sender() {
|
||||
assert!(Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.from("AnyBody <anybody@domain.tld>".parse().unwrap())
|
||||
.body("Happy new year!")
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_message() {
|
||||
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
|
||||
|
||||
let email = Message::builder()
|
||||
.date(date)
|
||||
.header(header::From(
|
||||
vec![Mailbox::new(
|
||||
Some("Каи".into()),
|
||||
"kayo@example.com".parse().unwrap(),
|
||||
)]
|
||||
.into(),
|
||||
))
|
||||
.header(header::To(
|
||||
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
|
||||
))
|
||||
.header(header::Subject("яңа ел белән!".into()))
|
||||
.body("Happy new year!")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(email.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\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",
|
||||
"\r\n",
|
||||
"Happy new year!"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
63
src/message/utf8_b.rs
Normal file
63
src/message/utf8_b.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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,11 +1,10 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use self::Error::*;
|
||||
use serde_json;
|
||||
use std::io;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
@@ -30,7 +29,7 @@ impl Display for Error {
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
JsonSerialization(ref err) => Some(&*err),
|
||||
@@ -56,6 +55,3 @@ impl From<&'static str> for Error {
|
||||
Error::Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type FileResult = Result<(), Error>;
|
||||
201
src/transport/file/mod.rs
Normal file
201
src/transport/file/mod.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
//! The file transport writes the emails to the given directory. The name of the file will be
|
||||
//! `message_id.json`.
|
||||
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 0.2
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "tokio02")]
|
||||
//! # async fn run() {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{Tokio02Transport, 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async async-std 1.x
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "async-std1")]
|
||||
//! # async fn run() {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{AsyncStd1Transport, 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! Example 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!"
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
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 async_trait::async_trait;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod error;
|
||||
|
||||
type Id = String;
|
||||
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
/// Creates a new transport to the given directory
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
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),
|
||||
}),
|
||||
}?;
|
||||
|
||||
Ok((email_id, file, serialized))
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for FileTransport {
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
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)?;
|
||||
|
||||
fs::write(file, serialized)?;
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for FileTransport {
|
||||
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, file, serialized) = self.send_raw_impl(envelope, email)?;
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
25
src/transport/mod.rs
Normal file
25
src/transport/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
//! ### Sending Messages
|
||||
//!
|
||||
//! This section explains how to manipulate emails you have created.
|
||||
//!
|
||||
//! 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`.
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
@@ -1,17 +1,20 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::io;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
Client(String),
|
||||
/// Error parsing UTF8 in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
}
|
||||
@@ -20,15 +23,17 @@ 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -40,11 +45,8 @@ impl From<io::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Error::Client(string)
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// sendmail result type
|
||||
pub type SendmailResult = Result<(), Error>;
|
||||
201
src/transport/sendmail/mod.rs
Normal file
201
src/transport/sendmail/mod.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::{Message, Envelope, Transport, 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 0.2 example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "tokio02")]
|
||||
//! # async fn run() {
|
||||
//! use lettre::{Message, Envelope, Tokio02Transport, 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async async-std 1.x example
|
||||
//!
|
||||
//!```rust
|
||||
//! # #[cfg(feature = "async-std1")]
|
||||
//! # async fn run() {
|
||||
//! use lettre::{Message, Envelope, AsyncStd1Transport, 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_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 async_trait::async_trait;
|
||||
use std::{
|
||||
convert::AsRef,
|
||||
ffi::OsString,
|
||||
io::prelude::*,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
mod error;
|
||||
|
||||
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
|
||||
|
||||
/// Sends an email using the `sendmail` command
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SendmailTransport {
|
||||
command: OsString,
|
||||
}
|
||||
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: DEFAUT_SENDMAIL.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given sendmail command
|
||||
pub fn new_with_command<S: Into<OsString>>(command: S) -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: command.into(),
|
||||
}
|
||||
}
|
||||
|
||||
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("\"\""))
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped());
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
|
||||
use tokio02_crate::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("\"\""))
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped());
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
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()?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(email)?;
|
||||
let output = process.wait_with_output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error::Error::Client(String::from_utf8(output.stderr)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for SendmailTransport {
|
||||
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();
|
||||
|
||||
// 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);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email).await?;
|
||||
let output = process.wait_with_output().await?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
243
src/transport/smtp/async_transport.rs
Normal file
243
src/transport/smtp/async_transport.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
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};
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncSmtpTransport<C> {
|
||||
// TODO: pool
|
||||
inner: AsyncSmtpClient<C>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
|
||||
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<C> AsyncSmtpTransport<C>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
{
|
||||
/// Simple and secure transport, using TLS connections to comunicate 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"))]
|
||||
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{TlsParameters, SUBMISSIONS_PORT};
|
||||
|
||||
let tls_parameters = TlsParameters::new_tokio02(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSIONS_PORT)
|
||||
.tls(Tls::Wrapper(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
|
||||
///
|
||||
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
|
||||
/// that don't take SMTPS connections.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, by first connecting using
|
||||
/// an unencrypted connection and then upgrading it with STARTTLS. The provided
|
||||
/// domain is used to validate TLS certificates.
|
||||
///
|
||||
/// 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"))]
|
||||
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{TlsParameters, SUBMISSION_PORT};
|
||||
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSION_PORT)
|
||||
.tls(Tls::Required(tls_parameters)))
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
Self::builder_dangerous("localhost").build()
|
||||
}
|
||||
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
||||
let mut new = SmtpInfo::default();
|
||||
new.server = server.into();
|
||||
AsyncSmtpTransportBuilder { info: new }
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncSmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `AsyncSmtpTransport`
|
||||
impl AsyncSmtpTransportBuilder {
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
|
||||
self.info.authentication = mechanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the port to use
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.info.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport (with default pool if enabled)
|
||||
pub fn build<C>(self) -> AsyncSmtpTransport<C>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
{
|
||||
let connector = Default::default();
|
||||
let client = AsyncSmtpClient {
|
||||
connector,
|
||||
info: self.info,
|
||||
};
|
||||
AsyncSmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncSmtpClient<C> {
|
||||
connector: C,
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
impl<C> AsyncSmtpClient<C>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
{
|
||||
/// 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(
|
||||
&self.info.server,
|
||||
self.info.port,
|
||||
&self.info.hello_name,
|
||||
&self.info.tls,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, &credentials).await?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl Sealed for Tokio02Connector {}
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
//! Provides limited SASL authentication mechanisms
|
||||
|
||||
use smtp::error::Error;
|
||||
use crate::transport::smtp::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Accepted authentication mechanisms on an encrypted connection
|
||||
/// Accepted authentication mechanisms
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Accepted authentication mechanisms on an unencrypted connection
|
||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
|
||||
|
||||
/// Convertable to user credentials
|
||||
/// Convertible to user credentials
|
||||
pub trait IntoCredentials {
|
||||
/// Converts to a `Credentials` struct
|
||||
fn into_credentials(self) -> Credentials;
|
||||
@@ -31,7 +28,7 @@ impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
|
||||
|
||||
/// Contains user credentials
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Credentials {
|
||||
authentication_identity: String,
|
||||
secret: String,
|
||||
@@ -49,7 +46,7 @@ impl Credentials {
|
||||
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Mechanism {
|
||||
/// PLAIN authentication mechanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
@@ -65,15 +62,11 @@ pub enum Mechanism {
|
||||
|
||||
impl Display for Mechanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
Mechanism::Xoauth2 => "XOAUTH2",
|
||||
}
|
||||
)
|
||||
f.write_str(match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
Mechanism::Xoauth2 => "XOAUTH2",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
269
src/transport/smtp/client/async_connection.rs
Normal file
269
src/transport/smtp/client/async_connection.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
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,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
},
|
||||
Envelope,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
$client.abort().await;
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct AsyncSmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: BufReader<AsyncNetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
/// Information about the server
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
impl AsyncSmtpConnection {
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&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(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_tokio02(hostname, port, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
async fn connect_impl(
|
||||
stream: AsyncNetworkStream,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = BufReader::new(stream);
|
||||
let mut conn = AsyncSmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
// TODO log
|
||||
let _response = conn.read_response().await?;
|
||||
|
||||
conn.ehlo(hello_name).await?;
|
||||
|
||||
// Print server information
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("server {}", conn.server_info);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options))
|
||||
.await,
|
||||
self
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(
|
||||
self.command(Rcpt::new(to_address.clone(), vec![])).await,
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.command(Data).await, self);
|
||||
|
||||
// Message content
|
||||
let result = try_smtp!(self.message(email).await, self);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn has_broken(&self) -> bool {
|
||||
self.panic
|
||||
}
|
||||
|
||||
pub fn can_starttls(&self) -> bool {
|
||||
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub async fn starttls(
|
||||
&mut self,
|
||||
tls_parameters: TlsParameters,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
try_smtp!(self.command(Starttls).await, self);
|
||||
try_smtp!(
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters).await,
|
||||
self
|
||||
);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name).await, self);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send EHLO and update server info
|
||||
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self);
|
||||
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn quit(&mut self) -> Result<Response, Error> {
|
||||
Ok(try_smtp!(self.command(Quit).await, self))
|
||||
}
|
||||
|
||||
pub async fn abort(&mut self) {
|
||||
// Only try to quit if we are not already broken
|
||||
if !self.panic {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: AsyncNetworkStream) {
|
||||
self.stream = BufReader::new(stream);
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.stream.get_ref().is_encrypted()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
pub async fn test_connected(&mut self) -> bool {
|
||||
self.command(Noop).await.is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub async fn auth(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
credentials: &Credentials,
|
||||
) -> Result<Response, Error> {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or(Error::Client(
|
||||
"No compatible authentication mechanism was found",
|
||||
))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
let mut response = self
|
||||
.command(Auth::new(mechanism, credentials.clone(), None)?)
|
||||
.await?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)
|
||||
.await,
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub async fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
codec.encode(message, &mut out_buf);
|
||||
self.write(out_buf.as_slice()).await?;
|
||||
self.write(b"\r\n.\r\n").await?;
|
||||
self.read_response().await
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub async fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
|
||||
self.write(command.to_string().as_bytes()).await?;
|
||||
self.read_response().await
|
||||
}
|
||||
|
||||
/// 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?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
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 {
|
||||
#[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());
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(Error::Parsing(e.1));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(Error::Parsing(e.1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
|
||||
}
|
||||
}
|
||||
244
src/transport/smtp/client/async_net.rs
Normal file
244
src/transport/smtp/client/async_net.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
net::{Shutdown, 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;
|
||||
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
use tokio02_native_tls_crate::TlsStream;
|
||||
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
use tokio02_rustls::client::TlsStream as RustlsTlsStream;
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
/// A network stream
|
||||
pub struct AsyncNetworkStream {
|
||||
inner: InnerAsyncNetworkStream,
|
||||
}
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
#[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>>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
impl AsyncNetworkStream {
|
||||
fn new(inner: InnerAsyncNetworkStream) -> Self {
|
||||
if let InnerAsyncNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
}
|
||||
|
||||
AsyncNetworkStream { inner }
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
"InnerAsyncNetworkStream::None should 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(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = TcpStream::connect((hostname, port)).await?;
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
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(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
async fn upgrade_tokio02_tls(
|
||||
tcp_stream: TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = std::mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
#[cfg(not(feature = "tokio02-native-tls"))]
|
||||
panic!("built without the tokio02-native-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
return {
|
||||
use tokio02_native_tls_crate::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(connector);
|
||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "tokio02-rustls-tls"))]
|
||||
panic!("built without the tokio02-rustls-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
return {
|
||||
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = connector.connect(domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(Box::new(stream)))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
InnerAsyncNetworkStream::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl futures_io::AsyncRead for AsyncNetworkStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
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),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should 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>> {
|
||||
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),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None should never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<IoResult<()>> {
|
||||
Poll::Ready(self.shutdown(Shutdown::Write))
|
||||
}
|
||||
}
|
||||
269
src/transport/smtp/client/connection.rs
Normal file
269
src/transport/smtp/client/connection.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
net::ToSocketAddrs,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::{ClientCodec, NetworkStream, TlsParameters};
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::*,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
},
|
||||
Envelope,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
$client.abort();
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct SmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: BufReader<NetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
/// Information about the server
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
impl SmtpConnection {
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
// FIXME add simple connect and rename this one
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
server: A,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
) -> Result<SmtpConnection, Error> {
|
||||
let stream = NetworkStream::connect(server, timeout, tls_parameters)?;
|
||||
let stream = BufReader::new(stream);
|
||||
let mut conn = SmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
conn.set_timeout(timeout)?;
|
||||
// TODO log
|
||||
let _response = conn.read_response()?;
|
||||
|
||||
conn.ehlo(hello_name)?;
|
||||
|
||||
// Print server information
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("server {}", conn.server_info);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options)),
|
||||
self
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.command(Data), self);
|
||||
|
||||
// Message content
|
||||
let result = try_smtp!(self.message(email), self);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn has_broken(&self) -> bool {
|
||||
self.panic
|
||||
}
|
||||
|
||||
pub fn can_starttls(&self) -> bool {
|
||||
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn starttls(
|
||||
&mut self,
|
||||
tls_parameters: &TlsParameters,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
{
|
||||
try_smtp!(self.command(Starttls), self);
|
||||
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name), self);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
// This should never happen as `Tls` can only be created
|
||||
// when a TLS library is enabled
|
||||
unreachable!("TLS support required but not supported");
|
||||
} else {
|
||||
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send EHLO and update server info
|
||||
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
|
||||
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) -> Result<Response, Error> {
|
||||
Ok(try_smtp!(self.command(Quit), self))
|
||||
}
|
||||
|
||||
pub fn abort(&mut self) {
|
||||
// Only try to quit if we are not already broken
|
||||
if !self.panic {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: NetworkStream) {
|
||||
self.stream = BufReader::new(stream);
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.stream.get_ref().is_encrypted()
|
||||
}
|
||||
|
||||
/// Set timeout
|
||||
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
self.stream.get_mut().set_read_timeout(duration)?;
|
||||
self.stream.get_mut().set_write_timeout(duration)
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
pub fn test_connected(&mut self) -> bool {
|
||||
self.command(Noop).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub fn auth(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
credentials: &Credentials,
|
||||
) -> Result<Response, Error> {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or(Error::Client(
|
||||
"No compatible authentication mechanism was found",
|
||||
))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?),
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
codec.encode(message, &mut out_buf);
|
||||
self.write(out_buf.as_slice())?;
|
||||
self.write(b"\r\n.\r\n")?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
|
||||
self.write(command.to_string().as_bytes())?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// 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()?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
pub fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer)? > 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());
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(Error::Parsing(e.1));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(Error::Parsing(e.1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
#![allow(missing_docs)]
|
||||
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
|
||||
|
||||
use std::io::{self, Cursor, Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Write},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
pub type MockCursor = Cursor<Vec<u8>>;
|
||||
|
||||
@@ -88,7 +90,7 @@ mod test {
|
||||
fn write_take_test() {
|
||||
let mut mock = MockStream::new();
|
||||
// write to mock stream
|
||||
mock.write(&[1, 2, 3]).unwrap();
|
||||
mock.write_all(&[1, 2, 3]).unwrap();
|
||||
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
@@ -104,7 +106,7 @@ mod test {
|
||||
fn clone_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut cloned = mock.clone();
|
||||
mock.write(&[6, 7]).unwrap();
|
||||
mock.write_all(&[6, 7]).unwrap();
|
||||
assert_eq!(cloned.take_vec(), vec![6, 7]);
|
||||
}
|
||||
|
||||
@@ -112,7 +114,7 @@ mod test {
|
||||
fn swap_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut vec = Vec::new();
|
||||
mock.write(&[8, 9, 10]).unwrap();
|
||||
mock.write_all(&[8, 9, 10]).unwrap();
|
||||
mock.swap();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![8, 9, 10]);
|
||||
137
src/transport/smtp/client/mod.rs
Normal file
137
src/transport/smtp/client/mod.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
//! SMTP client
|
||||
//!
|
||||
//! `SmtpConnection` allows manually sending SMTP commands.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(feature = "smtp-transport")]
|
||||
//! # {
|
||||
//! 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();
|
||||
//! client.command(
|
||||
//! Mail::new(Some("user@example.com".parse().unwrap()), vec![])
|
||||
//! ).unwrap();
|
||||
//! 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();
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub(crate) use self::async_connection::AsyncSmtpConnection;
|
||||
#[cfg(feature = "tokio02")]
|
||||
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},
|
||||
};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
mod async_connection;
|
||||
#[cfg(feature = "tokio02")]
|
||||
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 {
|
||||
escape_count: u8,
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub fn new() -> Self {
|
||||
ClientCodec::default()
|
||||
}
|
||||
|
||||
/// Adds transparency
|
||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
|
||||
match frame.len() {
|
||||
0 => {
|
||||
match self.escape_count {
|
||||
0 => buf.extend_from_slice(b"\r\n.\r\n"),
|
||||
1 => buf.extend_from_slice(b"\n.\r\n"),
|
||||
2 => buf.extend_from_slice(b".\r\n"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.escape_count = 0;
|
||||
}
|
||||
_ => {
|
||||
let mut start = 0;
|
||||
for (idx, byte) in frame.iter().enumerate() {
|
||||
match self.escape_count {
|
||||
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
|
||||
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
|
||||
2 => self.escape_count = if *byte == b'.' { 3 } else { 0 },
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if self.escape_count == 3 {
|
||||
self.escape_count = 0;
|
||||
buf.extend_from_slice(&frame[start..idx]);
|
||||
buf.extend_from_slice(b".");
|
||||
start = idx;
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(&frame[start..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||
/// Used for debug displays
|
||||
#[cfg(feature = "tracing")]
|
||||
pub(super) fn escape_crlf(string: &str) -> String {
|
||||
string.replace("\r\n", "<CRLF>")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_codec() {
|
||||
let mut codec = ClientCodec::new();
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
|
||||
codec.encode(b"test\r\n", &mut buf);
|
||||
codec.encode(b".\r\n", &mut buf);
|
||||
codec.encode(b"\r\ntest", &mut buf);
|
||||
codec.encode(b"te\r\n.\r\nst", &mut buf);
|
||||
codec.encode(b"test", &mut buf);
|
||||
codec.encode(b"test.", &mut buf);
|
||||
codec.encode(b"test\n", &mut buf);
|
||||
codec.encode(b".test\n", &mut buf);
|
||||
codec.encode(b"test", &mut buf);
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "log")]
|
||||
fn test_escape_crlf() {
|
||||
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
|
||||
assert_eq!(
|
||||
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CRLF>SIZE 42<CRLF>"
|
||||
);
|
||||
}
|
||||
}
|
||||
237
src/transport/smtp/client/net.rs
Normal file
237
src/transport/smtp/client/net.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsStream;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::{ClientSession, StreamOwned};
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::{MockStream, TlsParameters};
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
/// A network stream
|
||||
pub struct NetworkStream {
|
||||
inner: InnerNetworkStream,
|
||||
}
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
enum InnerNetworkStream {
|
||||
/// Plain TCP stream
|
||||
Tcp(TcpStream),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsStream<TcpStream>),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(Box<StreamOwned<ClientSession, TcpStream>>),
|
||||
/// Mock stream
|
||||
Mock(MockStream),
|
||||
}
|
||||
|
||||
impl NetworkStream {
|
||||
fn new(inner: InnerNetworkStream) -> Self {
|
||||
NetworkStream { inner }
|
||||
}
|
||||
|
||||
pub fn new_mock(mock: MockStream) -> Self {
|
||||
Self::new(InnerNetworkStream::Mock(mock))
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
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,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdowns the connection
|
||||
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
|
||||
#[cfg(feature = "native-tls")]
|
||||
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(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect<T: ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
) -> Result<NetworkStream, Error> {
|
||||
fn try_connect_timeout<T: ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
) -> Result<TcpStream, Error> {
|
||||
let addrs = server.to_socket_addrs()?;
|
||||
for addr in addrs {
|
||||
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Err(Error::Client("Could not connect"))
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
Some(t) => try_connect_timeout(server, t)?,
|
||||
None => TcpStream::connect(server)?,
|
||||
};
|
||||
|
||||
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters)?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream =
|
||||
std::mem::replace(&mut self.inner, InnerNetworkStream::Mock(MockStream::new()));
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
fn upgrade_tls_impl(
|
||||
tcp_stream: TcpStream,
|
||||
tls_parameters: &TlsParameters,
|
||||
) -> Result<InnerNetworkStream, Error> {
|
||||
Ok(match &tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
let stream = connector
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(|err| Error::Io(io::Error::new(io::ErrorKind::Other, err)))?;
|
||||
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,
|
||||
);
|
||||
|
||||
InnerNetworkStream::RustlsTls(Box::new(stream))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(_) | InnerNetworkStream::Mock(_) => false,
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => true,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set write timeout for IO calls
|
||||
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.read(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.write(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.flush(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/transport/smtp/client/tls.rs
Normal file
98
src/transport/smtp/client/tls.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use crate::transport::smtp::error::Error;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::ClientConfig;
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
// This is also rustls' default behavior
|
||||
#[cfg(feature = "native-tls")]
|
||||
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
pub enum Tls {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
None,
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Opportunistic(TlsParameters),
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Required(TlsParameters),
|
||||
/// Use TLS wrapped connection
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Wrapper(TlsParameters),
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum InnerTlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsConnector),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(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"))]
|
||||
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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls
|
||||
#[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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls
|
||||
#[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,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn domain(&self) -> &str {
|
||||
&self.domain
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,45 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||
|
||||
//! SMTP commands
|
||||
|
||||
use base64;
|
||||
use smtp::authentication::{Credentials, Mechanism};
|
||||
use smtp::error::Error;
|
||||
use smtp::extension::ClientId;
|
||||
use smtp::extension::{MailParameter, RcptParameter};
|
||||
use smtp::response::Response;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use EmailAddress;
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
error::Error,
|
||||
extension::{ClientId, MailParameter, RcptParameter},
|
||||
response::Response,
|
||||
},
|
||||
Address,
|
||||
};
|
||||
use std::{
|
||||
convert::AsRef,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct EhloCommand {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Ehlo {
|
||||
client_id: ClientId,
|
||||
}
|
||||
|
||||
impl Display for EhloCommand {
|
||||
impl Display for Ehlo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "EHLO {}\r\n", self.client_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl EhloCommand {
|
||||
impl Ehlo {
|
||||
/// Creates a EHLO command
|
||||
pub fn new(client_id: ClientId) -> EhloCommand {
|
||||
EhloCommand { client_id }
|
||||
pub fn new(client_id: ClientId) -> Ehlo {
|
||||
Ehlo { client_id }
|
||||
}
|
||||
}
|
||||
|
||||
/// STARTTLS command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct StarttlsCommand;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Starttls;
|
||||
|
||||
impl Display for StarttlsCommand {
|
||||
impl Display for Starttls {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("STARTTLS\r\n")
|
||||
}
|
||||
@@ -44,18 +47,18 @@ impl Display for StarttlsCommand {
|
||||
|
||||
/// MAIL command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct MailCommand {
|
||||
sender: Option<EmailAddress>,
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Mail {
|
||||
sender: Option<Address>,
|
||||
parameters: Vec<MailParameter>,
|
||||
}
|
||||
|
||||
impl Display for MailCommand {
|
||||
impl Display for Mail {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
|
||||
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
@@ -64,22 +67,22 @@ impl Display for MailCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl MailCommand {
|
||||
impl Mail {
|
||||
/// Creates a MAIL command
|
||||
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
|
||||
MailCommand { sender, parameters }
|
||||
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
|
||||
Mail { sender, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
/// RCPT command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct RcptCommand {
|
||||
recipient: EmailAddress,
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rcpt {
|
||||
recipient: Address,
|
||||
parameters: Vec<RcptParameter>,
|
||||
}
|
||||
|
||||
impl Display for RcptCommand {
|
||||
impl Display for Rcpt {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||
for parameter in &self.parameters {
|
||||
@@ -89,10 +92,10 @@ impl Display for RcptCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl RcptCommand {
|
||||
impl Rcpt {
|
||||
/// Creates an RCPT command
|
||||
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
|
||||
RcptCommand {
|
||||
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
|
||||
Rcpt {
|
||||
recipient,
|
||||
parameters,
|
||||
}
|
||||
@@ -101,10 +104,10 @@ impl RcptCommand {
|
||||
|
||||
/// DATA command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct DataCommand;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Data;
|
||||
|
||||
impl Display for DataCommand {
|
||||
impl Display for Data {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("DATA\r\n")
|
||||
}
|
||||
@@ -112,10 +115,10 @@ impl Display for DataCommand {
|
||||
|
||||
/// QUIT command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct QuitCommand;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Quit;
|
||||
|
||||
impl Display for QuitCommand {
|
||||
impl Display for Quit {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("QUIT\r\n")
|
||||
}
|
||||
@@ -123,10 +126,10 @@ impl Display for QuitCommand {
|
||||
|
||||
/// NOOP command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct NoopCommand;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Noop;
|
||||
|
||||
impl Display for NoopCommand {
|
||||
impl Display for Noop {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("NOOP\r\n")
|
||||
}
|
||||
@@ -134,75 +137,74 @@ impl Display for NoopCommand {
|
||||
|
||||
/// HELP command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct HelpCommand {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Help {
|
||||
argument: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for HelpCommand {
|
||||
impl Display for Help {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("HELP")?;
|
||||
if self.argument.is_some() {
|
||||
write!(f, " {}", self.argument.as_ref().unwrap())?;
|
||||
if let Some(argument) = &self.argument {
|
||||
write!(f, " {}", argument)?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl HelpCommand {
|
||||
impl Help {
|
||||
/// Creates an HELP command
|
||||
pub fn new(argument: Option<String>) -> HelpCommand {
|
||||
HelpCommand { argument }
|
||||
pub fn new(argument: Option<String>) -> Help {
|
||||
Help { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// VRFY command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct VrfyCommand {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Vrfy {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for VrfyCommand {
|
||||
impl Display for Vrfy {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||
write!(f, "VRFY {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl VrfyCommand {
|
||||
impl Vrfy {
|
||||
/// Creates a VRFY command
|
||||
pub fn new(argument: String) -> VrfyCommand {
|
||||
VrfyCommand { argument }
|
||||
pub fn new(argument: String) -> Vrfy {
|
||||
Vrfy { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPN command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct ExpnCommand {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Expn {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for ExpnCommand {
|
||||
impl Display for Expn {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "EXPN {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExpnCommand {
|
||||
impl Expn {
|
||||
/// Creates an EXPN command
|
||||
pub fn new(argument: String) -> ExpnCommand {
|
||||
ExpnCommand { argument }
|
||||
pub fn new(argument: String) -> Expn {
|
||||
Expn { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// RSET command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct RsetCommand;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rset;
|
||||
|
||||
impl Display for RsetCommand {
|
||||
impl Display for Rset {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("RSET\r\n")
|
||||
}
|
||||
@@ -210,20 +212,17 @@ impl Display for RsetCommand {
|
||||
|
||||
/// AUTH command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct AuthCommand {
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Auth {
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
response: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for AuthCommand {
|
||||
impl Display for Auth {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let encoded_response = self
|
||||
.response
|
||||
.as_ref()
|
||||
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
|
||||
let encoded_response = self.response.as_ref().map(base64::encode);
|
||||
|
||||
if self.mechanism.supports_initial_response() {
|
||||
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
||||
@@ -237,19 +236,19 @@ impl Display for AuthCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthCommand {
|
||||
impl Auth {
|
||||
/// Creates an AUTH command (from a challenge if provided)
|
||||
pub fn new(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
) -> Result<Auth, Error> {
|
||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||
Some(mechanism.response(&credentials, challenge.as_deref())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(AuthCommand {
|
||||
Ok(Auth {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge,
|
||||
@@ -263,7 +262,7 @@ impl AuthCommand {
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
response: &Response,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
) -> Result<Auth, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
}
|
||||
@@ -271,14 +270,16 @@ impl AuthCommand {
|
||||
let encoded_challenge = response
|
||||
.first_word()
|
||||
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
|
||||
debug!("auth encoded challenge: {}", encoded_challenge);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
||||
|
||||
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
|
||||
debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||
|
||||
Ok(AuthCommand {
|
||||
Ok(Auth {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge: Some(decoded_challenge),
|
||||
@@ -290,12 +291,13 @@ impl AuthCommand {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use smtp::extension::MailBodyParameter;
|
||||
use crate::transport::smtp::extension::MailBodyParameter;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let id = ClientId::Domain("localhost".to_string());
|
||||
let email = EmailAddress::new("test@example.com".to_string()).unwrap();
|
||||
let email = Address::from_str("test@example.com").unwrap();
|
||||
let mail_parameter = MailParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
@@ -304,26 +306,23 @@ mod test {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
};
|
||||
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
|
||||
format!("{}", Mail::new(Some(email.clone()), vec![])),
|
||||
"MAIL FROM:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(None, vec![])),
|
||||
"MAIL FROM:<>\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
||||
Mail::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::new(
|
||||
Mail::new(
|
||||
Some(email.clone()),
|
||||
vec![
|
||||
MailParameter::Size(42),
|
||||
@@ -335,42 +334,42 @@ mod test {
|
||||
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", RcptCommand::new(email.clone(), vec![])),
|
||||
format!("{}", Rcpt::new(email.clone(), vec![])),
|
||||
"RCPT TO:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
|
||||
format!("{}", Rcpt::new(email.clone(), vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
|
||||
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
|
||||
assert_eq!(format!("{}", Quit), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", Data), "DATA\r\n");
|
||||
assert_eq!(format!("{}", Noop), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", HelpCommand::new(Some("test".to_string()))),
|
||||
format!("{}", Help::new(Some("test".to_string()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", VrfyCommand::new("test".to_string())),
|
||||
format!("{}", Vrfy::new("test".to_string())),
|
||||
"VRFY test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", ExpnCommand::new("test".to_string())),
|
||||
format!("{}", Expn::new("test".to_string())),
|
||||
"EXPN test\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", RsetCommand), "RSET\r\n");
|
||||
assert_eq!(format!("{}", Rset), "RSET\r\n");
|
||||
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||
Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH LOGIN\r\n"
|
||||
);
|
||||
@@ -1,15 +1,14 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use self::Error::*;
|
||||
use crate::transport::smtp::response::{Response, Severity};
|
||||
use base64::DecodeError;
|
||||
use native_tls;
|
||||
use nom;
|
||||
use smtp::response::{Response, Severity};
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
use std::string::FromUtf8Error;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
@@ -35,9 +34,15 @@ pub enum Error {
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// TLS error
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(native_tls::Error),
|
||||
/// Parsing error
|
||||
Parsing(nom::ErrorKind),
|
||||
Parsing(nom::error::ErrorKind),
|
||||
/// Invalid hostname
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InvalidDNSName(webpki::InvalidDNSNameError),
|
||||
#[cfg(feature = "r2d2")]
|
||||
Pool(r2d2::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
@@ -59,18 +64,24 @@ impl Display for Error {
|
||||
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 cause(&self) -> Option<&dyn StdError> {
|
||||
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),
|
||||
_ => None,
|
||||
}
|
||||
@@ -83,15 +94,20 @@ impl From<io::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
impl From<native_tls::Error> for Error {
|
||||
fn from(err: native_tls::Error) -> Error {
|
||||
Tls(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::ErrorKind> for Error {
|
||||
fn from(err: nom::ErrorKind) -> Error {
|
||||
Parsing(err)
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +123,20 @@ impl From<FromUtf8Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
impl From<webpki::InvalidDNSNameError> for Error {
|
||||
fn from(err: webpki::InvalidDNSNameError) -> Error {
|
||||
InvalidDNSName(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
impl From<r2d2::Error> for Error {
|
||||
fn from(err: r2d2::Error) -> Error {
|
||||
Pool(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for Error {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.code.severity {
|
||||
@@ -122,6 +152,3 @@ impl From<&'static str> for Error {
|
||||
Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type SmtpResult = Result<Response, Error>;
|
||||
@@ -1,21 +1,19 @@
|
||||
//! ESMTP features
|
||||
|
||||
use hostname::get_hostname;
|
||||
use smtp::authentication::Mechanism;
|
||||
use smtp::error::Error;
|
||||
use smtp::response::Response;
|
||||
use smtp::util::XText;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::result::Result;
|
||||
|
||||
/// Default client id
|
||||
pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism, error::Error, response::Response, util::XText,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{self, Display, Formatter},
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
result::Result,
|
||||
};
|
||||
|
||||
/// Client identifier, the parameter to `EHLO`
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum ClientId {
|
||||
/// A fully-qualified domain name
|
||||
Domain(String),
|
||||
@@ -25,32 +23,51 @@ pub enum ClientId {
|
||||
Ipv6(Ipv6Addr),
|
||||
}
|
||||
|
||||
const LOCALHOST_CLIENT: ClientId = ClientId::Ipv4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
impl Default for ClientId {
|
||||
fn default() -> Self {
|
||||
// https://tools.ietf.org/html/rfc5321#section-4.1.4
|
||||
//
|
||||
// The SMTP client MUST, if possible, ensure that the domain parameter
|
||||
// to the EHLO command is a primary host name as specified for this
|
||||
// command in Section 2.3.5. If this is not possible (e.g., when the
|
||||
// client's address is dynamically assigned and the client does not have
|
||||
// an obvious name), an address literal SHOULD be substituted for the
|
||||
// domain name.
|
||||
#[cfg(feature = "hostname")]
|
||||
{
|
||||
hostname::get()
|
||||
.ok()
|
||||
.and_then(|s| s.into_string().map(Self::Domain).ok())
|
||||
.unwrap_or(LOCALHOST_CLIENT)
|
||||
}
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
LOCALHOST_CLIENT
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ClientId {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ClientId::Domain(ref value) => f.write_str(value),
|
||||
ClientId::Ipv4(ref value) => write!(f, "{}", value),
|
||||
ClientId::Ipv6(ref value) => write!(f, "{}", value),
|
||||
Self::Domain(ref value) => f.write_str(value),
|
||||
Self::Ipv4(ref value) => write!(f, "[{}]", value),
|
||||
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientId {
|
||||
#[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) -> ClientId {
|
||||
ClientId::Domain(domain)
|
||||
}
|
||||
|
||||
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
|
||||
/// found
|
||||
pub fn hostname() -> ClientId {
|
||||
ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
|
||||
pub fn new(domain: String) -> Self {
|
||||
Self::Domain(domain)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Extension {
|
||||
/// 8BITMIME keyword
|
||||
///
|
||||
@@ -71,17 +88,17 @@ pub enum Extension {
|
||||
impl Display for Extension {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Extension::EightBitMime => write!(f, "8BITMIME"),
|
||||
Extension::SmtpUtfEight => write!(f, "SMTPUTF8"),
|
||||
Extension::StartTls => write!(f, "STARTTLS"),
|
||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
Extension::StartTls => f.write_str("STARTTLS"),
|
||||
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains information about an SMTP server
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ServerInfo {
|
||||
/// Server name
|
||||
///
|
||||
@@ -95,16 +112,12 @@ pub struct ServerInfo {
|
||||
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} with {}",
|
||||
self.name,
|
||||
if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
}
|
||||
)
|
||||
let features = if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
};
|
||||
write!(f, "{} with {}", self.name, features)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,8 +136,8 @@ impl ServerInfo {
|
||||
continue;
|
||||
}
|
||||
|
||||
let split: Vec<&str> = line.split_whitespace().collect();
|
||||
match split[0] {
|
||||
let mut split = line.split_whitespace();
|
||||
match split.next().unwrap() {
|
||||
"8BITMIME" => {
|
||||
features.insert(Extension::EightBitMime);
|
||||
}
|
||||
@@ -135,7 +148,7 @@ impl ServerInfo {
|
||||
features.insert(Extension::StartTls);
|
||||
}
|
||||
"AUTH" => {
|
||||
for &mechanism in &split[1..] {
|
||||
for mechanism in split {
|
||||
match mechanism {
|
||||
"PLAIN" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Plain));
|
||||
@@ -170,11 +183,21 @@ impl ServerInfo {
|
||||
self.features
|
||||
.contains(&Extension::Authentication(mechanism))
|
||||
}
|
||||
|
||||
/// Gets a compatible mechanism from list
|
||||
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
|
||||
for mechanism in mechanisms {
|
||||
if self.supports_auth_mechanism(*mechanism) {
|
||||
return Some(*mechanism);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A `MAIL FROM` extension parameter
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum MailParameter {
|
||||
/// `BODY` parameter
|
||||
Body(MailBodyParameter),
|
||||
@@ -211,7 +234,7 @@ impl Display for MailParameter {
|
||||
|
||||
/// Values for the `BODY` parameter to `MAIL FROM`
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum MailBodyParameter {
|
||||
/// `7BIT`
|
||||
SevenBit,
|
||||
@@ -230,7 +253,7 @@ impl Display for MailBodyParameter {
|
||||
|
||||
/// A `RCPT TO` extension parameter
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum RcptParameter {
|
||||
/// Custom parameter
|
||||
Other {
|
||||
@@ -259,17 +282,20 @@ impl Display for RcptParameter {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::{ClientId, Extension, ServerInfo};
|
||||
use smtp::authentication::Mechanism;
|
||||
use smtp::response::{Category, Code, Detail, Response, Severity};
|
||||
use super::*;
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism,
|
||||
response::{Category, Code, Detail, Response, Severity},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_clientid_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", ClientId::new("test".to_string())),
|
||||
format!("{}", ClientId::Domain("test".to_string())),
|
||||
"test".to_string()
|
||||
);
|
||||
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
238
src/transport/smtp/mod.rs
Normal file
238
src/transport/smtp/mod.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
//! The SMTP transport sends emails using the SMTP protocol.
|
||||
//!
|
||||
//! This SMTP client follows [RFC
|
||||
//! 5321](https://tools.ietf.org/html/rfc5321), and is designed to efficiently send emails from an
|
||||
//! application to a relay email server, as it relies as much as possible on the relay server
|
||||
//! for sanity and RFC compliance checks.
|
||||
//!
|
||||
//! It implements the following extensions:
|
||||
//!
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](https://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](https://tools.ietf.org/html/rfc2487))
|
||||
//!
|
||||
//! #### SMTP Transport
|
||||
//!
|
||||
//! This transport uses the SMTP protocol to send emails over the network (locally or remotely).
|
||||
//!
|
||||
//! It is designed to be:
|
||||
//!
|
||||
//! * Secured: connections are encrypted by default
|
||||
//! * Modern: unicode support for email contents and sender/recipient addresses when compatible
|
||||
//! * Fast: supports connection reuse and pooling
|
||||
//!
|
||||
//! This client is designed to send emails to a relay server, and should *not* be used to send
|
||||
//! emails directly to the destination server.
|
||||
//!
|
||||
//! The relay server can be the local email server, a specific host or a third-party service.
|
||||
//!
|
||||
//! #### Simple example
|
||||
//!
|
||||
//! This is the most basic example of usage:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
//! # {
|
||||
//! 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())
|
||||
//! .subject("Happy new year")
|
||||
//! .body("Be happy!")
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! // Create TLS transport on port 465
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")
|
||||
//! .expect("relay valid")
|
||||
//! .build();
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_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;
|
||||
//!
|
||||
//! 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(),
|
||||
//! );
|
||||
//!
|
||||
//! 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(),
|
||||
//! );
|
||||
//!
|
||||
//! // 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 result_1 = mailer.send(&email_1);
|
||||
//! assert!(result_1.is_ok());
|
||||
//!
|
||||
//! // 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();
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! 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};
|
||||
//!
|
||||
//! 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 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()
|
||||
//! );
|
||||
//!
|
||||
//! 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();
|
||||
//!
|
||||
//! let result = mailer.send(&email);
|
||||
//!
|
||||
//! assert!(result.is_ok());
|
||||
//!
|
||||
//! mailer.close();
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub use self::async_transport::{
|
||||
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder, Tokio02Connector,
|
||||
};
|
||||
pub(crate) use self::transport::SmtpClient;
|
||||
pub use self::{
|
||||
error::Error,
|
||||
transport::{SmtpTransport, SmtpTransportBuilder},
|
||||
};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use crate::transport::smtp::client::TlsParameters;
|
||||
use crate::transport::smtp::{
|
||||
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||
client::SmtpConnection,
|
||||
extension::ClientId,
|
||||
response::Response,
|
||||
};
|
||||
use client::Tls;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
mod async_transport;
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub mod pool;
|
||||
pub mod response;
|
||||
mod transport;
|
||||
pub mod util;
|
||||
|
||||
// Registered port numbers:
|
||||
// https://www.iana.
|
||||
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
|
||||
/// Default smtp port
|
||||
pub const SMTP_PORT: u16 = 25;
|
||||
/// Default submission port
|
||||
pub const SUBMISSION_PORT: u16 = 587;
|
||||
/// Default submission over TLS port
|
||||
///
|
||||
/// 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)]
|
||||
struct SmtpInfo {
|
||||
/// Name sent during EHLO
|
||||
hello_name: ClientId,
|
||||
/// Server we are connecting to
|
||||
server: String,
|
||||
/// Port to connect to
|
||||
port: u16,
|
||||
/// TLS security configuration
|
||||
tls: Tls,
|
||||
/// Optional enforced authentication mechanism
|
||||
authentication: Vec<Mechanism>,
|
||||
/// Credentials
|
||||
credentials: Option<Credentials>,
|
||||
/// Define network timeout
|
||||
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for SmtpInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
server: "localhost".to_string(),
|
||||
port: SMTP_PORT,
|
||||
hello_name: ClientId::default(),
|
||||
credentials: None,
|
||||
authentication: DEFAULT_MECHANISMS.into(),
|
||||
timeout: Some(DEFAULT_TIMEOUT),
|
||||
tls: Tls::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/transport/smtp/pool.rs
Normal file
22
src/transport/smtp/pool.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
|
||||
use r2d2::ManageConnection;
|
||||
|
||||
impl ManageConnection for SmtpClient {
|
||||
type Connection = SmtpConnection;
|
||||
type Error = Error;
|
||||
|
||||
fn connect(&self) -> Result<Self::Connection, Error> {
|
||||
self.connection()
|
||||
}
|
||||
|
||||
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
||||
if conn.test_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::Client("is not connected anymore"))
|
||||
}
|
||||
|
||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||
conn.has_broken()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text
|
||||
//! message
|
||||
|
||||
use nom::{crlf, ErrorKind as NomErrorKind};
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::result;
|
||||
use std::str::{from_utf8, FromStr};
|
||||
use crate::transport::smtp::Error;
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::streaming::{tag, take_until},
|
||||
combinator::{complete, map},
|
||||
multi::many0,
|
||||
sequence::{preceded, tuple},
|
||||
IResult,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
result,
|
||||
str::FromStr,
|
||||
string::ToString,
|
||||
};
|
||||
|
||||
/// First digit indicates severity
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Severity {
|
||||
/// 2yx
|
||||
PositiveCompletion = 2,
|
||||
@@ -28,7 +39,7 @@ impl Display for Severity {
|
||||
|
||||
/// Second digit
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Category {
|
||||
/// x0z
|
||||
Syntax = 0,
|
||||
@@ -52,7 +63,7 @@ impl Display for Category {
|
||||
|
||||
/// The detail digit of a response code (third digit)
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Detail {
|
||||
#[allow(missing_docs)]
|
||||
Zero = 0,
|
||||
@@ -84,7 +95,7 @@ impl Display for Detail {
|
||||
|
||||
/// Represents a 3 digit SMTP response code
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Code {
|
||||
/// First digit of the response code
|
||||
pub severity: Severity,
|
||||
@@ -115,7 +126,7 @@ impl Code {
|
||||
///
|
||||
/// The text message is optional, only the code is mandatory
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Response {
|
||||
/// Response code
|
||||
pub code: Code,
|
||||
@@ -125,13 +136,10 @@ pub struct Response {
|
||||
}
|
||||
|
||||
impl FromStr for Response {
|
||||
type Err = NomErrorKind;
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> result::Result<Response, NomErrorKind> {
|
||||
match parse_response(s.as_bytes()) {
|
||||
Ok((_, res)) => Ok(res),
|
||||
Err(e) => Err(e.into_error_kind()),
|
||||
}
|
||||
fn from_str(s: &str) -> result::Result<Response, Error> {
|
||||
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +165,7 @@ impl Response {
|
||||
/// Returns only the first word of the message if possible
|
||||
pub fn first_word(&self) -> Option<&str> {
|
||||
self.message
|
||||
.get(0)
|
||||
.first()
|
||||
.and_then(|line| line.split_whitespace().next())
|
||||
}
|
||||
|
||||
@@ -169,101 +177,86 @@ impl Response {
|
||||
|
||||
// Parsers (originally from tokio-smtp)
|
||||
|
||||
named!(
|
||||
parse_code<Code>,
|
||||
map!(
|
||||
tuple!(parse_severity, parse_category, parse_detail),
|
||||
|(severity, category, detail)| Code {
|
||||
fn parse_code(i: &str) -> IResult<&str, Code> {
|
||||
let (i, severity) = parse_severity(i)?;
|
||||
let (i, category) = parse_category(i)?;
|
||||
let (i, detail) = parse_detail(i)?;
|
||||
Ok((
|
||||
i,
|
||||
Code {
|
||||
severity,
|
||||
category,
|
||||
detail,
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
named!(
|
||||
parse_severity<Severity>,
|
||||
alt!(
|
||||
tag!("2") => { |_| Severity::PositiveCompletion } |
|
||||
tag!("3") => { |_| Severity::PositiveIntermediate } |
|
||||
tag!("4") => { |_| Severity::TransientNegativeCompletion } |
|
||||
tag!("5") => { |_| Severity::PermanentNegativeCompletion }
|
||||
)
|
||||
);
|
||||
fn parse_severity(i: &str) -> IResult<&str, Severity> {
|
||||
alt((
|
||||
map(tag("2"), |_| Severity::PositiveCompletion),
|
||||
map(tag("3"), |_| Severity::PositiveIntermediate),
|
||||
map(tag("4"), |_| Severity::TransientNegativeCompletion),
|
||||
map(tag("5"), |_| Severity::PermanentNegativeCompletion),
|
||||
))(i)
|
||||
}
|
||||
|
||||
named!(
|
||||
parse_category<Category>,
|
||||
alt!(
|
||||
tag!("0") => { |_| Category::Syntax } |
|
||||
tag!("1") => { |_| Category::Information } |
|
||||
tag!("2") => { |_| Category::Connections } |
|
||||
tag!("3") => { |_| Category::Unspecified3 } |
|
||||
tag!("4") => { |_| Category::Unspecified4 } |
|
||||
tag!("5") => { |_| Category::MailSystem }
|
||||
)
|
||||
);
|
||||
fn parse_category(i: &str) -> IResult<&str, Category> {
|
||||
alt((
|
||||
map(tag("0"), |_| Category::Syntax),
|
||||
map(tag("1"), |_| Category::Information),
|
||||
map(tag("2"), |_| Category::Connections),
|
||||
map(tag("3"), |_| Category::Unspecified3),
|
||||
map(tag("4"), |_| Category::Unspecified4),
|
||||
map(tag("5"), |_| Category::MailSystem),
|
||||
))(i)
|
||||
}
|
||||
|
||||
named!(
|
||||
parse_detail<Detail>,
|
||||
alt!(
|
||||
tag!("0") => { |_| Detail::Zero } |
|
||||
tag!("1") => { |_| Detail::One } |
|
||||
tag!("2") => { |_| Detail::Two } |
|
||||
tag!("3") => { |_| Detail::Three } |
|
||||
tag!("4") => { |_| Detail::Four} |
|
||||
tag!("5") => { |_| Detail::Five } |
|
||||
tag!("6") => { |_| Detail::Six} |
|
||||
tag!("7") => { |_| Detail::Seven } |
|
||||
tag!("8") => { |_| Detail::Eight } |
|
||||
tag!("9") => { |_| Detail::Nine }
|
||||
)
|
||||
);
|
||||
fn parse_detail(i: &str) -> IResult<&str, Detail> {
|
||||
alt((
|
||||
map(tag("0"), |_| Detail::Zero),
|
||||
map(tag("1"), |_| Detail::One),
|
||||
map(tag("2"), |_| Detail::Two),
|
||||
map(tag("3"), |_| Detail::Three),
|
||||
map(tag("4"), |_| Detail::Four),
|
||||
map(tag("5"), |_| Detail::Five),
|
||||
map(tag("6"), |_| Detail::Six),
|
||||
map(tag("7"), |_| Detail::Seven),
|
||||
map(tag("8"), |_| Detail::Eight),
|
||||
map(tag("9"), |_| Detail::Nine),
|
||||
))(i)
|
||||
}
|
||||
|
||||
named!(
|
||||
parse_response<Response>,
|
||||
map_res!(
|
||||
tuple!(
|
||||
// Parse any number of continuation lines.
|
||||
many0!(tuple!(
|
||||
parse_code,
|
||||
preceded!(char!('-'), take_until_and_consume!(b"\r\n".as_ref()))
|
||||
)),
|
||||
// Parse the final line.
|
||||
tuple!(
|
||||
parse_code,
|
||||
terminated!(
|
||||
opt!(preceded!(char!(' '), take_until!(b"\r\n".as_ref()))),
|
||||
crlf
|
||||
)
|
||||
)
|
||||
),
|
||||
|(lines, (last_code, last_line)): (Vec<_>, _)| {
|
||||
// Check that all codes are equal.
|
||||
if !lines.iter().all(|&(ref code, _)| *code == last_code) {
|
||||
return Err(());
|
||||
}
|
||||
pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
|
||||
let (i, lines) = many0(tuple((
|
||||
parse_code,
|
||||
preceded(tag("-"), take_until("\r\n")),
|
||||
tag("\r\n"),
|
||||
)))(i)?;
|
||||
let (i, (last_code, last_line)) =
|
||||
tuple((parse_code, preceded(tag(" "), take_until("\r\n"))))(i)?;
|
||||
let (i, _) = complete(tag("\r\n"))(i)?;
|
||||
|
||||
// Extract text from lines, and append last line.
|
||||
let mut lines = lines.into_iter().map(|(_, text)| text).collect::<Vec<_>>();
|
||||
if let Some(text) = last_line {
|
||||
lines.push(text);
|
||||
}
|
||||
// Check that all codes are equal.
|
||||
if !lines.iter().all(|&(code, _, _)| code == last_code) {
|
||||
return Err(nom::Err::Failure(("", nom::error::ErrorKind::Not)));
|
||||
}
|
||||
|
||||
Ok(Response {
|
||||
code: last_code,
|
||||
message: lines
|
||||
.into_iter()
|
||||
.map(|line| from_utf8(line).map(|s| s.to_string()))
|
||||
.collect::<result::Result<Vec<_>, _>>()
|
||||
.map_err(|_| ())?,
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
// Extract text from lines, and append last line.
|
||||
let mut lines: Vec<String> = lines.into_iter().map(|(_, text, _)| text.into()).collect();
|
||||
lines.push(last_line.into());
|
||||
|
||||
Ok((
|
||||
i,
|
||||
Response {
|
||||
code: last_code,
|
||||
message: lines,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Category, Code, Detail, Response, Severity};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_severity_fmt() {
|
||||
@@ -473,6 +466,16 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_incomplete() {
|
||||
let raw_response = "250-smtp.example.org\r\n";
|
||||
let res = parse_response(raw_response);
|
||||
match res {
|
||||
Err(nom::Err::Incomplete(_)) => {}
|
||||
_ => panic!("Expected incomplete response, got {:?}", res),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_first_line() {
|
||||
assert_eq!(
|
||||
218
src/transport/smtp/transport.rs
Normal file
218
src/transport/smtp/transport.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
use r2d2::Pool;
|
||||
|
||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use crate::{Envelope, Transport};
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: Pool<SmtpClient>,
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
inner: SmtpClient,
|
||||
}
|
||||
|
||||
impl Transport for SmtpTransport {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
#[cfg(feature = "r2d2")]
|
||||
let mut conn = self.inner.get()?;
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
let result = conn.send(envelope, email)?;
|
||||
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
conn.quit()?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpTransport {
|
||||
/// Simple and secure transport, using TLS connections to comunicate 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"))]
|
||||
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSIONS_PORT)
|
||||
.tls(Tls::Wrapper(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
|
||||
///
|
||||
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
|
||||
/// that don't take SMTPS connections.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, by first connecting using
|
||||
/// an unencrypted connection and then upgrading it with STARTTLS. The provided
|
||||
/// domain is used to validate TLS certificates.
|
||||
///
|
||||
/// 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"))]
|
||||
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSION_PORT)
|
||||
.tls(Tls::Required(tls_parameters)))
|
||||
}
|
||||
|
||||
/// 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() -> SmtpTransport {
|
||||
Self::builder_dangerous("localhost").build()
|
||||
}
|
||||
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`SmtpTransport::relay`](#method.relay) or
|
||||
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
|
||||
let mut new = SmtpInfo::default();
|
||||
new.server = server.into();
|
||||
SmtpTransportBuilder { info: new }
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
impl SmtpTransportBuilder {
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
|
||||
self.info.authentication = mechanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.info.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the port to use
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.info.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[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 }
|
||||
}
|
||||
|
||||
/// 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
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
let client = self.build_client();
|
||||
SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: Pool::builder()
|
||||
.min_idle(Some(0))
|
||||
.idle_timeout(Some(Duration::from_secs(60)))
|
||||
.build_unchecked(client),
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
inner: client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpClient {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
impl SmtpClient {
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
pub fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match self.info.tls {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut conn = SmtpConnection::connect::<(&str, u16)>(
|
||||
(self.info.server.as_ref(), self.info.port),
|
||||
self.info.timeout,
|
||||
&self.info.hello_name,
|
||||
tls_parameters,
|
||||
)?;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
match self.info.tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, &credentials)?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
||||
/// Encode a string as xtext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct XText<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Display for XText<'a> {
|
||||
@@ -34,13 +34,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
for (input, expect) in vec![
|
||||
for (input, expect) in [
|
||||
("bjorn", "bjorn"),
|
||||
("bjørn", "bjørn"),
|
||||
("Ø+= ❤️‰", "Ø+2B+3D+20❤️‰"),
|
||||
("+", "+2B"),
|
||||
] {
|
||||
assert_eq!(format!("{}", XText(input)), expect);
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
assert_eq!(format!("{}", XText(input)), expect.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/transport/stub/mod.rs
Normal file
99
src/transport/stub/mod.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
//! The stub transport only logs message envelope and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//!
|
||||
//! #### Stub Transport
|
||||
//!
|
||||
//! The stub transport returns provided result and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::{Message, Envelope, Transport, StubTransport};
|
||||
//!
|
||||
//! 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();
|
||||
//!
|
||||
//! let mut sender = StubTransport::new_ok();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_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"))]
|
||||
use async_trait::async_trait;
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Error;
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("stub error")
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
|
||||
/// This transport logs the message envelope and returns the given response
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct StubTransport {
|
||||
response: Result<(), Error>,
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
/// Creates aResult new transport that always returns the given response
|
||||
pub fn new(response: Result<(), Error>) -> StubTransport {
|
||||
StubTransport { response }
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_ok() -> StubTransport {
|
||||
StubTransport { response: Ok(()) }
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns an error
|
||||
pub fn new_error() -> StubTransport {
|
||||
StubTransport {
|
||||
response: Err(Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for StubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(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 {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.response
|
||||
}
|
||||
}
|
||||
98
tests/transport_file.rs
Normal file
98
tests/transport_file.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
mod test {
|
||||
use lettre::{transport::file::FileTransport, Message};
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{remove_file, File},
|
||||
io::Read,
|
||||
};
|
||||
|
||||
#[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!")
|
||||
.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);
|
||||
|
||||
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 = "async-std1")]
|
||||
#[async_attributes::test]
|
||||
async fn file_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
63
tests/transport_sendmail.rs
Normal file
63
tests/transport_sendmail.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
mod test {
|
||||
use lettre::{transport::sendmail::SendmailTransport, Message};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[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!")
|
||||
.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;
|
||||
|
||||
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();
|
||||
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());
|
||||
}
|
||||
}
|
||||
21
tests/transport_smtp.rs
Normal file
21
tests/transport_smtp.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
mod test {
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
#[test]
|
||||
fn smtp_transport_simple() {
|
||||
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();
|
||||
SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build()
|
||||
.send(&email)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
58
tests/transport_smtp_pool.rs
Normal file
58
tests/transport_smtp_pool.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
|
||||
mod test {
|
||||
use lettre::{Envelope, SmtpTransport, Transport};
|
||||
use std::{sync::mpsc, thread};
|
||||
|
||||
fn envelope() -> Envelope {
|
||||
Envelope::new(
|
||||
Some("user@localhost".parse().unwrap()),
|
||||
vec!["root@localhost".parse().unwrap()],
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_one() {
|
||||
let mailer = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
|
||||
let result = mailer.send_raw(&envelope(), b"test");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_from_thread() {
|
||||
let mailer = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
|
||||
let (s1, r1) = mpsc::channel();
|
||||
let (s2, r2) = mpsc::channel();
|
||||
|
||||
let mailer1 = mailer.clone();
|
||||
let t1 = thread::spawn(move || {
|
||||
s1.send(()).unwrap();
|
||||
r2.recv().unwrap();
|
||||
mailer1
|
||||
.send_raw(&envelope(), b"test1")
|
||||
.expect("Send failed from thread 1");
|
||||
});
|
||||
|
||||
let mailer2 = mailer.clone();
|
||||
let t2 = thread::spawn(move || {
|
||||
s2.send(()).unwrap();
|
||||
r1.recv().unwrap();
|
||||
mailer2
|
||||
.send_raw(&envelope(), b"test2")
|
||||
.expect("Send failed from thread 2");
|
||||
});
|
||||
|
||||
t1.join().unwrap();
|
||||
t2.join().unwrap();
|
||||
|
||||
mailer
|
||||
.send_raw(&envelope(), b"test")
|
||||
.expect("Send failed from main thread");
|
||||
}
|
||||
}
|
||||
61
tests/transport_stub.rs
Normal file
61
tests/transport_stub.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use lettre::{transport::stub::StubTransport, Message};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[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();
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_attributes::test]
|
||||
async fn stub_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn stub_transport_tokio02() {
|
||||
use lettre::Tokio02Transport;
|
||||
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
}
|
||||
1
website/.gitignore
vendored
1
website/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/_book
|
||||
@@ -1,16 +0,0 @@
|
||||
all: depends _book
|
||||
|
||||
depends:
|
||||
cargo install --force mdbook --vers "^0.2"
|
||||
cargo install --force mdbook-linkcheck --vers "^0.2"
|
||||
|
||||
serve:
|
||||
mdbook serve
|
||||
|
||||
_book:
|
||||
mdbook build
|
||||
|
||||
clean:
|
||||
rm -rf _book/
|
||||
|
||||
.PHONY: _book
|
||||
@@ -1,11 +0,0 @@
|
||||
[book]
|
||||
title = "Lettre"
|
||||
author = "Alexis Mousset"
|
||||
description = "The user documentation of the Lettre crate."
|
||||
|
||||
[build]
|
||||
build-dir = "_book"
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.linkcheck]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Introduction
|
||||
|
||||
Lettre is an email library that allows creating and sending messages. It provides:
|
||||
|
||||
* An easy to use email builder
|
||||
* Pluggable email transports
|
||||
* Unicode support (for emails and transports, including for sender et recipient addresses when compatible)
|
||||
* Secure defaults (emails are only sent encrypted by default)
|
||||
|
||||
The `lettre_email` crate allows you to compose messages, and the `lettre`
|
||||
provide transports to send them.
|
||||
|
||||
Lettre requires Rust 1.32 or newer. Add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.9"
|
||||
lettre_email = "0.9"
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
# Summary
|
||||
|
||||
* [Introduction](README.md)
|
||||
* [Creating Messages](creating-messages/email.md)
|
||||
* [Sending Messages](sending-messages/_index.md)
|
||||
* [SMTP Transport](sending-messages/smtp.md)
|
||||
* [Sendmail Transport](sending-messages/sendmail.md)
|
||||
* [File Transport](sending-messages/file.md)
|
||||
* [Stub Transport](sending-messages/stub.md)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user