Compare commits

..

151 Commits

Author SHA1 Message Date
Alexis Mousset
195afe051e chore(all): 0.10.0-alpha.1 release 2020-05-31 11:49:37 +02:00
Alexis Mousset
e87f9950af Merge pull request #424 from amousset/async
Start implementing async transport
2020-05-10 16:21:13 +02:00
Alexis Mousset
8c8aa770bf feat(transport): Start async implementation (sendmail, file and stub transports) 2020-05-10 16:12:51 +02:00
Alexis Mousset
0d063873fc feat(transport-smtp): Use a streaming parser for response 2020-05-09 15:12:29 +02:00
Alexis Mousset
ce08d9e8aa feat(builder): Add a content-type method
for SinglePart
2020-05-09 11:27:25 +02:00
Alexis Mousset
83a0310c8c Merge pull request #423 from amousset/refactor-pool
Refactor pool
2020-05-08 17:22:44 +02:00
Alexis Mousset
c43e205212 feat(transport-smtp): Refactor connection pooling 2020-05-08 17:16:27 +02:00
Alexis Mousset
33b0a9e27d feat(transport-sendmail): Make the command parameter an OsStr 2020-05-08 11:46:25 +02:00
Alexis Mousset
4f0ea6366c fix(all): Document optional features 2020-05-07 19:51:14 +02:00
Alexis Mousset
2ade0b8846 Merge pull request #422 from amousset/sender
fix(builder): Fail if required headers are missing (fixes #95)
2020-05-06 20:41:02 +02:00
Alexis Mousset
6499bfe3d4 fix(builder): Fail if required headers are missing (fixes #95) 2020-05-06 20:36:36 +02:00
Alexis Mousset
4c7086968f Merge pull request #420 from amousset/transportresult
feat(transport): Make `Transport` result type an actual `Result`
2020-05-05 22:42:17 +02:00
Alexis Mousset
6afdb0cf3a feat(transport): Make Transport result type an actual Result (fixes #405) 2020-05-05 22:33:35 +02:00
Alexis Mousset
ed9f38d2c8 feat(builder): Allow overriding envelope 2020-05-03 22:03:45 +02:00
Alexis Mousset
88df2a502d chore(all): Add back test for smtp pool 2020-05-03 11:27:20 +02:00
Alexis Mousset
7dd392401c Merge pull request #419 from amousset/docs
chore(all): Move docs from website into API doc
2020-05-03 11:26:07 +02:00
Alexis Mousset
8425f1d7c4 chore(all): Move docs from website into API doc 2020-05-03 11:21:47 +02:00
Alexis Mousset
70c33882cc Merge pull request #418 from amousset/log-feature
fix(transport): Make logs optional and disabled by default (fixes #390)
2020-05-02 22:57:40 +02:00
Alexis Mousset
349b349518 fix(transport): Make logs optional and disabled by default (fixes #390) 2020-05-02 22:48:47 +02:00
Alexis Mousset
5acfa398f6 Merge pull request #417 from amousset/logs
fix(transport): Remove last info logs (fixes #398)
2020-05-02 22:33:30 +02:00
Alexis Mousset
e609c6bb72 fix(transport): Remove last info logs (fixes #398) 2020-05-02 22:29:10 +02:00
Alexis Mousset
0b98ccad45 Merge pull request #416 from amousset/retry-dns
Retry connection in SMTP transport
2020-05-02 22:10:10 +02:00
Alexis Mousset
c352efcb86 feat(transport-smtp): Retry over DNS connection issues (fixes #391) 2020-05-02 22:03:25 +02:00
Alexis Mousset
e640b6fe19 Merge pull request #415 from paolobarbolini/smtp-extension
Fix broken smtp/extension.rs
2020-05-02 21:23:23 +02:00
Paolo Barbolini
4c2f40dcb6 Fix broken smtp/extension.rs 2020-05-02 21:05:06 +02:00
Alexis Mousset
18a89d4407 feat(transport-smtp): Allow building without tls support 2020-05-02 20:58:40 +02:00
Alexis Mousset
4115652695 Merge pull request #413 from amousset/refactor-net
Refactor smtp client
2020-05-02 19:56:49 +02:00
Alexis Mousset
b3414bd1ff fix(transport): Fix connection pool 2020-05-02 19:45:58 +02:00
Alexis Mousset
0604030b91 Remove SmtpClient and make transport immutable
in Transport methods. Also make proper use of
connection pools.
2020-05-01 22:25:12 +02:00
Alexis Mousset
dfbe6e9ba2 Rename InnerClient to SmtpConnection 2020-04-30 19:59:31 +02:00
Alexis Mousset
123794a00d Merge pull request #412 from amousset/remove-bytes
Improve Message builder and remove bytes dependency
2020-04-29 12:34:48 +02:00
Alexis Mousset
1404cc8010 Merge branch 'master' into remove-bytes 2020-04-29 12:16:37 +02:00
Alexis Mousset
a661de16c9 fix(all): Rustfmt 2020-04-29 10:18:14 +02:00
Alexis Mousset
0ac3438d32 feat(transport-file): Store email as string when possible (fixes #385) 2020-04-28 23:45:37 +02:00
Alexis Mousset
7f22a98f2f feat(builder): Improve Message representation
* Remove bytes dependency and rely directly on bytes vec
* Allow encoding non-utf-8 strings
* Use Vec<u8> instead of Display for email formatting
2020-04-28 23:45:03 +02:00
Alexis Mousset
4e500ded50 feat(transport): Allow sending raw emails (fixes #409) 2020-04-28 23:44:23 +02:00
Alexis Mousset
edc22e842f Merge pull request #410 from amousset/raw-message
feat(transport): Allow sending raw emails (fixes #409)
2020-04-25 11:50:38 +02:00
Alexis Mousset
8b1399261f feat(transport): Allow sending raw emails (fixes #409) 2020-04-25 11:16:56 +02:00
Alexis Mousset
de277a2ee2 Merge pull request #407 from amousset/readme-0-9
fix(doc): README should be compatible with latest stabel release
2020-04-19 16:31:27 +02:00
Alexis Mousset
7d29d778ac fix(doc): README should be compatible with latest stabel release (fixes #389) 2020-04-19 16:30:45 +02:00
Alexis Mousset
8f31fb9804 Merge pull request #406 from amousset/fixes
* chore(all): Code cleanup

* fix(transport-smtp): Set root certs when using rustls
2020-04-19 15:59:56 +02:00
Ollie Ford
482d74f7bc fix(transport-smtp): Set root certs when using rustls 2020-04-19 15:21:37 +02:00
Alexis Mousset
2fdafcd573 chore(all): Code cleanup 2020-04-19 11:59:04 +02:00
Alexis Mousset
53aa5b4df6 Replace email builder by a new implementation (#393)
* Update dependencies (#386)

* Update dependencies and set MSRV to 1.40

* update hyperx

* Use display instead of description for errors

* Make hostname an optional feature

* Envelope from headers

* Update hyperx to 1.0

* rename builder to message

* Cleanup and make Transport send Messages

* Update rustls from 0.16 to 0.17

* Move transports into a common folder

* Merge imports from same crate

* Add message creation example to the site

* Hide "extern crate" in doc examples

* Add References and In-Reply-To methods

* Add message-id header

* Add blog posts and improve doc examples
2020-04-18 21:10:03 +00:00
Alexis Mousset
a440ae5c79 Merge pull request #392 from mjanda/content-id
Support for embeds with Content-ID
2020-02-06 13:04:22 +00:00
Marek Janda
16ac97e9d0 Support for embeds with Content-ID 2020-02-04 22:01:23 +01:00
Alexis Mousset
245c600c82 Update dependencies (#386)
Update dependencies and set MSRV to 1.40
2019-12-23 11:02:43 +00:00
Alexis Mousset
3995ea2983 fix(builder): rfc2047-encode non-ascii text 2019-12-19 11:15:24 +01:00
Alexis Mousset
8ed030a476 Merge pull request #384 from lettre/revert-383-encode-utf8
Revert "fix(builder): rfc2047-encode non-ascii text"
2019-12-18 23:07:00 +00:00
Alexis Mousset
d1b54dc990 Revert "fix(builder): rfc2047-encode non-ascii text" 2019-12-18 23:06:29 +00:00
Alexis Mousset
22eced1dbf Merge pull request #383 from amousset/encode-utf8
fix(builder): rfc2047-encode non-ascii text
2019-12-18 22:46:44 +00:00
Alexis Mousset
f0fd4556b5 fix(builder): rfc2047-encode non-ascii text 2019-12-18 23:42:24 +01:00
Alexis Mousset
715c169167 Merge pull request #382 from paolobarbolini/serde-impls-rename
fix(all): rename remaining 'serde-impls' features gates to 'serde'
2019-12-18 16:57:24 +00:00
Paolo Barbolini
c4d4413242 fix(all): rename remaining 'serde-impls' features gates to 'serde'
This renames the remaining 'serde-impls' features I forgot to change
in aac3e00 to 'serde'
2019-12-18 17:51:48 +01:00
Alexis Mousset
5667da9174 Merge pull request #381 from amousset/document-incompatibilities
feat(all): Document breaking changes separately in the changelog
2019-12-18 16:18:18 +00:00
Alexis Mousset
174791532d feat(all): Document breaking changes separately in the changelog 2019-12-18 17:11:32 +01:00
Alexis Mousset
99df787e24 Merge pull request #380 from amousset/remove-sendable-email
feat(all): Merge `Email` and `SendableEmail` into `lettre::Email`
2019-12-18 15:55:39 +00:00
Alexis Mousset
ce37464050 feat(all): Merge Email and SendableEmail into lettre::Email 2019-12-18 16:51:04 +01:00
Alexis Mousset
5e521b0c82 Update copyright year in LICENSE 2019-12-18 16:07:23 +01:00
Alexis Mousset
55ceb8f85d Merge pull request #379 from amousset/0-10
Change master version to 0.10
2019-12-18 12:55:33 +00:00
Alexis Mousset
a4a3f33180 change master version to 0.10 2019-12-18 13:47:09 +01:00
Alexis Mousset
b20e7a0964 feat(all): Update 0.10 changelog 2019-12-18 09:20:07 +01:00
Alexis Mousset
c4d91177dc fix(all): Fix conditional use for Debug 2019-12-17 21:05:34 +01:00
Alexis Mousset
f67d32ab86 Merge pull request #377 from paolobarbolini/default-native-tls
fix(all): make native-tls the default tls library
2019-12-17 20:00:28 +00:00
Alexis Mousset
21ae091f42 Merge pull request #376 from paolobarbolini/website-https
docs(all): change website url schemes to https
2019-12-17 18:27:08 +00:00
Paolo Barbolini
39a06862d4 fix(all): make native-tls the default tls library
This makes lettre behave like the rest of the libraries where
native-tls is the default tls library and rustls is optional
2019-12-17 18:34:18 +01:00
Paolo Barbolini
6014f5c3f4 docs(all): change website url schemes to https 2019-12-17 18:26:18 +01:00
Alexis Mousset
947af0acdd fix(all): Fix doc tests in website (#375) 2019-12-09 21:51:08 +00:00
Alexis Mousset
a90b548b4f fix(transport): Fix warnings ans test both TLS options 2019-12-08 23:16:44 +01:00
Alexis Mousset
b379adec28 fix(transport): Use default features in tests 2019-12-08 22:23:47 +01:00
Alexis Mousset
29e4829f69 feat(transport-smtp): Add rustls support 2019-12-08 22:13:38 +01:00
Alexis Mousset
d2675fab82 Merge pull request #374 from paolobarbolini/rename-serde-feature
style(all): rename 'serde-impls' feature to 'serde'
2019-12-04 20:42:14 +00:00
Paolo Barbolini
aac3e00f8d style(all): rename 'serde-impls' feature to 'serde'
This makes lettre behave like the rest of the libraries, where the
`serde` feature enables serde support
2019-12-04 21:39:05 +01:00
Alexis Mousset
0a450e64a8 Remove AppVeyor 2019-12-02 00:42:16 +01:00
Alexis Mousset
536b4451d7 Merge pull request #372 from mibac138/master
feat(all): Merge lettre_email into lettre with a `builder` feature
2019-11-30 21:49:25 +00:00
mibac138
0f3f27fdb6 feat(all): Merge lettre_email into lettre with a builder feature 2019-11-30 20:22:44 +01:00
Alexis Mousset
eb026838e3 Merge pull request #371 from mibac138/master
Box a large enum variant, minor code cleanup
2019-11-30 18:47:18 +00:00
mibac138
14fac980ed style(all): Minor code cleanup 2019-11-30 17:58:04 +01:00
mibac138
ff6a4ff910 perf(transport-smtp): Box TlsStream 2019-11-30 17:57:22 +01:00
Alexis Mousset
9b432aff7a Try to fix appveyor builds 2019-11-30 17:47:55 +01:00
Alexis Mousset
92ee714f9a Update ci status link 2019-11-30 17:44:32 +01:00
Alexis Mousset
ec184ca5ee Remove travis ci config 2019-11-30 17:33:00 +01:00
Alexis Mousset
f306c14575 Update website action 2019-11-30 16:51:37 +01:00
Alexis Mousset
4dc4dd29c5 Update website action 2019-11-30 16:44:19 +01:00
Alexis Mousset
61b4087a40 Update website action 2019-11-30 13:47:16 +01:00
Alexis Mousset
2200cf407d Create website.yml 2019-11-30 12:39:56 +00:00
Alexis Mousset
afc11951a6 Update test.yml 2019-11-30 12:26:46 +00:00
Alexis Mousset
4b6ea72aac Add coverage to test.yml 2019-11-30 13:12:39 +01:00
Alexis Mousset
6deeb02139 Clean cache before test and clippy 2019-11-30 13:06:17 +01:00
Alexis Mousset
97f60f111e Clean cache before test and clippy 2019-11-30 13:02:52 +01:00
Alexis Mousset
4601e0f8c8 Update test.yml 2019-11-30 11:54:12 +00:00
Alexis Mousset
1c879718cf Update test.yml 2019-11-30 11:44:29 +00:00
Alexis Mousset
da7b701e60 Update test.yml 2019-11-30 11:41:03 +00:00
Alexis Mousset
fe79b27b44 Add test to actions 2019-11-30 11:27:38 +00:00
Alexis Mousset
bc60857ce4 Fix clippy warnings 2019-11-30 12:18:03 +01:00
Alexis Mousset
e0910ad351 Run rustfmt 2019-11-30 12:05:20 +01:00
Alexis Mousset
ff6408f099 Update and rename rust.yml to test.yml 2019-11-30 11:03:42 +00:00
Alexis Mousset
7ba7560fbb Update audit.yml 2019-11-30 10:44:19 +00:00
Alexis Mousset
32e2a551b0 Create audit.yml 2019-11-30 10:42:49 +00:00
Alexis Mousset
bdd2076eec Update rust.yml 2019-11-30 10:38:52 +00:00
Alexis Mousset
3eef024f77 Add Github action config 2019-11-30 10:27:12 +00:00
Alexis Mousset
d227cd4384 Change MSRV to 1.36 (due to smallvec update) 2019-11-30 11:25:27 +01:00
Alexis Mousset
a4627f139a Merge pull request #369 from mibac138/master
Add EmailAddress::is_valid and into_inner
2019-11-30 09:59:32 +00:00
Alexis Mousset
9b825de617 Merge pull request #367 from gralpli/issue-362
fix(all): accept `Into<SendableEmail>`
2019-11-30 09:58:20 +00:00
Alexis Mousset
05735deb56 Merge pull request #368 from gralpli/issue-363
docs(transport-smtp): fix docs for `domain` field
2019-11-30 09:53:31 +00:00
mibac138
e5a1248a55 feat(email): Add EmailAddress::is_valid and into_inner 2019-11-29 19:52:44 +01:00
gralpli
0e05e0e792 docs(transport-smtp): fix docs for domain field
The `domain` field does not get “send to the server”,
but is used to check the authenticity of the server.
2019-11-29 15:39:22 +01:00
gralpli
86e51813ca fix(all): accept Into<SendableEmail>
When sending a mail, accept not only `SendableEmail`, but also anything
that can be converted .into() a `SendableEmail`.
2019-11-29 15:08:52 +01:00
Alexis Mousset
83a0185a83 Merge pull request #366 from paolobarbolini/use-serde-derive
style(all): use serde derive feature
2019-11-15 08:53:18 +00:00
Alexis Mousset
ebfd00b146 Merge pull request #365 from paolobarbolini/clippy-redundant-clone
clippy: remove redundant clone
2019-11-15 08:52:29 +00:00
Paolo Barbolini
4fbe7004cc style(all): use serde derive feature
The serde docs suggest using the `derive` feature instead of importing
`serde_derive` directly: https://serde.rs/derive.html
2019-11-15 08:58:14 +01:00
Paolo Barbolini
da8bf9a040 style(all): remove redundant clone 2019-11-15 08:56:54 +01:00
Alexis Mousset
6b6eadf134 Change MSRV to 1.34 (due to base64 0.11) 2019-11-14 20:37:42 +01:00
Alexis Mousset
75c640b4a9 Update dependencies 2019-11-14 19:50:01 +01:00
Alexis Mousset
24d694db3b Merge pull request #361 from amousset/criterion
feat(transport): Use criterion for benchmarks
2019-09-19 00:17:45 +00:00
Alexis Mousset
eda7fc1501 feat(transport): Use criterion for benchmarks 2019-09-19 02:12:39 +02:00
Alexis Mousset
5bc1cba2eb chore(transport): Use nom 5.0 with functions (#360) 2019-09-18 23:12:36 +00:00
Janrupf
bf2adcabed fix(smtp) Allow forcing of a specific auth (#358) 2019-09-18 20:06:07 +00:00
Maximilian Güntner
e927d0b2e5 feat(email): add build_body (#339) 2019-09-18 18:30:07 +00:00
Daniel Hauser
6eff9d3bee [Fix] Timeout bug causing infinite hang (#350) 2019-09-18 18:28:14 +00:00
Alexis Mousset
8336528f09 Merge pull request #353 from Atul9/cargo-fmt
Format code using 'cargo fmt'
2019-08-04 19:08:11 +00:00
Atul Bhosale
e86e33214f Format code using 'cargo fmt' 2019-08-05 00:20:25 +05:30
Alexis Mousset
089b811bbc Merge branch 'master' of github.com:lettre/lettre 2019-06-15 00:36:02 +02:00
Alexis Mousset
50d96ad8df feat(email): Allow providing a custom message id (fixes #346) 2019-06-15 00:35:30 +02:00
Alexis Mousset
657f2cd5ad Create FUNDING.yml 2019-06-11 19:07:07 +00:00
Alexis Mousset
e900b59008 feat(all): v0.9.2 release 2019-06-11 20:52:18 +02:00
Alexis Mousset
0313576fe1 fix(all): Add dyn keyword to trait objects 2019-06-11 19:52:35 +02:00
Alexis Mousset
5f75afe05c feat(all): Improve sendmail transport error handling 2019-06-11 19:40:36 +02:00
Alexis Mousset
0ead3cde09 Simplify header formatting and fix nightly build (fixes #340) 2019-05-18 19:33:37 +02:00
Alexis Mousset
4597884d31 fix(docs): Require rust 1.32 2019-05-05 21:14:51 +02:00
Alexis Mousset
2ad5e81e60 Merge branch 'v0.9.x' 2019-05-05 21:01:49 +02:00
Alexis Mousset
574afb0c9b fix(all): Properly override favicon in docs theme 2019-05-05 20:48:43 +02:00
Alexis Mousset
e17c1b8754 fix(all): Broken merge 2019-05-05 20:17:45 +02:00
Alexis Mousset
93066e4ca0 Merge branch 'v0.9.x' 2019-05-05 20:16:09 +02:00
Alexis Mousset
56b718f04d Merge branch 'v0.9.x' 2019-05-05 20:09:52 +02:00
Alexis Mousset
629b4b0501 feat(all): 0.9.1 release 2019-05-05 18:23:50 +02:00
Alexis Mousset
c638650d0a fix(email): Re-export mime crate 2019-05-05 18:12:02 +02:00
Alexis Mousset
1cbcbbb11f fix(transport): Correctly use minimum TLS version (fixes #323) 2019-05-01 20:28:35 +02:00
Alexis Mousset
c9bd7ed852 fix(Transport): Apply timeout to TCP connection 2019-05-01 18:23:58 +02:00
Alexis Mousset
334ce235ff fix(all): Formatting and style improvements 2019-04-14 18:21:44 +02:00
Alexis Mousset
10d362f509 fix(doc): Add a test on README example (refs #333) 2019-04-14 18:03:50 +02:00
Alexis Mousset
2e7bd5708f fix(doc): Fiw README syntax for 0.9 (fixes #333) 2019-04-14 17:57:33 +02:00
Alexis Mousset
cfe4ebf8cb fix(all): Remove unknown clippy annotations 2019-03-23 19:09:29 +01:00
Alexis Mousset
8f1c9dbec5 Merge branch 'master' of github.com:lettre/lettre 2019-03-23 18:23:21 +01:00
Alexis Mousset
0c055b50d1 feat(all): clippy 2019-03-23 18:22:53 +01:00
Alexis Mousset
139c07ca10 Update README.md 2019-03-23 13:20:40 +00:00
Alexis Mousset
5bb7316722 feat(all): Rust 2018 - fix for 1.31 2019-03-23 14:13:10 +01:00
Alexis Mousset
54f4cfcdab feat(all): Rust 2018 2019-03-23 13:59:35 +01:00
Alexis Mousset
9db66ce8e8 feat(all): Rust 2018 2019-03-23 13:25:01 +01:00
Alexis Mousset
d5e9ebc0db fix(all): 2018 compatibility 2019-03-23 12:16:54 +01:00
Alexis Mousset
cabc625009 Remove failure crate usage (fixes #331) 2019-03-23 12:04:39 +01:00
97 changed files with 5656 additions and 3457 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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
View File

@@ -0,0 +1 @@
liberapay: amousset

12
.github/workflows/audit.yml vendored Normal file
View 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 }}

115
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
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 -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
with:
command: test
args: --features=async
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
View File

@@ -1,6 +1,7 @@
.vscode/
.project/
.idea/
lettre.sublime-*
lettre.iml
target/
/Cargo.lock

View File

@@ -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'

View File

@@ -1,21 +1,42 @@
<a name="v0.9.4"></a>
### v0.9.4 (2020-04-21)
<a name="v0.10.0-alpha.1"></a>
### v0.10.0-alpha.1
#### 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))
#### 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)

View File

@@ -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/

View File

@@ -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
```

View File

@@ -1,5 +1,71 @@
[workspace]
members = [
"lettre",
"lettre_email",
]
[package]
name = "lettre"
version = "0.10.0-alpha.1" # remember to update html_root_url
description = "Email client"
readme = "README.md"
homepage = "https://lettre.at"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>", "Kayo <kayo@illumium.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 }
base64 = { version = "0.12", optional = true }
bufstream = { version = "0.1", optional = true }
hostname = { version = "0.3", optional = true }
hyperx = { version = "1", optional = true, features = ["headers"] }
idna = "0.2"
line-wrap = "0.1"
log = { version = "0.4", 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 }
regex = "1"
rustls = { version = "0.17", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
textnonce = { version = "0.7", optional = true }
uuid = { version = "0.8", features = ["v4"] }
webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.19", optional = true }
[dev-dependencies]
criterion = "0.3"
env_logger = "0.7"
glob = "0.3"
walkdir = "2"
[[bench]]
harness = false
name = "transport_smtp"
[features]
async = ["async-std", "async-trait", "async-attributes"]
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
default = ["file-transport", "smtp-transport", "rustls-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
file-transport = ["serde", "serde_json"]
rustls-tls = ["webpki", "webpki-roots", "rustls"]
sendmail-transport = []
smtp-transport = ["bufstream", "base64", "nom"]
unstable = []
[[example]]
name = "smtp"
required-features = ["smtp-transport"]
[[example]]
name = "smtp_gmail"
required-features = ["smtp-transport", "native-tls"]

View File

@@ -1,4 +1,5 @@
Copyright (c) 2014-2018 Alexis Mousset
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me>
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

100
README.md
View File

@@ -1,25 +1,31 @@
# 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 />
[![Build Status](https://travis-ci.org/lettre/lettre.svg?branch=master)](https://travis-ci.org/lettre/lettre)
[![Build status](https://ci.appveyor.com/api/projects/status/mpwglemugjtkps2d/branch/master?svg=true)](https://ci.appveyor.com/project/amousset/lettre/branch/master)
[![codecov](https://codecov.io/gh/lettre/lettre/branch/master/graph/badge.svg)](https://codecov.io/gh/lettre/lettre)
[![Crate](https://img.shields.io/crates/v/lettre.svg)](https://crates.io/crates/lettre)
[![Docs](https://docs.rs/lettre/badge.svg)](https://docs.rs/lettre/)
[![Required Rust version](https://img.shields.io/badge/rustc-1.20-green.svg)]()
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Gitter](https://badges.gitter.im/lettre/lettre.svg)](https://gitter.im/lettre/lettre?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/lettre/lettre.svg)](http://isitmaintained.com/project/lettre/lettre "Average time to resolve an issue")
[![Percentage of issues still open](http://isitmaintained.com/badge/open/lettre/lettre.svg)](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)
<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>
---
@@ -32,11 +38,17 @@ Lettre provides the following features:
* Secure delivery with SMTP using encryption and authentication
* Easy email builders
Lettre does not provide (for now):
* Async support
* Email parsing
## Example
This library requires Rust 1.32 or newer.
This library requires Rust 1.20 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.9"
@@ -44,38 +56,33 @@ lettre_email = "0.9"
```
```rust,no_run
extern crate lettre;
extern crate lettre_email;
use lettre::{EmailTransport, SmtpTransport};
use lettre_email::EmailBuilder;
use std::path::Path;
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 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();
// Open a local connection on port 25
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap()
// Open a local connection on port 25
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap()
.build();
// Send the email
let result = mailer.send(&email);
// 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());
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
```
## Testing
@@ -91,4 +98,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.

40
benches/transport_smtp.rs Normal file
View 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);

25
examples/smtp.rs Normal file
View File

@@ -0,0 +1,25 @@
use lettre::{Message, SmtpTransport, Transport};
fn main() {
env_logger::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
let result = mailer.send(&email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

33
examples/smtp_gmail.rs Normal file
View File

@@ -0,0 +1,33 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
fn main() {
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(
"example_username".to_string(),
"example_password".to_string(),
);
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// 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());
}

View File

@@ -1 +0,0 @@
../CHANGELOG.md

View File

@@ -1,53 +0,0 @@
[package]
name = "lettre"
version = "0.9.3" # 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"]

View File

@@ -1 +0,0 @@
../LICENSE

View File

@@ -1 +0,0 @@
../README.md

View File

@@ -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()
}

View File

@@ -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());
}

View File

@@ -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());
}

View File

@@ -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>;

View File

@@ -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(())
}
}

View File

@@ -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.3")]
#![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;
}

View File

@@ -1,78 +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("\"\""),
)
.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"))
}
}
}

View File

@@ -1,312 +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 { 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".\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\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>"
);
}
}

View File

@@ -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(()),
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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");
}
}

View File

@@ -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)
);
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -1 +0,0 @@
../CHANGELOG.md

View File

@@ -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"

View File

@@ -1 +0,0 @@
../LICENSE

View File

@@ -1 +0,0 @@
../README.md

View File

@@ -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());
}

View File

@@ -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)
}
}

View File

@@ -1,582 +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);
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()
);
}
}

View File

@@ -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();
}

View File

@@ -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)
);
}

280
src/address.rs Normal file
View File

@@ -0,0 +1,280 @@
//! 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 {
/// User part
pub user: String,
/// Domain part
pub domain: String,
/// Complete address
complete: String,
}
impl<U, D> TryFrom<(U, D)> for Address
where
U: Into<String>,
D: Into<String>,
{
type Error = AddressError;
fn try_from(from: (U, D)) -> Result<Self, Self::Error> {
let (user, domain) = from;
let user = user.into();
Address::check_user(&user)?;
let domain = domain.into();
Address::check_domain(&domain)?;
let complete = format!("{}@{}", &user, &domain);
Ok(Address {
user,
domain,
complete,
})
}
}
// 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: Into<String>, D: Into<String>>(user: U, domain: D) -> Result<Self, AddressError> {
(user, domain).try_into()
}
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.complete)
}
}
impl FromStr for Address {
type Err = AddressError;
fn from_str(val: &str) -> Result<Self, AddressError> {
if val.is_empty() || !val.contains('@') {
return Err(AddressError::MissingParts);
}
let parts: Vec<&str> = val.rsplitn(2, '@').collect();
let user = parts[1];
let domain = parts[0];
Address::check_user(user)
.and_then(|_| Address::check_domain(domain))
.map(|_| Address {
user: user.into(),
domain: domain.into(),
complete: val.to_string(),
})
}
}
impl AsRef<str> for Address {
fn as_ref(&self) -> &str {
&self.complete.as_ref()
}
}
impl AsRef<OsStr> for Address {
fn as_ref(&self) -> &OsStr {
self.complete.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")]
pub 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.to_string())
}
}
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)
}
}
}

52
src/error.rs Normal file
View 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, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str(&match self {
Error::MissingFrom => "missing source address, invalid envelope".to_string(),
Error::MissingTo => "missing destination address, invalid envelope".to_string(),
Error::TooManyFrom => "there can only be one source address".to_string(),
Error::EmailMissingAt => "missing @ in email address".to_string(),
Error::EmailMissingLocalPart => "missing local part in email address".to_string(),
Error::EmailMissingDomain => "missing domain in email address".to_string(),
Error::CannotParseFilename => "could not parse attachment filename".to_string(),
Error::NonAsciiChars => "contains non-ASCII chars".to_string(),
Error::Io(e) => e.to_string(),
})
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
Error::Io(err)
}
}
impl StdError for Error {}

240
src/lib.rs Normal file
View File

@@ -0,0 +1,240 @@
//! 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
//! * **r2d2**: Connection pool for SMTP transport
//! * **log**: Logging using the `log` crate
//! * **serde**: Serialization/Deserialization of entities
//! * **hostname**: Ability to try to use actual hostname in SMTP transaction
#![doc(html_root_url = "https://docs.rs/lettre/0.10.0")]
#![doc(html_favicon_url = "https://lettre.at/favicon.png")]
#![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(feature = "smtp-transport")]
pub use crate::transport::smtp::client::net::TlsParameters;
#[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, Tls};
pub use crate::{address::Address, transport::stub::StubTransport};
#[cfg(feature = "builder")]
use std::convert::TryFrom;
use std::{error::Error as StdError, fmt};
/// 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)
}
}
/// Transport method for emails
pub trait Transport {
/// Result types for the transport
type Ok: fmt::Debug;
type Error: StdError;
/// 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>;
}
#[cfg(feature = "async")]
pub mod r#async {
use super::*;
use async_trait::async_trait;
#[async_trait]
pub trait Transport {
/// Result types for the transport
type Ok: fmt::Debug;
type Error: StdError;
/// 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(),);
}
}

251
src/message/encoder.rs Normal file
View File

@@ -0,0 +1,251 @@
use crate::message::header::ContentTransferEncoding;
use line_wrap::{crlf, line_wrap, LineEnding};
use std::io::Write;
/// 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> {
if input.iter().all(u8::is_ascii) {
self.line_wrapper.encode(input)
} else {
panic!("")
}
}
}
/// 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 = &crlf();
let mut out = vec![0_u8; input.len() + input.len() / self.max_length * ending.len()];
let mut writer: &mut [u8] = out.as_mut();
writer.write_all(input).unwrap();
line_wrap(&mut out, input.len(), self.max_length, ending);
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::*;
if let Some(encoding) = encoding {
match encoding {
SevenBit => Box::new(SevenBitCodec::new()),
QuotedPrintable => Box::new(QuotedPrintableCodec::new()),
Base64 => Box::new(Base64Codec::new()),
EightBit => Box::new(EightBitCodec::new()),
Binary => Box::new(BinaryCodec::new()),
}
} else {
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, мир!"
);
}
}

View 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)
);
}
}

View 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`](::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`](::Mailboxes).
*/
(From, "From")
}
mailboxes_header! {
/**
`Reply-To` header
This header contains [`Mailboxes`](::Mailboxes).
*/
(ReplyTo, "Reply-To")
}
mailboxes_header! {
/**
`To` header
This header contains [`Mailboxes`](::Mailboxes).
*/
(To, "To")
}
mailboxes_header! {
/**
`Cc` header
This header contains [`Mailboxes`](::Mailboxes).
*/
(Cc, "Cc")
}
mailboxes_header! {
/**
`Bcc` header
This header contains [`Mailboxes`](::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
View 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,
};

View File

@@ -0,0 +1,86 @@
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 s: Vec<&str> = from_utf8(r)
.map_err(|_| HeaderError::Header)?
.split('.')
.collect();
if s.len() != 2 {
return Err(HeaderError::Header);
}
let major = s[0].parse().map_err(|_| HeaderError::Header)?;
let minor = s[1].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)));
}
}

View 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()))
);
}
}

View File

@@ -0,0 +1,5 @@
#[cfg(feature = "serde")]
mod serde;
mod types;
pub use self::types::*;

View 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()
);
}
}

View 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()
))
);
}
}

604
src/message/mimebody.rs Normal file
View File

@@ -0,0 +1,604 @@
use crate::message::{
encoder::codec,
header::{ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat,
};
use mime::Mime;
use textnonce::TextNonce;
/// 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, Copy)]
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,
}
impl MultiPartKind {
fn to_mime<S: AsRef<str>>(self, boundary: Option<S>) -> Mime {
let boundary = boundary
.map(|s| s.as_ref().into())
.unwrap_or_else(|| TextNonce::sized(68).unwrap().into_string());
use self::MultiPartKind::*;
format!(
"multipart/{}; boundary=\"{}\"",
match self {
Mixed => "mixed",
Alternative => "alternative",
Related => "related",
},
boundary
)
.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),
_ => 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)
}
/// 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_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"));
}
}

550
src/message/mod.rs Normal file
View File

@@ -0,0 +1,550 @@
//! 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
//! # extern crate lettre;
//! 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
//! # extern crate lettre;
//! 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
//! # extern crate lettre;
//! 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`](::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`](::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!"
)
);
}
}

69
src/message/utf8_b.rs Normal file
View File

@@ -0,0 +1,69 @@
use std::str::from_utf8;
// 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> {
let s = s.trim();
if s.starts_with("=?utf-8?b?") && s.ends_with("?=") {
let s = s.split_at(10).1;
let s = s.split_at(s.len() - 2).0;
base64::decode(s)
.map_err(|_| ())
.and_then(|v| {
if let Ok(s) = from_utf8(&v) {
Ok(Some(s.into()))
} else {
Err(())
}
})
.unwrap_or(None)
} 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())
);
}
}

View File

@@ -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>;

144
src/transport/file/mod.rs Normal file
View File

@@ -0,0 +1,144 @@
//! 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.
//!
//! #### File Transport
//!
//! 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.
//!
//! ```rust
//! # #[cfg(feature = "file-transport")]
//! # {
//! use std::env::temp_dir;
//! use lettre::{Transport, Envelope, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//!
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # }
//! ```
//!
//! Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.json`:
//!
//! ```json
//! TODO
//! ```
use crate::{transport::file::error::Error, Envelope, Transport};
use std::{
fs::File,
io::prelude::*,
path::{Path, PathBuf},
str,
};
use uuid::Uuid;
pub 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 Transport for FileTransport {
type Ok = Id;
type Error = Error;
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::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),
}),
}?;
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
Ok(email_id.to_string())
}
}
#[cfg(feature = "async")]
pub mod r#async {
use super::{FileTransport, Id, SerializableEmail};
use crate::{r#async::Transport, transport::file::error::Error, Envelope};
use async_std::fs::File;
use async_std::prelude::*;
use async_trait::async_trait;
use std::str;
use uuid::Uuid;
#[async_trait]
impl Transport for FileTransport {
type Ok = Id;
type Error = Error;
async fn send_raw(
&self,
envelope: &Envelope,
email: &[u8],
) -> Result<Self::Ok, Self::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),
}),
}?;
let mut file = File::create(file.as_path()).await?;
file.write_all(serialized.as_bytes()).await?;
Ok(email_id.to_string())
}
}
}

25
src/transport/mod.rs Normal file
View 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;

View File

@@ -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>;

View File

@@ -0,0 +1,128 @@
//! The sendmail transport sends the email using the local sendmail command.
//!
//! #### Sendmail Transport
//!
//! The sendmail transport sends the email using the local sendmail command.
//!
//! ```rust,no_run
//! # #[cfg(feature = "sendmail-transport")]
//! # {
//! 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());
//! # }
//! ```
use crate::{transport::sendmail::error::Error, Envelope, Transport};
use std::{
convert::AsRef,
ffi::OsString,
io::prelude::*,
process::{Command, Stdio},
};
pub 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
}
}
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")]
pub mod r#async {
use super::SendmailTransport;
use crate::{r#async::Transport, transport::sendmail::error::Error, Envelope};
use async_trait::async_trait;
use std::io::Write;
#[async_trait]
impl Transport for SendmailTransport {
type Ok = ();
type Error = Error;
// TODO: Convert to real async, once async-std has a process implementation.
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();
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)?))
}
}
}
}

View File

@@ -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

View File

@@ -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]);

View File

@@ -0,0 +1,376 @@
//! SMTP client
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
client::net::{NetworkStream, TlsParameters},
commands::*,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
Envelope,
};
use bufstream::BufStream;
#[cfg(feature = "log")]
use log::debug;
#[cfg(feature = "serde")]
use std::fmt::Debug;
use std::{
fmt::Display,
io::{self, BufRead, Write},
net::ToSocketAddrs,
string::String,
time::Duration,
};
pub mod mock;
pub mod net;
/// 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>) -> 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 { 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
#[cfg(feature = "log")]
fn escape_crlf(string: &str) -> String {
string.replace("\r\n", "<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: BufStream<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 = BufStream::new(NetworkStream::connect(server, timeout, tls_parameters)?);
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 = "log")]
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.stream.get_ref().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 = "log")]
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(ClientId::new(hello_name.to_string()))),
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 = BufStream::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 = match self.server_info.get_auth_mechanism(mechanisms) {
Some(m) => m,
None => {
return Err(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.write_all(string)?;
self.stream.flush()?;
#[cfg(feature = "log")]
debug!(
"Wrote: {}",
escape_crlf(String::from_utf8_lossy(string).as_ref())
);
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 = "log")]
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())
}
}
#[cfg(test)]
mod test {
use super::*;
#[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".\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\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>"
);
}
}

View File

@@ -0,0 +1,230 @@
//! A trait to represent a stream
use crate::transport::smtp::{client::mock::MockStream, error::Error};
#[cfg(feature = "native-tls")]
use native_tls::{TlsConnector, TlsStream};
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConfig, ClientSession};
#[cfg(feature = "native-tls")]
use std::io::ErrorKind;
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
use std::{
io::{self, Read, Write},
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
time::Duration,
};
/// Parameters to use for secure clients
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct TlsParameters {
/// A connector from `native-tls`
#[cfg(feature = "native-tls")]
connector: TlsConnector,
/// A client from `rustls`
#[cfg(feature = "rustls-tls")]
// TODO use the same in all transports of the client
connector: Box<ClientConfig>,
/// The domain name which is expected in the TLS certificate from the server
domain: String,
}
impl TlsParameters {
/// Creates a `TlsParameters`
#[cfg(feature = "native-tls")]
pub fn new(domain: String, connector: TlsConnector) -> Self {
Self { connector, domain }
}
/// Creates a `TlsParameters`
#[cfg(feature = "rustls-tls")]
pub fn new(domain: String, connector: ClientConfig) -> Self {
Self {
connector: Box::new(connector),
domain,
}
}
}
/// Represents the different types of underlying network streams
pub enum NetworkStream {
/// Plain TCP stream
Tcp(TcpStream),
/// Encrypted TCP stream
#[cfg(feature = "native-tls")]
Tls(Box<TlsStream<TcpStream>>),
#[cfg(feature = "rustls-tls")]
Tls(Box<rustls::StreamOwned<ClientSession, 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(),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")]
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),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")]
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
NetworkStream::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 {
let result = TcpStream::connect_timeout(&addr, timeout);
if result.is_ok() {
return result.map_err(|e| e.into());
}
}
Err(Error::Client("Could not connect"))
}
let tcp_stream = match timeout {
Some(t) => try_connect_timeout(server, t)?,
None => TcpStream::connect(server)?,
};
match tls_parameters {
#[cfg(feature = "native-tls")]
Some(context) => context
.connector
.connect(context.domain.as_ref(), tcp_stream)
.map(|tls| NetworkStream::Tls(Box::new(tls)))
.map_err(|e| Error::Io(io::Error::new(ErrorKind::Other, e))),
#[cfg(feature = "rustls-tls")]
Some(context) => {
let domain = webpki::DNSNameRef::try_from_ascii_str(&context.domain)?;
Ok(NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
ClientSession::new(&Arc::new(*context.connector.clone()), domain),
tcp_stream,
))))
}
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
Some(_) => panic!("TLS configuration without support"),
None => Ok(NetworkStream::Tcp(tcp_stream)),
}
}
#[allow(unused_variables, unreachable_code)]
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
*self = match *self {
#[cfg(feature = "native-tls")]
NetworkStream::Tcp(ref mut stream) => match tls_parameters
.connector
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
{
Ok(tls_stream) => NetworkStream::Tls(Box::new(tls_stream)),
Err(err) => return Err(Error::Io(io::Error::new(ErrorKind::Other, err))),
},
#[cfg(feature = "rustls-tls")]
NetworkStream::Tcp(ref mut stream) => {
let domain = webpki::DNSNameRef::try_from_ascii_str(&tls_parameters.domain)?;
NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
ClientSession::new(&Arc::new(*tls_parameters.connector.clone()), domain),
stream.try_clone().unwrap(),
)))
}
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
NetworkStream::Tcp(_) => panic!("STARTTLS without TLS support"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
NetworkStream::Tls(_) => return Ok(()),
NetworkStream::Mock(_) => return Ok(()),
};
Ok(())
}
pub fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Tcp(_) | NetworkStream::Mock(_) => false,
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
NetworkStream::Tls(_) => true,
}
}
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
/// Set write timeout for IO calls
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
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),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref mut s) => s.read(buf),
#[cfg(feature = "rustls-tls")]
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),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref mut s) => s.write(buf),
#[cfg(feature = "rustls-tls")]
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(),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref mut s) => s.flush(),
#[cfg(feature = "rustls-tls")]
NetworkStream::Tls(ref mut s) => s.flush(),
NetworkStream::Mock(ref mut s) => s.flush(),
}
}
}

View File

@@ -1,42 +1,47 @@
#![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,
};
#[cfg(feature = "log")]
use log::debug;
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 +49,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 +69,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 +94,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 +106,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 +117,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 +128,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,12 +139,12 @@ 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() {
@@ -149,60 +154,59 @@ impl Display for HelpCommand {
}
}
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,15 +214,15 @@ 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
@@ -237,19 +241,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 +267,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 +275,16 @@ impl AuthCommand {
let encoded_challenge = response
.first_word()
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
#[cfg(feature = "log")]
debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
#[cfg(feature = "log")]
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 +296,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 +311,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 +339,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"
);

View File

@@ -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>;

View File

@@ -1,21 +1,21 @@
//! 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;
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,
};
/// Default client id
pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
/// 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))]
pub enum ClientId {
/// A fully-qualified domain name
Domain(String),
@@ -43,14 +43,24 @@ impl ClientId {
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
/// found
#[cfg(feature = "hostname")]
pub fn hostname() -> ClientId {
ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
ClientId::Domain(
hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_DOMAIN_CLIENT_ID.to_string()),
)
}
#[cfg(not(feature = "hostname"))]
pub fn hostname() -> ClientId {
ClientId::Domain(DEFAULT_DOMAIN_CLIENT_ID.to_string())
}
}
/// 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
///
@@ -80,8 +90,8 @@ impl Display for Extension {
}
/// 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
///
@@ -123,8 +133,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 +145,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 +180,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 +231,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 +250,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 {
@@ -260,8 +280,10 @@ impl Display for RcptParameter {
mod test {
use super::{ClientId, Extension, ServerInfo};
use smtp::authentication::Mechanism;
use smtp::response::{Category, Code, Detail, Response, Severity};
use crate::transport::smtp::{
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
};
use std::collections::HashSet;
#[test]

478
src/transport/smtp/mod.rs Normal file
View File

@@ -0,0 +1,478 @@
//! 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))
//!
//! #### SMTP Transport
//!
//! This transport uses the SMTP protocol to send emails over the network (locally or remotely).
//!
//! It is designed to be:
//!
//! * Secured: email are encrypted by default
//! * Modern: unicode support for email content 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.
//!
//! 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(feature = "smtp-transport")]
//! # {
//! 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 local transport on port 25
//! let sender = SmtpTransport::unencrypted_localhost();
//! // Send the email on local 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();
//! # }
//! ```
//!
//! #### Lower level
//!
//! You can also send commands, here is a simple email transaction without
//! error handling:
//!
//! ```rust,no_run
//! # #[cfg(feature = "smtp-transport")]
//! # {
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection};
//!
//! let hello = ClientId::new("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(any(feature = "native-tls", feature = "rustls-tls"))]
use crate::transport::smtp::client::net::TlsParameters;
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
client::SmtpConnection,
error::Error,
extension::ClientId,
response::Response,
},
Envelope, Transport,
};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "r2d2")]
use r2d2::{Builder, Pool};
#[cfg(feature = "rustls-tls")]
use rustls::ClientConfig;
use std::time::Duration;
#[cfg(feature = "rustls-tls")]
use webpki_roots::TLS_SERVER_ROOTS;
pub mod authentication;
pub mod client;
pub mod commands;
pub mod error;
pub mod extension;
#[cfg(feature = "r2d2")]
pub mod pool;
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
///
/// https://tools.ietf.org/html/rfc8314
pub const SUBMISSIONS_PORT: u16 = 465;
/// Default timeout
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
/// 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_debug_implementations, 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),
}
#[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 {
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No authentication
/// * A 60 seconds timeout for smtp commands
/// * Port 587
///
/// Consider using [`SmtpTransport::new`] instead, if possible.
pub fn builder<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let mut new = SmtpInfo::default();
new.server = server.into();
SmtpTransportBuilder { info: new }
}
/// Simple and secure transport, should be used when possible.
/// 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> {
#[cfg(feature = "native-tls")]
let mut tls_builder = TlsConnector::builder();
#[cfg(feature = "native-tls")]
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
#[cfg(feature = "native-tls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls_builder.build()?);
#[cfg(feature = "rustls-tls")]
let mut tls = ClientConfig::new();
#[cfg(feature = "rustls-tls")]
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
#[cfg(feature = "rustls-tls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls);
Ok(Self::builder(relay)
.port(SUBMISSIONS_PORT)
.tls(Tls::Wrapper(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("localhost").port(SMTP_PORT).build()
}
}
#[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: SUBMISSION_PORT,
hello_name: ClientId::hostname(),
credentials: None,
authentication: DEFAULT_MECHANISMS.into(),
timeout: Some(DEFAULT_TIMEOUT),
tls: Tls::None,
}
}
}
/// 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 with custom pool settings
#[cfg(feature = "r2d2")]
pub fn build_with_pool(self, pool: Builder<SmtpClient>) -> SmtpTransport {
let pool = pool.build_unchecked(self.build_client());
SmtpTransport { inner: pool }
}
/// Build the transport (with default pool if enabled)
pub fn build(self) -> SmtpTransport {
let client = self.build_client();
SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool::builder().max_size(5).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> {
let mut conn = SmtpConnection::connect::<(&str, u16)>(
(self.info.server.as_ref(), self.info.port),
self.info.timeout,
&self.info.hello_name,
#[allow(clippy::match_single_binding)]
match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
},
)?;
#[allow(clippy::match_single_binding)]
match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters, &self.info.hello_name)?;
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters, &self.info.hello_name)?;
}
_ => (),
}
match &self.info.credentials {
Some(credentials) => {
conn.auth(self.info.authentication.as_slice(), &credentials)?;
}
None => (),
}
Ok(conn)
}
}

View 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()
}
}

View File

@@ -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,92 @@ 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(|&(ref 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<&str> = lines
.into_iter()
.map(|(_, text, _)| text)
.collect::<Vec<_>>();
lines.push(last_line);
Ok((
i,
Response {
code: last_code,
message: lines
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>(),
},
))
}
#[cfg(test)]
mod test {
use super::{Category, Code, Detail, Response, Severity};
use super::*;
#[test]
fn test_severity_fmt() {
@@ -473,6 +472,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!(

View File

@@ -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());
}
}
}

96
src/transport/stub/mod.rs Normal file
View File

@@ -0,0 +1,96 @@
//! 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());
//! ```
use crate::{Envelope, Transport};
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 {
write!(f, "stub error")
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
None
}
}
/// 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")]
pub mod r#async {
use super::StubTransport;
use crate::{r#async::Transport, transport::stub::Error, Envelope};
use async_trait::async_trait;
#[async_trait]
impl Transport for StubTransport {
type Ok = ();
type Error = Error;
async fn send_raw(
&self,
_envelope: &Envelope,
_email: &[u8],
) -> Result<Self::Ok, Self::Error> {
self.response
}
}
}

65
tests/transport_file.rs Normal file
View File

@@ -0,0 +1,65 @@
#[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,
};
#[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")]
#[async_attributes::test]
async fn file_transport_async() {
use lettre::r#async::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).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();
}
}

View File

@@ -0,0 +1,40 @@
#[cfg(test)]
#[cfg(feature = "sendmail-transport")]
mod test {
use lettre::{transport::sendmail::SendmailTransport, Message};
#[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")]
#[async_attributes::test]
async fn sendmail_transport_async() {
use lettre::r#async::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")
.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
View 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("127.0.0.1")
.port(2525)
.build()
.send(&email)
.unwrap();
}
}

View File

@@ -0,0 +1,62 @@
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
mod test {
use lettre::{Envelope, SmtpTransport, Transport};
use r2d2::Pool;
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 pool = Pool::builder().max_size(1);
let mailer = SmtpTransport::builder("127.0.0.1")
.port(2525)
.build_with_pool(pool);
let result = mailer.send_raw(&envelope(), b"test");
assert!(result.is_ok());
}
#[test]
fn send_from_thread() {
let pool = Pool::builder().max_size(1);
let mailer = SmtpTransport::builder("127.0.0.1")
.port(2525)
.build_with_pool(pool);
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");
}
}

37
tests/transport_stub.rs Normal file
View File

@@ -0,0 +1,37 @@
use lettre::{transport::stub::StubTransport, Message};
#[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")]
#[async_attributes::test]
async fn stub_transport_async() {
use lettre::r#async::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")
.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
View File

@@ -1 +0,0 @@
/_book

View File

@@ -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

View File

@@ -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]

View File

@@ -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"
```

View File

@@ -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)

View File

@@ -1,36 +0,0 @@
### Creating messages
This section explains how to create emails.
#### Simple example
The `email` part builds email messages. For now, it does not support attachments.
An email is built using an `EmailBuilder`. The simplest email could be:
```rust
extern crate lettre_email;
use lettre_email::Email;
fn main() {
// Create an email
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")
.alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
.build();
assert!(email.is_ok());
}
```
When the `build` method is called, the `EmailBuilder` will add the missing headers (like
`Message-ID` or `Date`) and check for missing necessary ones (like `From` or `To`). It will
then generate an `Email` that can be sent.
The `text()` method will create a plain text email, while the `html()` method will create an
HTML email. You can use the `alternative()` method to provide both versions, using plain text
as fallback for the HTML version.

View File

@@ -1,17 +0,0 @@
### 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 `SendableEmail`, which is the case for emails created with `lettre_email`.
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.

View File

@@ -1,43 +0,0 @@
#### File Transport
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.
```rust
extern crate lettre;
use std::env::temp_dir;
use lettre::file::FileTransport;
use lettre::{Transport, Envelope, EmailAddress, SendableEmail};
fn main() {
// Write to the local temp directory
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 world".to_string().into_bytes(),
);
let result = sender.send(email);
assert!(result.is_ok());
}
```
Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.txt`:
```text
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
To: <root@localhost>
From: <user@localhost>
Subject: Hello
Date: Sat, 31 Oct 2015 13:42:19 +0100
Message-ID: <b7c211bc-9811-45ce-8cd9-68eab575d695.lettre@localhost>
Hello World!
```

View File

@@ -1,25 +0,0 @@
#### Sendmail Transport
The sendmail transport sends the email using the local sendmail command.
```rust,no_run
extern crate lettre;
use lettre::sendmail::SendmailTransport;
use lettre::{SendableEmail, Envelope, EmailAddress, Transport};
fn main() {
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 world".to_string().into_bytes(),
);
let mut sender = SendmailTransport::new();
let result = sender.send(email);
assert!(result.is_ok());
}
```

View File

@@ -1,180 +0,0 @@
#### SMTP Transport
This transport uses the SMTP protocol to send emails over the network (locally or remotely).
It is designed to be:
* Secured: email are encrypted by default
* Modern: Unicode support for email content and sender/recipient addresses when compatible
* Fast: supports tcp connection reuse
This client is designed to send emails to a relay server, and should *not* be used to send
emails directly to the destination.
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
extern crate lettre;
use lettre::{SendableEmail, EmailAddress, Transport, Envelope, SmtpClient};
fn main() {
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 world".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);
assert!(result.is_ok());
}
```
#### Complete example
```rust,no_run
extern crate lettre;
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::{SendableEmail, Envelope, EmailAddress, Transport, SmtpClient};
use lettre::smtp::extension::ClientId;
use lettre::smtp::ConnectionReuseParameters;
fn main() {
let email_1 = SendableEmail::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 = SendableEmail::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:
```rust,no_run
extern crate native_tls;
extern crate lettre;
extern crate lettre_email;
use lettre::{
ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
SendableEmail, SmtpClient, Transport,
};
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::smtp::ConnectionReuseParameters;
use native_tls::{Protocol, TlsConnector};
fn main() {
let email = SendableEmail::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();
}
```
#### Lower level
You can also send commands, here is a simple email transaction without
error handling:
```rust,no_run
extern crate lettre;
use lettre::EmailAddress;
use lettre::smtp::SMTP_PORT;
use lettre::smtp::client::InnerClient;
use lettre::smtp::client::net::NetworkStream;
use lettre::smtp::extension::ClientId;
use lettre::smtp::commands::*;
fn main() {
let mut email_client: InnerClient<NetworkStream> = InnerClient::new();
let _ = email_client.connect(&("localhost", SMTP_PORT), None);
let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
let _ = email_client.command(
MailCommand::new(Some(EmailAddress::new("user@example.com".to_string()).unwrap()), vec![])
);
let _ = email_client.command(
RcptCommand::new(EmailAddress::new("user@example.org".to_string()).unwrap(), vec![])
);
let _ = email_client.command(DataCommand);
let _ = email_client.message(Box::new("Test email".as_bytes()));
let _ = email_client.command(QuitCommand);
}
```

View File

@@ -1,32 +0,0 @@
#### Stub Transport
The stub transport only logs message envelope and drops the content. It can be useful for
testing purposes.
```rust
extern crate lettre;
use lettre::stub::StubTransport;
use lettre::{SendableEmail, Envelope, EmailAddress, Transport};
fn main() {
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 world".to_string().into_bytes(),
);
let mut sender = StubTransport::new_positive();
let result = sender.send(email);
assert!(result.is_ok());
}
```
Will log (when using a logger like `env_logger`):
```text
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB