Compare commits

..

260 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
cf8f934c56 fix(docs): Broken title syntax in SMTP docs 2019-05-05 21:01:36 +02:00
Alexis Mousset
df949f837e fix(all): Properly override favicon in docs theme 2019-05-05 20:49:12 +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
0a3d51dc25 fix(docs): Use doc root and set custom favicon 2019-05-05 20:15:45 +02:00
Alexis Mousset
56b718f04d Merge branch 'v0.9.x' 2019-05-05 20:09:52 +02:00
Alexis Mousset
4828cf4e92 feat(all): Require rust 1.32 2019-05-05 20:07:38 +02:00
Alexis Mousset
c33de49fbb feat(all): Move to mdBook for the docs 2019-05-05 19:45:51 +02:00
Alexis Mousset
629b4b0501 feat(all): 0.9.1 release 2019-05-05 18:23:50 +02:00
Alexis Mousset
4f470a2c3f feat(all): 0.9.1 release 2019-05-05 18:20:30 +02:00
Alexis Mousset
a0c8fb947c fix(email): Re-export mime crate 2019-05-05 18:16:05 +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
Alexis Mousset
101189882a Prepare 0.9 release 2019-03-17 13:49:45 +01:00
Alexis Mousset
be50862d55 Fix clippy and formatting 2019-03-17 13:26:14 +01:00
Alexis Mousset
2d4320ea45 Fix clippy and formatting 2019-03-17 13:21:14 +01:00
Alexis Mousset
0444e7833b feat(email): Uptae dev-dependencies 2019-03-17 12:55:25 +01:00
Alexis Mousset
6997ab7ce4 Merge pull request #316 from marmistrz/SmtpClient
Mention SmtpClient::new_simple in the docs for SmtpClient::new
2019-03-17 11:38:12 +00:00
Alexis Mousset
139eb9e67e Merge pull request #317 from stammw/master
feat(transport-smtp): SMTP connection pool implementation with r2d2
2019-03-17 11:37:45 +00:00
Alexis Mousset
917d34210d Merge pull request #319 from architekton/gmail_example
docs(transport-smtp): Gmail transport simple example
2019-03-17 11:37:03 +00:00
Alexis Mousset
ed2730a0a7 Merge pull request #320 from TyPR124/get-from-from-envelope
Get 'from' from envelope
2019-03-17 11:36:43 +00:00
Alexis Mousset
81d174d7ed Merge pull request #321 from funkill/original_email_crate
Using orginal email dependency
2019-03-17 11:36:23 +00:00
Alexis Mousset
bdb4f78bd3 Merge branch 'master' into original_email_crate 2019-03-17 11:36:15 +00:00
Alexis Mousset
4a76fbb46c Merge pull request #324 from cynede/base64
Update base64 to ^0.10
2019-03-17 11:35:27 +00:00
Alexis Mousset
52535c4554 Merge pull request #328 from Eijebong/unfork
Use the original rust-email instead of the lettre fork
2019-03-17 11:35:17 +00:00
Alexis Mousset
5918803778 Merge pull request #329 from leo-lb/master
fix(smtp-transport): Client::read_response infinite loop
2019-03-17 11:34:43 +00:00
leo-lb
72f3cd8f12 fix(smtp-transport): Client::read_response infinite loop
I have encountered an issue on Gmail where the server returned an error,
and the code was stuck here looping indefinitely.
This commit fixes the issue.
2019-03-12 01:44:04 +01:00
Bastien Orivel
78ba8007cd fix(lettre_email): Use the original rust-email
The fork is 10 commits behind the original repo, I don't see a point in
using it as the rest of the history is the same
2019-02-24 14:30:41 +01:00
Cynede
e202eafb7f feat(transport): Update base64 to ^0.10 2019-02-11 18:46:24 +04:00
funkill2
adbd50a6ce fix(email): Use original "email" crate 2019-01-05 02:07:04 +03:00
Tyler Ruckinger
058fa694f0 fix issue, inserting 'from' from envelope into message headers.
add test case to expect failure when there really is no 'from'
2018-12-28 20:13:17 -05:00
Tyler Ruckinger
f64721702f Add test case 2018-12-28 19:47:48 -05:00
Architekton
a8d8e2ac00 docs(transport-smtp): Gmail transport simple example 2018-11-13 12:50:18 +11:00
Jean-Christophe BEGUE
434654e9af feat(transport-smtp): SMTP connection pool implementation with r2d2 2018-11-09 16:43:44 +01:00
Marcin Mielniczuk
4a77f587c3 docs(transport-smtp): Improve SmtpClient docs.
Mention SmtpClient::new_simple in the docs for SmtpClient::new
2018-10-16 01:44:17 +02:00
Alexis Mousset
c988b1760a Merge pull request #311 from stammw/master
check email validity before creating any new EmailAddress #308
2018-10-03 20:32:23 +02:00
Jean-Christophe BEGUE
e08d4e3ee5 feat(email): check validity for EmailAddress #308 2018-09-22 10:06:45 +02:00
Alexis Mousset
fc91bb6ee8 feat(email): Add In-Reply-To and References headers 2018-09-20 21:45:08 +02:00
Alexis Mousset
ee31bbe9e3 fix(email): Do not include Bcc addresses in headers 2018-09-20 20:20:20 +02:00
Alexis Mousset
0b92881b48 feat(email): Update uuid to 0.7 2018-09-20 20:12:15 +02:00
Alexis Mousset
8bb97e62ca Update nom to 0.4 2018-09-20 20:09:06 +02:00
Alexis Mousset
8ba1c3a3f7 Merge pull request #303 from tyranron/upgrade-to-native-tls-0-2
feat(transport-smtp): Upgrade to 0.2 version of native-tls crate
2018-09-20 19:59:52 +02:00
tyranron
644b1e59b0 feat(transport-smtp): Upgrade to 0.2 version of native-tls crate 2018-07-08 10:40:13 +03:00
Alexis Mousset
e225afbec2 Merge pull request #297 from amousset/code-cleanup
style(transport): Improve code style
2018-05-31 15:49:20 +02:00
Alexis Mousset
22555f0620 style(transport): Improve code style 2018-05-31 15:49:05 +02:00
Alexis Mousset
c09e3ff8cd Merge pull request #296 from amousset/failure
feat(all): Start using the failure crate for errors
2018-05-31 13:15:14 +02:00
Alexis Mousset
c10fe3db84 feat(all): Start using the failure crate for errors 2018-05-31 13:08:02 +02:00
Alexis Mousset
9d14630552 Merge pull request #295 from amousset/submissions
feat(transport): Use submissions port by default
2018-05-31 09:34:42 +02:00
Alexis Mousset
186ad29424 feat(transport): Use submissions port by default 2018-05-31 09:17:08 +02:00
Alexis Mousset
dce8d53310 Merge pull request #293 from amousset/fix-vec-attachment
feat(email): Improve attachment methods
2018-05-31 09:02:14 +02:00
Alexis Mousset
c9c82495ce feat(email): Improve attachment methods 2018-05-31 00:21:14 +02:00
Alexis Mousset
283d45824e Merge pull request #291 from madmaxio/master
Added attachment from a vector of bytes
2018-05-29 23:50:42 +02:00
Alexis Mousset
0ee089fc37 Merge pull request #292 from amousset/clean
style(all): Run rustfmt and clippy
2018-05-28 23:55:28 +02:00
Alexis Mousset
7f545301e1 style(all): Run rustfmt and clippy 2018-05-28 23:46:05 +02:00
Баранов Максим Игоревич
ce932c15d6 Added reference to bytes_vec 2018-05-24 10:31:34 +03:00
Maximb
afc23de20f Added attach_from_vec function to email builder 2018-05-23 14:22:46 +00:00
Alexis Mousset
c52c28ab80 Merge pull request #287 from astonbitecode/master
fix (transport): Sendmail transport: Fix to-list manipulation
2018-05-22 20:56:23 +02:00
aston
4f9067f258 fix (transport): Sendmail transport: Fix to-list manipulation
Use the `to_addresses` Vec as a separate `args` entry.
2018-05-22 10:58:39 +03:00
Alexis Mousset
964b9dc00b Merge pull request #286 from amousset/fix-mime-readme
docs(all): README should not use external crates
2018-05-20 00:06:02 +02:00
Alexis Mousset
866c804ef3 docs(all): README should not use external crates 2018-05-20 00:03:28 +02:00
Alexis Mousset
a7d35325ed Merge pull request #285 from amousset/readme-08
docs(all): README should be compatible with latest stable version
2018-05-19 23:59:41 +02:00
Alexis Mousset
319be26031 docs(all): README should be compatible with latest stable version 2018-05-19 23:53:29 +02:00
Alexis Mousset
3a0b6e1a31 Merge pull request #282 from amousset/add-xoauth2
feat(transport): Initial support for XOAUTH2
2018-05-15 01:18:06 +02:00
Alexis Mousset
ed7c16452c feat(transport): Initial support for XOAUTH2 2018-05-15 01:04:16 +02:00
Alexis Mousset
2e2f614517 Merge pull request #281 from amousset/remove-crammd5
feat(transport): Remove support for CRAM-MD5
2018-05-15 00:53:33 +02:00
Alexis Mousset
bc09aa2185 feat(transport): Remove support for CRAM-MD5
It is obsolete and may give a false sense of security.
We may add a better mechanism later.
2018-05-15 00:35:21 +02:00
Alexis Mousset
bab4519baa Merge pull request #278 from amousset/tls12
feat(transport): Remove TLS 1.1 in accepted protocols by default
2018-05-06 11:39:33 +02:00
Alexis Mousset
1f1359502e Merge pull request #279 from amousset/disable-crammd5
feat(transport): Disable the CRAM-MD5 AUTH feature dy default
2018-05-06 11:39:11 +02:00
Alexis Mousset
5423f53bad feat(transport): Disable the CRAM-MD5 AUTH feature dy default 2018-05-06 11:28:42 +02:00
Alexis Mousset
4b48bdbd9a feat(transport): Remove TLS 1.1 in accepted protocols by default (only allow TLS 1.2) 2018-05-06 11:24:17 +02:00
Alexis Mousset
31442e96d0 Update issue templates 2018-05-05 01:51:42 +02:00
Alexis Mousset
70720d7cdd Add 0.8.2 to changelog 2018-05-04 00:52:04 +02:00
Alexis Mousset
706ed8b4fd Merge branch 'devel' 2018-05-04 00:27:04 +02:00
Alexis Mousset
da63de72fc Build doc from v0.8.x branch 2018-05-04 00:07:54 +02:00
dimlev
d71b560077 fix(transport): Write timeout is not set in smtp transport 2018-05-03 08:52:36 +02:00
Alexis Mousset
917ecbc477 Rename SmtpTransportBuilder to SmtpClient 2018-04-30 01:01:59 +02:00
Alexis Mousset
4a61357205 Rework internal structs 2018-04-29 11:28:41 +02:00
Alexis Mousset
e7e0f3485d feat(transport): Use md-5 and hmac instead of rust-crypto
RustCrypto is not supported anymore, and this avoids
compiling useless code.
2018-04-28 15:04:49 +02:00
Alexis Mousset
33d20c61d1 Merge pull request #269 from amousset/remove-builders
feat(email): Remove non-chaining builder methods
2018-04-27 22:49:44 +02:00
Alexis Mousset
1baf8a9516 feat(email): Remove non-chaining builder methods 2018-04-27 22:40:50 +02:00
Alexis Mousset
5bb4f4f8e7 Merge pull request #267 from amousset/remove-rust-crypto
feat(transport): Use md-5 and hmac instead of rust-crypto
2018-04-17 22:25:59 +02:00
Alexis Mousset
2e56bd6a82 feat(transport): Use md-5 and hmac instead of rust-crypto
RustCrypto is not supported anymore, and this avoids
compiling useless code.
2018-04-17 22:15:50 +02:00
Alexis Mousset
3159981e4a Merge pull request #266 from amousset/result-type-transport
feat(transport): Use an associated type for result type of EmailTrans…
2018-04-17 00:29:35 +02:00
Alexis Mousset
a0c95f748e feat(transport): Use an associated type for result type of EmailTransport 2018-04-17 00:21:43 +02:00
Alexis Mousset
f949dd53ed Update book.json 2018-04-15 19:12:07 +02:00
Alexis Mousset
d990ab4de3 Merge pull request #265 from amousset/clean-buildscripts
feat(all): Add set -xe option to build scripts
2018-04-15 18:57:20 +02:00
Alexis Mousset
c489a0bdc2 feat(all): Add set -xe option to build scripts 2018-04-15 18:49:03 +02:00
Alexis Mousset
9dd08ad4c2 Update .travis.yml 2018-04-15 18:33:00 +02:00
Alexis Mousset
4313207896 Update .travis.yml 2018-04-15 18:05:20 +02:00
Alexis Mousset
944a236aa7 Update .travis.yml 2018-04-15 17:46:40 +02:00
Alexis Mousset
ddd80f5dcd Update .travis.yml 2018-04-15 16:00:14 +02:00
Alexis Mousset
530b595424 Update .travis.yml 2018-04-15 15:54:47 +02:00
Alexis Mousset
f985cf7559 Merge pull request #264 from amousset/use-deploy-script
feat(all): Move post-success scripts to separate files
2018-04-15 15:46:25 +02:00
Alexis Mousset
7b6ac2e677 feat(all): Move post-success scripts to separate files 2018-04-15 15:39:59 +02:00
Alexis Mousset
6797d3d3f5 Merge pull request #263 from amousset/typos
style(all): Fix typos
2018-04-15 13:45:42 +02:00
Alexis Mousset
bffe2978d2 style(all): Fix typos 2018-04-15 13:37:29 +02:00
Alexis Mousset
4d86840bc9 Update .travis.yml 2018-04-15 13:10:29 +02:00
Alexis Mousset
a7e5493aad Update .travis.yml 2018-04-15 13:04:27 +02:00
Alexis Mousset
d6828f5150 Merge pull request #262 from amousset/deploy-website-travis
feat(all): Add website upload to travis build script
2018-04-15 12:54:10 +02:00
Alexis Mousset
0b850e3b2f feat(all): Add website upload to travis build script 2018-04-15 12:52:49 +02:00
Alexis Mousset
57be14112a Remove doc redirect to lettre.at 2018-04-14 16:23:22 +02:00
Alexis Mousset
928fd413a4 Delete CNAME 2018-04-14 16:14:25 +02:00
Alexis Mousset
2d196599c7 Merge pull request #260 from amousset/code-coverage
feat(all): Add codecov upload in travis
2018-04-14 15:56:43 +02:00
Alexis Mousset
ad2fef9bbc feat(all): Add codecov upload in travis 2018-04-14 15:56:08 +02:00
Alexis Mousset
6577aa17d9 Merge pull request #259 from amousset/update-readme
feat(all): Update README to put useful links at the top
2018-04-14 15:18:16 +02:00
Alexis Mousset
34fc101f31 feat(all): Update README to put useful links at the top 2018-04-14 15:10:58 +02:00
Alexis Mousset
91f0cfa27c Merge pull request #258 from amousset/update-changelog
feat(all): Update changelog with 0.8.1
2018-04-14 14:44:20 +02:00
Alexis Mousset
78bd429310 feat(all): Update changelog with 0.8.1 2018-04-14 14:31:01 +02:00
Alexis Mousset
54e3dd3e41 Merge pull request #257 from amousset/update-badges
feat(all): Update badges in README and Cargo.toml
2018-04-14 14:27:32 +02:00
Alexis Mousset
36381eb345 feat(all): Update badges in README and Cargo.toml 2018-04-14 14:15:35 +02:00
Alexis Mousset
8d92dbb0c2 Create CNAME 2018-04-14 13:56:47 +02:00
Alexis Mousset
c4b1100bdb Merge pull request #256 from amousset/move-doc-gitbook
feat(all): Move docs from hugo to gitbook and move generated html to …
2018-04-14 13:24:36 +02:00
Alexis Mousset
b5e2c67dbd feat(all): Move docs from hugo to gitbook
Also move generated html to the lettre.github.io repo
2018-04-14 13:16:26 +02:00
Alexis Mousset
bf3bb78534 Merge pull request #254 from amousset/rename-master-to-0-9
feat(all): Change master version to 0.9.0-pre
2018-04-13 21:23:03 +02:00
Alexis Mousset
a914d9990c feat(all): Change master version to 0.9.0-pre 2018-04-13 20:59:23 +02:00
99 changed files with 6432 additions and 4342 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/CNAME
sudo pip install ghp-import
ghp-import -n _book
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

21
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@@ -0,0 +1,21 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Code allowing to reproduce the bug.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- Lettre version
- OS
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

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,31 +0,0 @@
language: rust
rust:
- stable
- beta
- nightly
- 1.20.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
- npm
before_script:
- smtp-sink 2525 1000&
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
- sudo npm set strict-ssl false && sudo npm install -g gitbook-cli
script:
- cargo test --verbose --all
after_success:
- ./.build-scripts/codecov.sh
- '[ "$TRAVIS_BRANCH" = "v0.8.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'

View File

@@ -1,3 +1,90 @@
<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
* 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)
#### Bug Fixes
* **email:**
* Fix compilation with Rust 1.36+ ([393ef8d](https://github.com/lettre/lettre/commit/393ef8dcd1b1c6a6119d0666d5f09b12f50f6b4b))
<a name="v0.9.1"></a>
### v0.9.1 (2019-05-05)
#### Features
* **email:**
* Re-export mime crate ([a0c8fb9](https://github.com/lettre/lettre/commit/a0c8fb9))
<a name="v0.9.0"></a>
### v0.9.0 (2019-03-17)
#### Bug Fixes
* **email:**
* Inserting 'from' from envelope into message headers ([058fa69](https://github.com/lettre/lettre/commit/058fa69))
* Do not include Bcc addresses in headers ([ee31bbe](https://github.com/lettre/lettre/commit/ee31bbe))
* **transport:**
* Write timeout is not set in smtp transport ([d71b560](https://github.com/lettre/lettre/commit/d71b560))
* Client::read_response infinite loop ([72f3cd8](https://github.com/lettre/lettre/commit/72f3cd8))
#### Features
* **all:**
* Update dependencies
* Start using the failure crate for errors ([c10fe3d](https://github.com/lettre/lettre/commit/c10fe3d))
* **transport:**
* Remove TLS 1.1 in accepted protocols by default (only allow TLS 1.2) ([4b48bdb](https://github.com/lettre/lettre/commit/4b48bdb))
* Initial support for XOAUTH2 ([ed7c164](https://github.com/lettre/lettre/commit/ed7c164))
* Remove support for CRAM-MD5 ([bc09aa2](https://github.com/lettre/lettre/commit/bc09aa2))
* SMTP connection pool implementation with r2d2 ([434654e](https://github.com/lettre/lettre/commit/434654e))
* Use md-5 and hmac instead of rust-crypto ([e7e0f34](https://github.com/lettre/lettre/commit/e7e0f34))
* Gmail transport simple example ([a8d8e2a](https://github.com/lettre/lettre/commit/a8d8e2a))
* **email:**
* Add In-Reply-To and References headers ([fc91bb6](https://github.com/lettre/lettre/commit/fc91bb6))
* Remove non-chaining builder methods ([1baf8a9](https://github.com/lettre/lettre/commit/1baf8a9))
<a name="v0.8.2"></a>
### v0.8.2 (2018-05-03)

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

104
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,52 +38,51 @@ 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.20 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.8"
lettre_email = "0.8"
lettre = "0.9"
lettre_email = "0.9"
```
```rust,no_run
extern crate lettre;
extern crate lettre_email;
extern crate mime;
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.")
.attachment(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN).unwrap()
.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
@@ -93,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,50 +0,0 @@
[package]
name = "lettre"
version = "0.8.2" # 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 = "^3.2", optional = true }
bufstream = { version = "^0.1", optional = true }
native-tls = { version = "^0.1", optional = true }
base64 = { version = "^0.9", optional = true }
hex = { version = "^0.3", optional = true }
hostname = { version = "^0.1", optional = true }
md-5 = { version = "^0.7", optional = true }
hmac = { version = "^0.6", optional = true }
serde = { version = "^1.0", optional = true }
serde_json = { version = "^1.0", optional = true }
serde_derive = { version = "^1.0", optional = true }
[dev-dependencies]
env_logger = "^0.5"
glob = "0.2"
[features]
default = ["file-transport", "crammd5-auth", "smtp-transport", "sendmail-transport"]
unstable = []
serde-impls = ["serde", "serde_derive"]
file-transport = ["serde-impls", "serde_json"]
crammd5-auth = ["md-5", "hmac", "hex"]
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
sendmail-transport = []
[[example]]
name = "smtp"
required-features = ["smtp-transport"]

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
#![feature(test)]
extern crate lettre;
extern crate test;
use lettre::{ClientSecurity, SmtpTransport};
use lettre::{EmailAddress, EmailTransport, SimpleSendableEmail};
use lettre::smtp::ConnectionReuseParameters;
#[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 = SimpleSendableEmail::new(
EmailAddress::new("user@localhost".to_string()),
vec![EmailAddress::new("root@localhost".to_string())],
"id".to_string(),
"Hello world".to_string(),
);
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 = SimpleSendableEmail::new(
EmailAddress::new("user@localhost".to_string()),
vec![EmailAddress::new("root@localhost".to_string())],
"id".to_string(),
"Hello world".to_string(),
);
let result = sender.send(&email);
assert!(result.is_ok());
});
sender.close()
}

View File

@@ -1,30 +0,0 @@
extern crate env_logger;
extern crate lettre;
use lettre::{EmailTransport, SimpleSendableEmail, SmtpTransport};
fn main() {
env_logger::init();
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"my-message-id".to_string(),
"Hello ß☺ example".to_string(),
).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);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

View File

@@ -1,54 +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 EmailTransport;
use SendableEmail;
use SimpleSendableEmail;
use file::error::FileResult;
use serde_json;
use std::fs::File;
use std::io::Read;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
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 FileEmailTransport {
path: PathBuf,
}
impl FileEmailTransport {
/// Creates a new transport to the given directory
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport {
let mut path_buf = PathBuf::new();
path_buf.push(path);
FileEmailTransport { path: path_buf }
}
}
impl<'a, T: Read + 'a> EmailTransport<'a, T, FileResult> for FileEmailTransport {
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> FileResult {
let mut file = self.path.clone();
file.push(format!("{}.txt", email.message_id()));
let mut f = File::create(file.as_path())?;
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
let simple_email = SimpleSendableEmail::new_with_envelope(
email.envelope().clone(),
email.message_id().to_string(),
message_content,
);
f.write_all(serde_json::to_string(&simple_email)?.as_bytes())?;
Ok(())
}
}

View File

@@ -1,279 +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. To be sendable, the
//! emails have to implement `SendableEmail`.
//!
#![doc(html_root_url = "https://docs.rs/lettre/0.8.2")]
#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts,
trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces,
unused_qualifications)]
#[cfg(feature = "smtp-transport")]
extern crate base64;
#[cfg(feature = "smtp-transport")]
extern crate bufstream;
#[cfg(feature = "crammd5-auth")]
extern crate hex;
#[cfg(feature = "crammd5-auth")]
extern crate hmac;
#[cfg(feature = "smtp-transport")]
extern crate hostname;
#[macro_use]
extern crate log;
#[cfg(feature = "crammd5-auth")]
extern crate md5;
#[cfg(feature = "smtp-transport")]
extern crate native_tls;
#[cfg(feature = "smtp-transport")]
#[macro_use]
extern crate nom;
#[cfg(feature = "serde-impls")]
#[macro_use]
extern crate serde_derive;
#[cfg(feature = "file-transport")]
extern crate serde_json;
#[cfg(feature = "smtp-transport")]
pub mod smtp;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
pub mod stub;
#[cfg(feature = "file-transport")]
pub mod file;
#[cfg(feature = "file-transport")]
pub use file::FileEmailTransport;
#[cfg(feature = "sendmail-transport")]
pub use sendmail::SendmailTransport;
#[cfg(feature = "smtp-transport")]
pub use smtp::{ClientSecurity, SmtpTransport};
#[cfg(feature = "smtp-transport")]
pub use smtp::client::net::ClientTlsParameters;
use std::fmt::{self, Display, Formatter};
use std::io::Read;
use std::error::Error as StdError;
use std::str::FromStr;
/// 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 StdError for Error {
fn description(&self) -> &str {
match *self {
Error::MissingFrom => "missing source address, invalid envelope",
Error::MissingTo => "missing destination address, invalid envelope",
Error::InvalidEmailAddress => "invalid email address",
}
}
fn cause(&self) -> Option<&StdError> {
None
}
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
/// Email result type
pub type EmailResult<T> = Result<T, Error>;
/// Email address
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EmailAddress(String);
impl EmailAddress {
/// Creates a new `EmailAddress`. For now it makes no validation.
pub fn new(address: String) -> EmailResult<EmailAddress> {
// TODO make some basic sanity checks
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)
}
}
/// 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()
}
/// Creates a new builder
pub fn builder() -> EnvelopeBuilder {
EnvelopeBuilder::new()
}
}
/// Simple email envelope representation
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct EnvelopeBuilder {
/// The envelope recipients' addresses
to: Vec<EmailAddress>,
/// The envelope sender address
from: Option<EmailAddress>,
}
impl EnvelopeBuilder {
/// Constructs an envelope with no recipients and an empty sender
pub fn new() -> Self {
EnvelopeBuilder {
to: vec![],
from: None,
}
}
/// Adds a recipient
pub fn to<S: Into<EmailAddress>>(mut self, address: S) -> Self {
self.add_to(address);
self
}
/// Adds a recipient
pub fn add_to<S: Into<EmailAddress>>(&mut self, address: S) {
self.to.push(address.into());
}
/// Sets the sender
pub fn from<S: Into<EmailAddress>>(mut self, address: S) -> Self {
self.set_from(address);
self
}
/// Sets the sender
pub fn set_from<S: Into<EmailAddress>>(&mut self, address: S) {
self.from = Some(address.into());
}
/// Build the envelope
pub fn build(self) -> EmailResult<Envelope> {
Envelope::new(self.from, self.to)
}
}
/// Email sendable by an SMTP client
pub trait SendableEmail<'a, T: Read + 'a> {
/// Envelope
fn envelope(&self) -> Envelope;
/// Message ID, used for logging
fn message_id(&self) -> String;
/// Message content
fn message(&'a self) -> Box<T>;
}
/// Transport method for emails
pub trait EmailTransport<'a, U: Read + 'a, V> {
/// Sends the email
fn send<T: SendableEmail<'a, U> + 'a>(&mut self, email: &'a T) -> V;
}
/// Minimal email structure
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct SimpleSendableEmail {
/// Envelope
envelope: Envelope,
/// Message ID
message_id: String,
/// Message content
message: Vec<u8>,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(
from_address: String,
to_addresses: &[String],
message_id: String,
message: String,
) -> EmailResult<SimpleSendableEmail> {
let to: Result<Vec<EmailAddress>, Error> = to_addresses
.iter()
.map(|x| EmailAddress::new(x.clone()))
.collect();
Ok(SimpleSendableEmail::new_with_envelope(
Envelope::new(Some(EmailAddress::new(from_address)?), to?)?,
message_id,
message,
))
}
/// Returns a new email from a valid envelope
pub fn new_with_envelope(
envelope: Envelope,
message_id: String,
message: String,
) -> SimpleSendableEmail {
SimpleSendableEmail {
envelope,
message_id,
message: message.into_bytes(),
}
}
}
impl<'a> SendableEmail<'a, &'a [u8]> for SimpleSendableEmail {
fn envelope(&self) -> Envelope {
self.envelope.clone()
}
fn message_id(&self) -> String {
self.message_id.clone()
}
fn message(&'a self) -> Box<&[u8]> {
Box::new(self.message.as_slice())
}
}

View File

@@ -1,52 +0,0 @@
//! Error and result type for sendmail transport
use self::Error::*;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(&'static str),
/// IO error
Io(io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Client(err) => err,
Io(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
Io(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
}
/// sendmail result type
pub type SendmailResult = Result<(), Error>;

View File

@@ -1,80 +0,0 @@
//! The sendmail transport sends the email using the local sendmail command.
//!
use {EmailTransport, SendableEmail};
use sendmail::error::SendmailResult;
use std::io::Read;
use std::io::prelude::*;
use std::process::{Command, Stdio};
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, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTransport {
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SendmailResult {
let envelope = email.envelope();
// Spawn the sendmail command
let to_addresses: Vec<String> = envelope.to().iter().map(|x| x.to_string()).collect();
let mut process = Command::new(&self.command)
.args(&[
"-i",
"-f",
&match envelope.from() {
Some(address) => address.to_string(),
None => "\"\"".to_string(),
},
&to_addresses.join(" "),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
match process
.stdin
.as_mut()
.unwrap()
.write_all(message_content.as_bytes())
{
Ok(_) => (),
Err(error) => return Err(From::from(error)),
}
info!("Wrote message to stdin");
if let Ok(output) = process.wait_with_output() {
if output.status.success() {
Ok(())
} else {
Err(From::from("The message could not be sent"))
}
} else {
Err(From::from("The sendmail process stopped"))
}
}
}

View File

@@ -1,213 +0,0 @@
//! Provides authentication mechanisms
#[cfg(feature = "crammd5-auth")]
use md5::Md5;
#[cfg(feature = "crammd5-auth")]
use hmac::{Hmac, Mac};
#[cfg(feature = "crammd5-auth")]
use hex;
use smtp::NUL;
use smtp::error::Error;
use std::fmt::{self, Display, Formatter};
/// Accepted authentication mechanisms on an encrypted connection
/// Trying LOGIN last as it is deprecated.
#[cfg(feature = "crammd5-auth")]
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] =
&[Mechanism::Plain, Mechanism::CramMd5, Mechanism::Login];
/// Accepted authentication mechanisms on an encrypted connection
/// Trying LOGIN last as it is deprecated.
#[cfg(not(feature = "crammd5-auth"))]
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Accepted authentication mechanisms on an unencrypted connection
#[cfg(feature = "crammd5-auth")]
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::CramMd5];
/// Accepted authentication mechanisms on an unencrypted connection
/// When CRAMMD5 support is not enabled, no mechanisms are allowed.
#[cfg(not(feature = "crammd5-auth"))]
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
/// Convertable to user credentials
pub trait IntoCredentials {
/// Converts to a `Credentials` struct
fn into_credentials(self) -> Credentials;
}
impl IntoCredentials for Credentials {
fn into_credentials(self) -> Credentials {
self
}
}
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
fn into_credentials(self) -> Credentials {
let (username, password) = self;
Credentials::new(username.into(), password.into())
}
}
/// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Credentials {
username: String,
password: String,
}
impl Credentials {
/// Create a `Credentials` struct from username and password
pub fn new(username: String, password: String) -> Credentials {
Credentials { username, password }
}
}
/// Represents authentication mechanisms
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Mechanism {
/// PLAIN authentication mechanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
Plain,
/// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login,
/// CRAM-MD5 authentication mechanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195
#[cfg(feature = "crammd5-auth")]
CramMd5,
}
impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => "CRAM-MD5",
}
)
}
}
impl Mechanism {
/// Does the mechanism supports initial response
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
pub fn supports_initial_response(&self) -> bool {
match *self {
Mechanism::Plain => true,
Mechanism::Login => false,
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => false,
}
}
/// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases
pub fn response(
&self,
credentials: &Credentials,
challenge: Option<&str>,
) -> Result<String, Error> {
match *self {
Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"{}{}{}{}",
NUL, credentials.username, NUL, credentials.password
)),
},
Mechanism::Login => {
let decoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.username.to_string());
}
if vec!["Password", "Password:"].contains(&decoded_challenge) {
return Ok(credentials.password.to_string());
}
Err(Error::Client("Unrecognized challenge"))
}
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => {
let decoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
let mut hmac: Hmac<Md5> = Hmac::new_varkey(credentials.password.as_bytes())
.expect("md5 should support variable key size");
hmac.input(decoded_challenge.as_bytes());
Ok(format!(
"{} {}",
credentials.username,
hex::encode(hmac.result().code())
))
}
}
}
}
#[cfg(test)]
mod test {
use super::{Credentials, Mechanism};
#[test]
fn test_plain() {
let mechanism = Mechanism::Plain;
let credentials = Credentials::new("username".to_string(), "password".to_string());
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
"\u{0}username\u{0}password"
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
#[test]
fn test_login() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
assert_eq!(
mechanism.response(&credentials, Some("Username")).unwrap(),
"alice"
);
assert_eq!(
mechanism.response(&credentials, Some("Password")).unwrap(),
"wonderland"
);
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
#[cfg(feature = "crammd5-auth")]
fn test_cram_md5() {
let mechanism = Mechanism::CramMd5;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
assert_eq!(
mechanism
.response(
&credentials,
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")
)
.unwrap(),
"alice a540ebe4ef2304070bbc3c456c1f64c0"
);
assert!(mechanism.response(&credentials, None).is_err());
}
}

View File

@@ -1,306 +0,0 @@
//! SMTP client
use bufstream::BufStream;
use nom::ErrorKind as NomErrorKind;
use smtp::{CRLF, MESSAGE_ENDING};
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 net;
pub mod mock;
/// 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(CRLF, "<CRLF>")
}
/// Structure that implements the SMTP client
#[derive(Debug, Default)]
pub struct Client<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(new_without_default_derive))]
impl<S: Write + Read> Client<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new() -> Client<S> {
Client { stream: None }
}
}
impl<S: Connector + Write + Read + Timeout + Debug> Client<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(wrong_self_convention))]
pub fn is_connected(&mut self) -> bool {
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<T: Read>(&mut self, mut message: Box<T>) -> SmtpResult {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new();
let mut message_reader = BufReader::new(message.as_mut());
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(MESSAGE_ENDING.as_bytes())?;
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
self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
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 compared to tls-native defaults.
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv11, 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(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(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,433 +0,0 @@
//! SMTP commands
use EmailAddress;
use base64;
use smtp::CRLF;
use smtp::authentication::{Credentials, Mechanism};
use smtp::error::Error;
use smtp::extension::{MailParameter, RcptParameter};
use smtp::extension::ClientId;
use smtp::response::Response;
use std::fmt::{self, Display, Formatter};
/// EHLO command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EhloCommand {
client_id: ClientId,
}
impl Display for EhloCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EHLO {}", self.client_id)?;
f.write_str(CRLF)
}
}
impl EhloCommand {
/// Creates a EHLO command
pub fn new(client_id: ClientId) -> EhloCommand {
EhloCommand { client_id }
}
}
/// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct StarttlsCommand;
impl Display for StarttlsCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("STARTTLS")?;
f.write_str(CRLF)
}
}
/// MAIL command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct MailCommand {
sender: Option<EmailAddress>,
parameters: Vec<MailParameter>,
}
impl Display for MailCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"MAIL FROM:<{}>",
match self.sender {
Some(ref address) => address.to_string(),
None => "".to_string(),
}
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str(CRLF)
}
}
impl MailCommand {
/// Creates a MAIL command
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
MailCommand { sender, parameters }
}
}
/// RCPT command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct RcptCommand {
recipient: EmailAddress,
parameters: Vec<RcptParameter>,
}
impl Display for RcptCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str(CRLF)
}
}
impl RcptCommand {
/// Creates an RCPT command
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
RcptCommand {
recipient,
parameters,
}
}
}
/// DATA command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct DataCommand;
impl Display for DataCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("DATA")?;
f.write_str(CRLF)
}
}
/// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct QuitCommand;
impl Display for QuitCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("QUIT")?;
f.write_str(CRLF)
}
}
/// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct NoopCommand;
impl Display for NoopCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NOOP")?;
f.write_str(CRLF)
}
}
/// HELP command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct HelpCommand {
argument: Option<String>,
}
impl Display for HelpCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("HELP")?;
if self.argument.is_some() {
write!(f, " {}", self.argument.as_ref().unwrap())?;
}
f.write_str(CRLF)
}
}
impl HelpCommand {
/// Creates an HELP command
pub fn new(argument: Option<String>) -> HelpCommand {
HelpCommand { argument }
}
}
/// VRFY command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct VrfyCommand {
argument: String,
}
impl Display for VrfyCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "VRFY {}", self.argument)?;
f.write_str(CRLF)
}
}
impl VrfyCommand {
/// Creates a VRFY command
pub fn new(argument: String) -> VrfyCommand {
VrfyCommand { argument }
}
}
/// EXPN command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct ExpnCommand {
argument: String,
}
impl Display for ExpnCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EXPN {}", self.argument)?;
f.write_str(CRLF)
}
}
impl ExpnCommand {
/// Creates an EXPN command
pub fn new(argument: String) -> ExpnCommand {
ExpnCommand { argument }
}
}
/// RSET command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct RsetCommand;
impl Display for RsetCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("RSET")?;
f.write_str(CRLF)
}
}
/// AUTH command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct AuthCommand {
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
response: Option<String>,
}
impl Display for AuthCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = if self.response.is_some() {
Some(base64::encode_config(
self.response.as_ref().unwrap().as_bytes(),
base64::STANDARD,
))
} else {
None
};
if self.mechanism.supports_initial_response() {
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap(),)?;
} else {
match encoded_response {
Some(response) => f.write_str(&response)?,
None => write!(f, "AUTH {}", self.mechanism)?,
}
}
f.write_str(CRLF)
}
}
impl AuthCommand {
/// Creates an AUTH command (from a challenge if provided)
pub fn new(
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
) -> Result<AuthCommand, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?)
} else {
None
};
Ok(AuthCommand {
mechanism,
credentials,
challenge,
response,
})
}
/// Creates an AUTH command from a response that needs to be a
/// valid challenge (with 334 response code)
pub fn new_from_response(
mechanism: Mechanism,
credentials: Credentials,
response: &Response,
) -> Result<AuthCommand, Error> {
if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge"));
}
let encoded_challenge = match response.first_word() {
Some(challenge) => challenge.to_string(),
None => return Err(Error::ResponseParsing("Could not read auth challenge")),
};
debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = match base64::decode(&encoded_challenge) {
Ok(challenge) => match String::from_utf8(challenge) {
Ok(value) => value,
Err(error) => return Err(Error::Utf8Parsing(error)),
},
Err(error) => return Err(Error::ChallengeParsing(error)),
};
debug!("auth decoded challenge: {}", decoded_challenge);
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
Ok(AuthCommand {
mechanism,
credentials,
challenge: Some(decoded_challenge),
response,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use smtp::extension::MailBodyParameter;
#[cfg(feature = "crammd5-auth")]
use smtp::response::{Category, Code, Detail, Severity};
#[test]
fn test_display() {
let id = ClientId::Domain("localhost".to_string());
let email = EmailAddress::new("test@example.com".to_string()).unwrap();
let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
};
let rcpt_parameter = RcptParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
};
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
assert_eq!(
format!("{}", MailCommand::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!(
"{}",
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])
),
"MAIL FROM:<test@example.com> SIZE=42\r\n"
);
assert_eq!(
format!(
"{}",
MailCommand::new(
Some(email.clone()),
vec![
MailParameter::Size(42),
MailParameter::Body(MailBodyParameter::EightBitMime),
mail_parameter,
],
)
),
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
);
assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![])),
"RCPT TO:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", RcptCommand::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!("{}", HelpCommand::new(Some("test".to_string()))),
"HELP test\r\n"
);
assert_eq!(
format!("{}", VrfyCommand::new("test".to_string())),
"VRFY test\r\n"
);
assert_eq!(
format!("{}", ExpnCommand::new("test".to_string())),
"EXPN test\r\n"
);
assert_eq!(format!("{}", RsetCommand), "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 PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
);
#[cfg(feature = "crammd5-auth")]
assert_eq!(
format!(
"{}",
AuthCommand::new(
Mechanism::CramMd5,
credentials.clone(),
Some("test".to_string()),
).unwrap()
),
"dXNlciAzMTYxY2NmZDdmMjNlMzJiYmMzZTQ4NjdmYzk0YjE4Nw==\r\n"
);
assert_eq!(
format!(
"{}",
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
),
"AUTH LOGIN\r\n"
);
#[cfg(feature = "crammd5-auth")]
assert_eq!(
format!(
"{}",
AuthCommand::new_from_response(
Mechanism::CramMd5,
credentials.clone(),
&Response::new(
Code::new(
Severity::PositiveIntermediate,
Category::Unspecified3,
Detail::Four,
),
vec!["dGVzdAo=".to_string()],
),
).unwrap()
),
"dXNlciA1NTIzNThiMzExOWFjOWNkYzM2YWRiN2MxNWRmMWJkNw==\r\n"
);
}
}

View File

@@ -1,121 +0,0 @@
//! Error and result type for SMTP clients
use self::Error::*;
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;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Response),
/// Error parsing a response
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(DecodeError),
/// Error parsing UTF8in response
Utf8Parsing(FromUtf8Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
Resolution,
/// IO error
Io(io::Error),
/// TLS error
Tls(native_tls::Error),
/// Parsing error
Parsing(nom::simple_errors::Err),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl StdError for Error {
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
fn description(&self) -> &str {
match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => match err.first_line() {
Some(line) => line,
None => "undetailed transient error during SMTP transaction",
},
Permanent(ref err) => match err.first_line() {
Some(line) => line,
None => "undetailed permanent error during SMTP transaction",
},
ResponseParsing(err) => err,
ChallengeParsing(ref err) => err.description(),
Utf8Parsing(ref err) => err.description(),
Resolution => "could not resolve hostname",
Client(err) => err,
Io(ref err) => err.description(),
Tls(ref err) => err.description(),
Parsing(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err),
Tls(ref err) => Some(&*err),
Parsing(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
}
}
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Tls(err)
}
}
impl From<nom::simple_errors::Err> for Error {
fn from(err: nom::simple_errors::Err) -> Error {
Parsing(err)
}
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.code.severity {
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
}
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
}
/// SMTP result type
pub type SmtpResult = Result<Response, Error>;

View File

@@ -1,485 +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
//! CRAM-MD5 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
use EmailTransport;
use SendableEmail;
use native_tls::TlsConnector;
use smtp::authentication::{Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS,
DEFAULT_UNENCRYPTED_MECHANISMS};
use smtp::client::Client;
use smtp::client::net::ClientTlsParameters;
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
use smtp::commands::*;
use smtp::error::{Error, SmtpResult};
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
use std::io::Read;
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
pub mod extension;
pub mod commands;
pub mod authentication;
pub mod response;
pub mod client;
pub mod error;
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;
// Useful strings and characters
/// The word separator for SMTP transactions
pub const SP: &str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub const CRLF: &str = "\r\n";
/// Colon
pub const COLON: &str = ":";
/// The ending of message content
pub const MESSAGE_ENDING: &str = "\r\n.\r\n";
/// NUL unicode character
pub const NUL: &str = "\0";
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub enum ClientSecurity {
/// Insecure connection
None,
/// Use `STARTTLS` when available
Opportunistic(ClientTlsParameters),
/// Always use `STARTTLS`
Required(ClientTlsParameters),
/// Use TLS wrapped connection without negotiation
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
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)]
pub struct SmtpTransportBuilder {
/// 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 SmtpTransportBuilder {
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No connection reuse
/// * No authentication
/// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands
pub fn new<A: ToSocketAddrs>(
addr: A,
security: ClientSecurity,
) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = addr.to_socket_addrs()?;
match addresses.next() {
Some(addr) => Ok(SmtpTransportBuilder {
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),
}
}
/// Enable SMTPUTF8 if the server supports it
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder {
self.smtp_utf8 = enabled;
self
}
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> SmtpTransportBuilder {
self.hello_name = name;
self
}
/// Enable connection reuse
pub fn connection_reuse(
mut self,
parameters: ConnectionReuseParameters,
) -> SmtpTransportBuilder {
self.connection_reuse = parameters;
self
}
/// Set the client credentials
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpTransportBuilder {
self.credentials = Some(credentials.into());
self
}
/// Set the authentication mechanism to use
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpTransportBuilder {
self.authentication_mechanism = Some(mechanism);
self
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpTransportBuilder {
self.timeout = timeout;
self
}
/// Build the SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn build(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: SmtpTransportBuilder,
/// Low level client
client: Client,
}
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 {
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submission port, using the provided domain
/// to validate TLS certificates.
pub fn simple_builder(domain: &str) -> Result<SmtpTransportBuilder, Error> {
let mut tls_builder = TlsConnector::builder()?;
tls_builder.supported_protocols(DEFAULT_TLS_PROTOCOLS)?;
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpTransportBuilder::new(
(domain, SUBMISSION_PORT),
ClientSecurity::Required(tls_parameters),
)
}
/// Creates a new configurable builder
pub fn builder<A: ToSocketAddrs>(
addr: A,
security: ClientSecurity,
) -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(addr, security)
}
/// Creates a new local SMTP client to port 25
pub fn builder_unencrypted_localhost() -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(("localhost", SMTP_PORT), ClientSecurity::None)
}
/// Creates a new SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
let client = Client::new();
SmtpTransport {
client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
/// 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, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
/// Sends an email
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms, cyclomatic_complexity))]
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SmtpResult {
// Extract email information
let message_id = email.message_id();
let envelope = email.envelope();
// 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 {
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");
}
}
}
// 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(envelope.from().cloned(), mail_options,)),
self
);
// Log the mail command
info!(
"{}: from=<{}>",
message_id,
match envelope.from() {
Some(address) => address.to_string(),
None => "".to_string(),
}
);
// Recipient
for to_address in 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(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,44 +0,0 @@
//! The stub transport only logs message envelope and drops the content. It can be useful for
//! testing purposes.
//!
use EmailTransport;
use SendableEmail;
use std::io::Read;
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
pub struct StubEmailTransport {
response: StubResult,
}
impl StubEmailTransport {
/// Creates a new transport that always returns the given response
pub fn new(response: StubResult) -> StubEmailTransport {
StubEmailTransport { response }
}
/// Creates a new transport that always returns a success response
pub fn new_positive() -> StubEmailTransport {
StubEmailTransport { response: Ok(()) }
}
}
/// SMTP result type
pub type StubResult = Result<(), ()>;
impl<'a, T: Read + 'a> EmailTransport<'a, T, StubResult> for StubEmailTransport {
fn send<U: SendableEmail<'a, T>>(&mut self, email: &'a U) -> StubResult {
let envelope = email.envelope();
info!(
"{}: from=<{}> to=<{:?}>",
email.message_id(),
match envelope.from() {
Some(address) => address.to_string(),
None => "".to_string(),
},
envelope.to()
);
self.response
}
}

View File

@@ -1,62 +0,0 @@
extern crate glob;
use self::glob::glob;
use std::env::consts::EXE_EXTENSION;
use std::env;
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,40 +0,0 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "file-transport")]
mod test {
use lettre::{EmailTransport, SendableEmail, SimpleSendableEmail};
use lettre::file::FileEmailTransport;
use std::env::temp_dir;
use std::fs::File;
use std::fs::remove_file;
use std::io::Read;
#[test]
fn file_transport() {
let mut sender = FileEmailTransport::new(temp_dir());
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"file_id".to_string(),
"Hello file".to_string(),
).unwrap();
let result = sender.send(&email);
assert!(result.is_ok());
let message_id = email.message_id();
let file = format!("{}/{}.txt", 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\":\"file_id\",\"message\":[72,101,108,108,111,32,102,105,108,101]}"
);
remove_file(file).unwrap();
}
}

View File

@@ -1,25 +0,0 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "sendmail-transport")]
mod test {
use lettre::{EmailTransport, SimpleSendableEmail};
use lettre::sendmail::SendmailTransport;
#[test]
fn sendmail_transport_simple() {
let mut sender = SendmailTransport::new();
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"sendmail_id".to_string(),
"Hello sendmail".to_string(),
).unwrap();
let result = sender.send(&email);
println!("{:?}", result);
assert!(result.is_ok());
}
}

View File

@@ -1,24 +0,0 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "smtp-transport")]
mod test {
use lettre::{ClientSecurity, EmailTransport, SimpleSendableEmail, SmtpTransport};
#[test]
fn smtp_transport_simple() {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.build();
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"smtp_id".to_string(),
"Hello smtp".to_string(),
).unwrap();
sender.send(&email).unwrap();
}
}

View File

@@ -1,19 +0,0 @@
extern crate lettre;
use lettre::{EmailTransport, SimpleSendableEmail};
use lettre::stub::StubEmailTransport;
#[test]
fn stub_transport() {
let mut sender_ok = StubEmailTransport::new_positive();
let mut sender_ko = StubEmailTransport::new(Err(()));
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"stub_id".to_string(),
"Hello stub".to_string(),
).unwrap();
sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err();
}

View File

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

View File

@@ -1,31 +0,0 @@
[package]
name = "lettre_email"
version = "0.8.2" # 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.8", path = "../lettre", features = ["smtp-transport"] }
glob = "0.2"
[dependencies]
email = "^0.0"
mime = "^0.3"
time = "^0.1"
uuid = { version = "^0.6", features = ["v4"] }
lettre = { version = "^0.8", path = "../lettre", default-features = false }
base64 = "^0.9"

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
extern crate lettre;
extern crate lettre_email;
extern crate mime;
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.")
.attachment(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN).unwrap()
.build()
.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);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

View File

@@ -1,47 +0,0 @@
//! Error and result type for emails
use self::Error::*;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
use lettre;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Envelope error
Email(lettre::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(self.description())
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Email(ref err) => err.description(),
CannotParseFilename => "the attachment filename could not be parsed",
Io(ref err) => err.description(),
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
}
}
impl From<lettre::Error> for Error {
fn from(err: lettre::Error) -> Error {
Email(err)
}
}

View File

@@ -1,947 +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.8.2")]
#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts,
trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces,
unused_qualifications)]
extern crate base64;
extern crate email as email_format;
extern crate lettre;
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::{EmailAddress, Envelope, Error as EmailError, SendableEmail};
use mime::Mime;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use time::{now, Tm};
use uuid::Uuid;
use std::str::FromStr;
/// Converts an address or an address with an alias to a `Header`
pub trait IntoHeader {
/// Converts to a `Header` struct
fn into_header(self) -> Header;
}
impl IntoHeader for Header {
fn into_header(self) -> Header {
self
}
}
impl<S: Into<String>, T: Into<String>> IntoHeader for (S, T) {
fn into_header(self) -> Header {
let (name, value) = self;
Header::new(name.into(), value.into())
}
}
/// Converts an address or an address with an alias to a `Mailbox`
pub trait IntoMailbox {
/// Converts to a `Mailbox` struct
fn into_mailbox(self) -> Mailbox;
}
impl IntoMailbox for Mailbox {
fn into_mailbox(self) -> Mailbox {
self
}
}
impl<'a> IntoMailbox for &'a str {
fn into_mailbox(self) -> Mailbox {
Mailbox::new(self.into())
}
}
impl IntoMailbox for String {
fn into_mailbox(self) -> Mailbox {
Mailbox::new(self)
}
}
impl<S: Into<String>, T: Into<String>> IntoMailbox for (S, T) {
fn into_mailbox(self) -> Mailbox {
let (address, alias) = self;
Mailbox::new_with_name(alias.into(), address.into())
}
}
/// Can be transformed to a sendable email
pub trait IntoEmail {
/// Builds an email
fn into_email(self) -> Result<Email, Error>;
}
impl IntoEmail for SimpleEmail {
fn into_email(self) -> Result<Email, Error> {
let mut builder = EmailBuilder::new();
if self.from.is_some() {
builder.add_from(self.from.unwrap());
}
for to_address in self.to {
builder.add_to(to_address.into_mailbox());
}
for cc_address in self.cc {
builder.add_cc(cc_address.into_mailbox());
}
if self.reply_to.is_some() {
builder.add_reply_to(self.reply_to.unwrap().into_mailbox());
}
if self.subject.is_some() {
builder.set_subject(self.subject.unwrap());
}
// No date for now
match (self.text, self.html) {
(Some(text), Some(html)) => builder.set_alternative(html, text),
(Some(text), None) => builder.set_text(text),
(None, Some(html)) => builder.set_html(html),
(None, None) => (),
}
for header in self.headers {
builder.add_header(header.into_header());
}
builder.build()
}
}
/// Simple representation of an email, useful for some transports
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct SimpleEmail {
from: Option<Mailbox>,
to: Vec<Mailbox>,
cc: Vec<Mailbox>,
bcc: Vec<Mailbox>,
reply_to: Option<Mailbox>,
subject: Option<String>,
date: Option<Tm>,
html: Option<String>,
text: Option<String>,
attachments: Vec<String>,
headers: Vec<Header>,
}
impl SimpleEmail {
/// Adds a generic header
pub fn header<A: IntoHeader>(mut self, header: A) -> SimpleEmail {
self.add_header(header);
self
}
/// Adds a generic header
pub fn add_header<A: IntoHeader>(&mut self, header: A) {
self.headers.push(header.into_header());
}
/// Adds a `From` header and stores the sender address
pub fn from<A: IntoMailbox>(mut self, address: A) -> SimpleEmail {
self.add_from(address);
self
}
/// Adds a `From` header and stores the sender address
pub fn add_from<A: IntoMailbox>(&mut self, address: A) {
self.from = Some(address.into_mailbox());
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: IntoMailbox>(mut self, address: A) -> SimpleEmail {
self.add_to(address);
self
}
/// Adds a `To` header and stores the recipient address
pub fn add_to<A: IntoMailbox>(&mut self, address: A) {
self.to.push(address.into_mailbox());
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: IntoMailbox>(mut self, address: A) -> SimpleEmail {
self.add_cc(address);
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn add_cc<A: IntoMailbox>(&mut self, address: A) {
self.cc.push(address.into_mailbox());
}
/// Adds a `Bcc` header and stores the recipient address
pub fn bcc<A: IntoMailbox>(mut self, address: A) -> SimpleEmail {
self.add_bcc(address);
self
}
/// Adds a `Bcc` header and stores the recipient address
pub fn add_bcc<A: IntoMailbox>(&mut self, address: A) {
self.bcc.push(address.into_mailbox());
}
/// Adds a `Reply-To` header
pub fn reply_to<A: IntoMailbox>(mut self, address: A) -> SimpleEmail {
self.add_reply_to(address);
self
}
/// Adds a `Reply-To` header
pub fn add_reply_to<A: IntoMailbox>(&mut self, address: A) {
self.reply_to = Some(address.into_mailbox());
}
/// Adds a `Subject` header
pub fn subject<S: Into<String>>(mut self, subject: S) -> SimpleEmail {
self.set_subject(subject);
self
}
/// Adds a `Subject` header
pub fn set_subject<S: Into<String>>(&mut self, subject: S) {
self.subject = Some(subject.into());
}
/// Adds a `Date` header with the given date
pub fn date(mut self, date: Tm) -> SimpleEmail {
self.set_date(date);
self
}
/// Adds a `Date` header with the given date
pub fn set_date(&mut self, date: Tm) {
self.date = Some(date);
}
/// Adds an attachment to the message
pub fn attachment<S: Into<String>>(mut self, path: S) -> SimpleEmail {
self.add_attachment(path);
self
}
/// Adds an attachment to the message
pub fn add_attachment<S: Into<String>>(&mut self, path: S) {
self.attachments.push(path.into());
}
/// Sets the email body to plain text content
pub fn text<S: Into<String>>(mut self, body: S) -> SimpleEmail {
self.set_text(body);
self
}
/// Sets the email body to plain text content
pub fn set_text<S: Into<String>>(&mut self, body: S) {
self.text = Some(body.into());
}
/// Sets the email body to HTML content
pub fn html<S: Into<String>>(mut self, body: S) -> SimpleEmail {
self.set_html(body);
self
}
/// Sets the email body to HTML content
pub fn set_html<S: Into<String>>(&mut self, body: S) {
self.html = Some(body.into());
}
}
/// Builds a `MimeMessage` structure
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct PartBuilder {
/// Message
message: MimeMessage,
}
impl Default for PartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Builds an `Email` structure
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct EmailBuilder {
/// Message
message: PartBuilder,
/// The recipients' addresses for the mail header
to_header: Vec<Address>,
/// The sender addresses for the mail header
from_header: Vec<Address>,
/// The Cc addresses for the mail header
cc_header: Vec<Address>,
/// The Bcc addresses for the mail header
bcc_header: Vec<Address>,
/// The Reply-To addresses for the mail header
reply_to_header: Vec<Address>,
/// The sender address for the mail header
sender_header: 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 PartBuilder {
/// Creates a new empty part
pub fn new() -> PartBuilder {
PartBuilder {
message: MimeMessage::new_blank_message(),
}
}
/// Adds a generic header
pub fn header<A: IntoHeader>(mut self, header: A) -> PartBuilder {
self.add_header(header);
self
}
/// Adds a generic header
pub fn add_header<A: IntoHeader>(&mut self, header: A) {
self.message.headers.insert(header.into_header());
}
/// Sets the body
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
self.set_body(body);
self
}
/// Sets the body
pub fn set_body<S: Into<String>>(&mut self, body: S) {
self.message.body = body.into();
}
/// Defines a `MimeMultipartType` value
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
self.set_message_type(mime_type);
self
}
/// Defines a `MimeMultipartType` value
pub fn set_message_type(&mut self, mime_type: MimeMultipartType) {
self.message.message_type = Some(mime_type);
}
/// Adds a `ContentType` header with the given MIME type
pub fn content_type(mut self, content_type: &Mime) -> PartBuilder {
self.set_content_type(content_type);
self
}
/// Adds a `ContentType` header with the given MIME type
pub fn set_content_type(&mut self, content_type: &Mime) {
self.add_header(("Content-Type", format!("{}", content_type).as_ref()));
}
/// Adds a child part
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
self.add_child(child);
self
}
/// Adds a child part
pub fn add_child(&mut self, child: MimeMessage) {
self.message.children.push(child);
}
/// 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_header: vec![],
from_header: vec![],
cc_header: vec![],
bcc_header: vec![],
reply_to_header: vec![],
sender_header: None,
envelope: None,
date_issued: false,
}
}
/// Sets the email body
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
self.message.set_body(body);
self
}
/// Sets the email body
pub fn set_body<S: Into<String>>(&mut self, body: S) {
self.message.set_body(body);
}
/// Add a generic header
pub fn header<A: IntoHeader>(mut self, header: A) -> EmailBuilder {
self.message.add_header(header);
self
}
/// Add a generic header
pub fn add_header<A: IntoHeader>(&mut self, header: A) {
self.message.add_header(header);
}
/// Adds a `From` header and stores the sender address
pub fn from<A: IntoMailbox>(mut self, address: A) -> EmailBuilder {
self.add_from(address);
self
}
/// Adds a `From` header and stores the sender address
pub fn add_from<A: IntoMailbox>(&mut self, address: A) {
let mailbox = address.into_mailbox();
self.from_header.push(Address::Mailbox(mailbox));
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: IntoMailbox>(mut self, address: A) -> EmailBuilder {
self.add_to(address);
self
}
/// Adds a `To` header and stores the recipient address
pub fn add_to<A: IntoMailbox>(&mut self, address: A) {
let mailbox = address.into_mailbox();
self.to_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: IntoMailbox>(mut self, address: A) -> EmailBuilder {
self.add_cc(address);
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn add_cc<A: IntoMailbox>(&mut self, address: A) {
let mailbox = address.into_mailbox();
self.cc_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Bcc` header and stores the recipient address
pub fn bcc<A: IntoMailbox>(mut self, address: A) -> EmailBuilder {
self.add_bcc(address);
self
}
/// Adds a `Bcc` header and stores the recipient address
pub fn add_bcc<A: IntoMailbox>(&mut self, address: A) {
let mailbox = address.into_mailbox();
self.bcc_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Reply-To` header
pub fn reply_to<A: IntoMailbox>(mut self, address: A) -> EmailBuilder {
self.add_reply_to(address);
self
}
/// Adds a `Reply-To` header
pub fn add_reply_to<A: IntoMailbox>(&mut self, address: A) {
let mailbox = address.into_mailbox();
self.reply_to_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Sender` header
pub fn sender<A: IntoMailbox>(mut self, address: A) -> EmailBuilder {
self.set_sender(address);
self
}
/// Adds a `Sender` header
pub fn set_sender<A: IntoMailbox>(&mut self, address: A) {
let mailbox = address.into_mailbox();
self.sender_header = Some(mailbox);
}
/// Adds a `Subject` header
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
self.set_subject(subject);
self
}
/// Adds a `Subject` header
pub fn set_subject<S: Into<String>>(&mut self, subject: S) {
self.message
.add_header(("Subject".to_string(), subject.into()));
}
/// Adds a `Date` header with the given date
pub fn date(mut self, date: &Tm) -> EmailBuilder {
self.set_date(date);
self
}
/// Adds a `Date` header with the given date
pub fn set_date(&mut self, date: &Tm) {
self.message
.add_header(("Date", Tm::rfc822z(date).to_string()));
self.date_issued = true;
}
/// Adds an attachment to the email
pub fn attachment(
mut self,
path: &Path,
filename: Option<&str>,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
self.set_attachment(path, filename, content_type)?;
Ok(self)
}
/// Adds an attachment to the email
/// If filename is not provided, the name of the file will be used.
pub fn set_attachment(
&mut self,
path: &Path,
filename: Option<&str>,
content_type: &Mime,
) -> Result<(), Error> {
let file = File::open(path);
let body = match file {
Ok(mut f) => {
let mut data = Vec::new();
let read = f.read_to_end(&mut data);
match read {
Ok(_) => data,
Err(e) => {
return Err(From::from(e));
}
}
}
Err(e) => {
return Err(From::from(e));
}
};
let actual_filename = match filename {
Some(name) => name,
None => match path.file_name() {
Some(name) => match name.to_str() {
Some(name) => name,
None => return Err(Error::CannotParseFilename),
},
None => return Err(Error::CannotParseFilename),
},
};
let encoded_body = base64::encode(&body);
let content = PartBuilder::new()
.body(encoded_body)
.header((
"Content-Disposition",
format!("attachment; filename=\"{}\"", actual_filename),
))
.header(("Content-Type", content_type.to_string()))
.header(("Content-Transfer-Encoding", "base64"))
.build();
self.set_message_type(MimeMultipartType::Mixed);
self.add_child(content);
Ok(())
}
/// Set the message type
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
self.set_message_type(message_type);
self
}
/// Set the message type
pub fn set_message_type(&mut self, message_type: MimeMultipartType) {
self.message.set_message_type(message_type);
}
/// Adds a child
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
self.add_child(child);
self
}
/// Adds a child
pub fn add_child(&mut self, child: MimeMessage) {
self.message.add_child(child);
}
/// Sets the email body to plain text content
pub fn text<S: Into<String>>(mut self, body: S) -> EmailBuilder {
self.set_text(body);
self
}
/// Sets the email body to plain text content
pub fn set_text<S: Into<String>>(&mut self, body: S) {
let text = PartBuilder::new()
.body(body)
.header((
"Content-Type",
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
))
.build();
self.add_child(text);
}
/// Sets the email body to HTML content
pub fn html<S: Into<String>>(mut self, body: S) -> EmailBuilder {
self.set_html(body);
self
}
/// Sets the email body to HTML content
pub fn set_html<S: Into<String>>(&mut self, body: S) {
let html = PartBuilder::new()
.body(body)
.header((
"Content-Type",
format!("{}", mime::TEXT_HTML_UTF_8).as_ref(),
))
.build();
self.add_child(html);
}
/// Sets the email content
pub fn alternative<S: Into<String>, T: Into<String>>(
mut self,
body_html: S,
body_text: T,
) -> EmailBuilder {
self.set_alternative(body_html, body_text);
self
}
/// Sets the email content
pub fn set_alternative<S: Into<String>, T: Into<String>>(
&mut self,
body_html: S,
body_text: T,
) {
let mut alternate = PartBuilder::new();
alternate.set_message_type(MimeMultipartType::Alternative);
let text = PartBuilder::new()
.body(body_text)
.header((
"Content-Type",
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
))
.build();
let html = PartBuilder::new()
.body(body_html)
.header((
"Content-Type",
format!("{}", mime::TEXT_HTML_UTF_8).as_ref(),
))
.build();
alternate.add_child(text);
alternate.add_child(html);
self.set_message_type(MimeMultipartType::Mixed);
self.add_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.set_envelope(envelope);
self
}
/// 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 set_envelope(&mut self, envelope: Envelope) {
self.envelope = Some(envelope);
}
/// 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_header.len() >= 2 && self.sender_header.is_none() {
// So, we must find something to put as Sender.
for possible_sender in &self.from_header {
// Only a mailbox can be used as sender, not Address::Group.
if let Address::Mailbox(ref mbx) = *possible_sender {
self.sender_header = 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_header.is_some());
}
// Add the sender header, if any.
if let Some(ref v) = self.sender_header {
self.message.add_header(("Sender", v.to_string().as_ref()));
}
// Calculate the envelope
let envelope = match self.envelope {
Some(e) => e,
None => {
// we need to generate the envelope
let mut e = Envelope::builder();
// add all receivers in to_header and cc_header
for receiver in self.to_header
.iter()
.chain(self.cc_header.iter())
.chain(self.bcc_header.iter())
{
match *receiver {
Address::Mailbox(ref m) => e.add_to(EmailAddress::from_str(&m.address)?),
Address::Group(_, ref ms) => for m in ms.iter() {
e.add_to(EmailAddress::from_str(&m.address.clone())?);
},
}
}
e.set_from(EmailAddress::from_str(&match self.sender_header {
Some(x) => x.address.clone(), // if we have a sender_header, use it
None => {
// use a from header
debug_assert!(self.from_header.len() <= 1); // else we'd have sender_header
match self.from_header.first() {
Some(a) => match *a {
// if we have a from header
Address::Mailbox(ref mailbox) => 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) => mailbox.address.clone(),
// for an empty author group (the rarest of the rare cases)
None => return Err(Error::Email(EmailError::MissingFrom)), // empty envelope sender
},
},
// if we don't have a from header
None => return Err(Error::Email(EmailError::MissingFrom)), // empty envelope sender
}
}
})?);
e.build()?
}
};
// 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_header.is_empty() {
self.message
.add_header(Header::new_with_value("To".into(), self.to_header).unwrap());
}
if !self.from_header.is_empty() {
self.message
.add_header(Header::new_with_value("From".into(), self.from_header).unwrap());
} else {
return Err(Error::Email(EmailError::MissingFrom));
}
if !self.cc_header.is_empty() {
self.message
.add_header(Header::new_with_value("Cc".into(), self.cc_header).unwrap());
}
if !self.bcc_header.is_empty() {
self.message
.add_header(Header::new_with_value("Bcc".into(), self.bcc_header).unwrap());
}
if !self.reply_to_header.is_empty() {
self.message.add_header(
Header::new_with_value("Reply-To".into(), self.reply_to_header).unwrap(),
);
}
if !self.date_issued {
self.message
.add_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
}
self.message.add_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.add_header(header)
}
Ok(Email {
message: self.message.build().as_string().into_bytes(),
envelope,
message_id,
})
}
}
impl<'a> SendableEmail<'a, &'a [u8]> for Email {
fn envelope(&self) -> Envelope {
self.envelope.clone()
}
fn message_id(&self) -> String {
self.message_id.to_string()
}
fn message(&'a self) -> Box<&[u8]> {
Box::new(self.message.as_slice())
}
}
/// Email sendable by any type of client, giving access to all fields
pub trait ExtractableEmail {
/// From address
fn from_address(&self) -> Option<String>;
/// To addresses
fn to_addresses(&self) -> Vec<String>;
/// Cc addresses
fn cc_addresses(&self) -> Vec<String>;
/// Bcc addresses
fn bcc_addresses(&self) -> Vec<String>;
/// Replay-To addresses
fn reply_to_address(&self) -> String;
/// Subject
fn subject(&self) -> String;
/// Message ID
fn message_id(&self) -> String;
/// Other Headers
fn headers(&self) -> Vec<String>;
/// html content
fn html(self) -> String;
/// text content
fn text(self) -> String;
}
#[cfg(test)]
mod test {
use super::EmailBuilder;
use lettre::{EmailAddress, SendableEmail};
use time::now;
#[test]
fn test_multiple_from() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = 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();
assert_eq!(
format!("{}", String::from_utf8_lossy(email.message().as_ref())),
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(),
email.message_id()
)
);
}
#[test]
fn test_email_builder() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = 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();
assert_eq!(
format!("{}", String::from_utf8_lossy(email.message().as_ref())),
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\
Bcc: <bcc@localhost>\r\nReply-To: <reply@localhost>\r\n\
MIME-Version: 1.0\r\nMessage-ID: \
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
date_now.rfc822z(),
email.message_id()
)
);
}
#[test]
fn test_email_sendable() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = 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();
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,48 +0,0 @@
extern crate glob;
use self::glob::glob;
use std::env::consts::EXE_EXTENSION;
use std::env;
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,10 +1,11 @@
//! Error and result type for file transport
use self::Error::*;
use serde_json;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
};
/// An enum of all error kinds.
#[derive(Debug)]
@@ -19,20 +20,16 @@ pub enum Error {
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
JsonSerialization(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Client(err) => err,
Io(ref err) => err.description(),
JsonSerialization(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
@@ -43,21 +40,18 @@ impl StdError for Error {
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
Error::Io(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
JsonSerialization(err)
Error::JsonSerialization(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
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

@@ -0,0 +1,52 @@
//! Error and result type for sendmail transport
use self::Error::*;
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(String),
/// Error parsing UTF8 in response
Utf8Parsing(FromUtf8Error),
/// IO error
Io(io::Error),
}
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 source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
}

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

@@ -0,0 +1,175 @@
//! Provides limited SASL authentication mechanisms
use crate::transport::smtp::error::Error;
use std::fmt::{self, Display, Formatter};
/// Accepted authentication mechanisms
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Convertible to user credentials
pub trait IntoCredentials {
/// Converts to a `Credentials` struct
fn into_credentials(self) -> Credentials;
}
impl IntoCredentials for Credentials {
fn into_credentials(self) -> Credentials {
self
}
}
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
fn into_credentials(self) -> Credentials {
let (username, password) = self;
Credentials::new(username.into(), password.into())
}
}
/// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Credentials {
authentication_identity: String,
secret: String,
}
impl Credentials {
/// Create a `Credentials` struct from username and password
pub fn new(username: String, password: String) -> Credentials {
Credentials {
authentication_identity: username,
secret: password,
}
}
}
/// Represents authentication mechanisms
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Mechanism {
/// PLAIN authentication mechanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
Plain,
/// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login,
/// Non-standard XOAUTH2 mechanism
/// https://developers.google.com/gmail/imap/xoauth2-protocol
Xoauth2,
}
impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
Mechanism::Xoauth2 => "XOAUTH2",
}
)
}
}
impl Mechanism {
/// Does the mechanism supports initial response
pub fn supports_initial_response(self) -> bool {
match self {
Mechanism::Plain | Mechanism::Xoauth2 => true,
Mechanism::Login => false,
}
}
/// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases
pub fn response(
self,
credentials: &Credentials,
challenge: Option<&str>,
) -> Result<String, Error> {
match self {
Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
)),
},
Mechanism::Login => {
let decoded_challenge =
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.authentication_identity.to_string());
}
if vec!["Password", "Password:"].contains(&decoded_challenge) {
return Ok(credentials.secret.to_string());
}
Err(Error::Client("Unrecognized challenge"))
}
Mechanism::Xoauth2 => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"user={}\x01auth=Bearer {}\x01\x01",
credentials.authentication_identity, credentials.secret
)),
},
}
}
}
#[cfg(test)]
mod test {
use super::{Credentials, Mechanism};
#[test]
fn test_plain() {
let mechanism = Mechanism::Plain;
let credentials = Credentials::new("username".to_string(), "password".to_string());
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
"\u{0}username\u{0}password"
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
#[test]
fn test_login() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
assert_eq!(
mechanism.response(&credentials, Some("Username")).unwrap(),
"alice"
);
assert_eq!(
mechanism.response(&credentials, Some("Password")).unwrap(),
"wonderland"
);
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new(
"username".to_string(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
);
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
}

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

@@ -0,0 +1,382 @@
//! SMTP commands
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", derive(serde::Serialize, serde::Deserialize))]
pub struct Ehlo {
client_id: ClientId,
}
impl Display for Ehlo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EHLO {}\r\n", self.client_id)
}
}
impl Ehlo {
/// Creates a EHLO command
pub fn new(client_id: ClientId) -> Ehlo {
Ehlo { client_id }
}
}
/// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Starttls;
impl Display for Starttls {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("STARTTLS\r\n")
}
}
/// MAIL command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Mail {
sender: Option<Address>,
parameters: Vec<MailParameter>,
}
impl Display for Mail {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"MAIL FROM:<{}>",
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str("\r\n")
}
}
impl Mail {
/// Creates a MAIL command
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
Mail { sender, parameters }
}
}
/// RCPT command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rcpt {
recipient: Address,
parameters: Vec<RcptParameter>,
}
impl Display for Rcpt {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str("\r\n")
}
}
impl Rcpt {
/// Creates an RCPT command
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
Rcpt {
recipient,
parameters,
}
}
}
/// DATA command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Data;
impl Display for Data {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("DATA\r\n")
}
}
/// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Quit;
impl Display for Quit {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("QUIT\r\n")
}
}
/// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Noop;
impl Display for Noop {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NOOP\r\n")
}
}
/// HELP command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Help {
argument: Option<String>,
}
impl Display for Help {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("HELP")?;
if self.argument.is_some() {
write!(f, " {}", self.argument.as_ref().unwrap())?;
}
f.write_str("\r\n")
}
}
impl Help {
/// Creates an HELP command
pub fn new(argument: Option<String>) -> Help {
Help { argument }
}
}
/// VRFY command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vrfy {
argument: String,
}
impl Display for Vrfy {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "VRFY {}\r\n", self.argument)
}
}
impl Vrfy {
/// Creates a VRFY command
pub fn new(argument: String) -> Vrfy {
Vrfy { argument }
}
}
/// EXPN command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Expn {
argument: String,
}
impl Display for Expn {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EXPN {}\r\n", self.argument)
}
}
impl Expn {
/// Creates an EXPN command
pub fn new(argument: String) -> Expn {
Expn { argument }
}
}
/// RSET command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rset;
impl Display for Rset {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("RSET\r\n")
}
}
/// AUTH command
#[derive(PartialEq, Clone, Debug)]
#[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 Auth {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = self
.response
.as_ref()
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
if self.mechanism.supports_initial_response() {
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
} else {
match encoded_response {
Some(response) => f.write_str(&response)?,
None => write!(f, "AUTH {}", self.mechanism)?,
}
}
f.write_str("\r\n")
}
}
impl Auth {
/// Creates an AUTH command (from a challenge if provided)
pub fn new(
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
) -> Result<Auth, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_deref())?)
} else {
None
};
Ok(Auth {
mechanism,
credentials,
challenge,
response,
})
}
/// Creates an AUTH command from a response that needs to be a
/// valid challenge (with 334 response code)
pub fn new_from_response(
mechanism: Mechanism,
credentials: Credentials,
response: &Response,
) -> Result<Auth, Error> {
if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge"));
}
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(Auth {
mechanism,
credentials,
challenge: Some(decoded_challenge),
response,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::transport::smtp::extension::MailBodyParameter;
use std::str::FromStr;
#[test]
fn test_display() {
let id = ClientId::Domain("localhost".to_string());
let email = Address::from_str("test@example.com").unwrap();
let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
};
let rcpt_parameter = RcptParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
};
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
assert_eq!(
format!("{}", Mail::new(Some(email.clone()), vec![])),
"MAIL FROM:<test@example.com>\r\n"
);
assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
assert_eq!(
format!(
"{}",
Mail::new(Some(email.clone()), vec![MailParameter::Size(42)])
),
"MAIL FROM:<test@example.com> SIZE=42\r\n"
);
assert_eq!(
format!(
"{}",
Mail::new(
Some(email.clone()),
vec![
MailParameter::Size(42),
MailParameter::Body(MailBodyParameter::EightBitMime),
mail_parameter,
],
)
),
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
);
assert_eq!(
format!("{}", Rcpt::new(email.clone(), vec![])),
"RCPT TO:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", Rcpt::new(email.clone(), vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n"
);
assert_eq!(format!("{}", Quit), "QUIT\r\n");
assert_eq!(format!("{}", Data), "DATA\r\n");
assert_eq!(format!("{}", Noop), "NOOP\r\n");
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
assert_eq!(
format!("{}", Help::new(Some("test".to_string()))),
"HELP test\r\n"
);
assert_eq!(
format!("{}", Vrfy::new("test".to_string())),
"VRFY test\r\n"
);
assert_eq!(
format!("{}", Expn::new("test".to_string())),
"EXPN test\r\n"
);
assert_eq!(format!("{}", Rset), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string());
assert_eq!(
format!(
"{}",
Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
),
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
);
assert_eq!(
format!(
"{}",
Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
),
"AUTH LOGIN\r\n"
);
}
}

154
src/transport/smtp/error.rs Normal file
View File

@@ -0,0 +1,154 @@
//! Error and result type for SMTP clients
use self::Error::*;
use crate::transport::smtp::response::{Response, Severity};
use base64::DecodeError;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
string::FromUtf8Error,
};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Response),
/// Error parsing a response
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(DecodeError),
/// Error parsing UTF8in response
Utf8Parsing(FromUtf8Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
Resolution,
/// IO error
Io(io::Error),
/// TLS error
#[cfg(feature = "native-tls")]
Tls(native_tls::Error),
/// Parsing error
Parsing(nom::error::ErrorKind),
/// Invalid hostname
#[cfg(feature = "rustls-tls")]
InvalidDNSName(webpki::InvalidDNSNameError),
#[cfg(feature = "r2d2")]
Pool(r2d2::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "transient error during SMTP transaction",
}),
Permanent(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "permanent error during SMTP transaction",
}),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Resolution => fmt.write_str("could not resolve hostname"),
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
#[cfg(feature = "native-tls")]
Tls(ref err) => err.fmt(fmt),
Parsing(ref err) => fmt.write_str(err.description()),
#[cfg(feature = "rustls-tls")]
InvalidDNSName(ref err) => err.fmt(fmt),
#[cfg(feature = "r2d2")]
Pool(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err),
#[cfg(feature = "native-tls")]
Tls(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
}
}
#[cfg(feature = "native-tls")]
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Tls(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,
})
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Error {
ChallengeParsing(err)
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
}
#[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 {
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
}
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
}

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 ehlo clinet id
pub const DEFAULT_EHLO_HOSTNAME: &str = "localhost";
/// Default client id
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,17 +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(match get_hostname() {
Some(name) => name,
None => DEFAULT_EHLO_HOSTNAME.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
///
@@ -83,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
///
@@ -126,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);
}
@@ -137,21 +144,22 @@ impl ServerInfo {
"STARTTLS" => {
features.insert(Extension::StartTls);
}
"AUTH" => for &mechanism in &split[1..] {
match mechanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
"AUTH" => {
for mechanism in split {
match mechanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
}
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
"XOAUTH2" => {
features.insert(Extension::Authentication(Mechanism::Xoauth2));
}
_ => (),
}
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
#[cfg(feature = "crammd5-auth")]
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mechanism::CramMd5));
}
_ => (),
}
},
}
_ => (),
};
}
@@ -172,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),
@@ -213,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,
@@ -232,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 {
@@ -262,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]
@@ -357,8 +377,6 @@ mod test {
assert!(server_info.supports_feature(Extension::EightBitMime));
assert!(!server_info.supports_feature(Extension::StartTls));
#[cfg(feature = "crammd5-auth")]
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
let response2 = Response::new(
Code::new(
@@ -368,7 +386,7 @@ mod test {
),
vec![
"me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
@@ -377,8 +395,7 @@ mod test {
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
#[cfg(feature = "crammd5-auth")]
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5),));
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo {
name: "me".to_string(),
@@ -389,8 +406,6 @@ mod test {
assert!(server_info2.supports_feature(Extension::EightBitMime));
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
#[cfg(feature = "crammd5-auth")]
assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
assert!(!server_info2.supports_feature(Extension::StartTls));
}
}

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,15 +1,25 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use nom::{crlf, ErrorKind as NomErrorKind, IResult as NomResult};
use nom::simple_errors::Err as NomError;
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::{FromStr, from_utf8};
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,
@@ -29,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,
@@ -53,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,
@@ -85,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,
@@ -116,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,
@@ -126,14 +136,10 @@ pub struct Response {
}
impl FromStr for Response {
type Err = NomError;
type Err = Error;
fn from_str(s: &str) -> result::Result<Response, NomError> {
match parse_response(s.as_bytes()) {
NomResult::Done(_, res) => Ok(res),
NomResult::Error(e) => Err(e),
NomResult::Incomplete(_) => Err(NomErrorKind::Complete),
}
fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
}
}
@@ -159,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())
}
@@ -171,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() {
@@ -333,20 +330,19 @@ mod test {
#[test]
fn test_response_is_positive() {
assert!(
Response::new(
Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).is_positive()
);
assert!(Response::new(
Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.is_positive());
assert!(!Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
@@ -358,25 +354,25 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).is_positive());
)
.is_positive());
}
#[test]
fn test_response_has_code() {
assert!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).has_code(451)
);
assert!(Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.has_code(451));
assert!(!Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
@@ -388,7 +384,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).has_code(251));
)
.has_code(251));
}
#[test]
@@ -405,7 +402,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).first_word(),
)
.first_word(),
Some("me")
);
assert_eq!(
@@ -420,7 +418,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).first_word(),
)
.first_word(),
Some("me")
);
assert_eq!(
@@ -431,7 +430,8 @@ mod test {
detail: Detail::One,
},
vec![],
).first_word(),
)
.first_word(),
None
);
assert_eq!(
@@ -442,7 +442,8 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
).first_word(),
)
.first_word(),
None
);
assert_eq!(
@@ -453,7 +454,8 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
).first_word(),
)
.first_word(),
None
);
assert_eq!(
@@ -464,11 +466,22 @@ mod test {
detail: Detail::One,
},
vec!["".to_string()],
).first_word(),
)
.first_word(),
None
);
}
#[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!(
@@ -483,7 +496,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).first_line(),
)
.first_line(),
Some("me")
);
assert_eq!(
@@ -498,7 +512,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).first_line(),
)
.first_line(),
Some("me mo")
);
assert_eq!(
@@ -509,7 +524,8 @@ mod test {
detail: Detail::One,
},
vec![],
).first_line(),
)
.first_line(),
None
);
assert_eq!(
@@ -520,7 +536,8 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
).first_line(),
)
.first_line(),
Some(" ")
);
assert_eq!(
@@ -531,7 +548,8 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
).first_line(),
)
.first_line(),
Some(" ")
);
assert_eq!(
@@ -542,7 +560,8 @@ mod test {
detail: Detail::One,
},
vec!["".to_string()],
).first_line(),
)
.first_line(),
Some("")
);
}

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

2
website/.gitignore vendored
View File

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

View File

@@ -1,13 +0,0 @@
all: depends _book
depends:
gitbook install
serve:
gitbook serve
_book:
gitbook build
clean:
rm -rf _book/

View File

@@ -1,10 +0,0 @@
{
"root": "./content",
"plugins": [ "-sharing", "edit-link" ],
"pluginsConfig": {
"edit-link": {
"base": "https://github.com/lettre/lettre/edit/master/website/src",
"label": "Edit"
}
}
}

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.20 or newer. Add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.8"
lettre_email = "0.8"
```

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,65 +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::EmailBuilder;
fn main() {
// Create an email
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();
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.
#### Complete example
Below is a more complete example, not using method chaining:
```rust
extern crate lettre_email;
use lettre_email::EmailBuilder;
fn main() {
let mut builder = EmailBuilder::new();
builder.add_to(("user@example.org", "Alias name"));
builder.add_cc(("user@example.net", "Alias name"));
builder.add_from("no-reply@example.com");
builder.add_from("no-reply@example.eu");
builder.set_sender("no-reply@example.com");
builder.set_subject("Hello world");
builder.set_alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.");
builder.add_reply_to("contact@example.com");
builder.add_header(("X-Custom-Header", "my header"));
let email = builder.build();
assert!(email.is_ok());
}
```
See the `EmailBuilder` documentation for a complete list of methods.

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,41 +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::FileEmailTransport;
use lettre::{SimpleSendableEmail, EmailTransport};
fn main() {
// Write to the local temp directory
let mut sender = FileEmailTransport::new(temp_dir());
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
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,23 +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::{SimpleSendableEmail, EmailTransport};
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let mut sender = SendmailTransport::new();
let result = sender.send(&email);
assert!(result.is_ok());
}
```

View File

@@ -1,116 +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::{SimpleSendableEmail, EmailTransport, SmtpTransport};
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).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);
assert!(result.is_ok());
}
```
#### Complete example
```rust,no_run
extern crate lettre;
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport};
use lettre::smtp::extension::ClientId;
use lettre::smtp::ConnectionReuseParameters;
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
// Connect to a remote server on a custom port
let mut mailer = SmtpTransport::simple_builder("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).build();
let result_1 = mailer.send(&email);
assert!(result_1.is_ok());
// The second email will use the same connection
let result_2 = mailer.send(&email);
assert!(result_2.is_ok());
// Explicitly close the SMTP transaction as we enabled connection reuse
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::Client;
use lettre::smtp::client::net::NetworkStream;
use lettre::smtp::extension::ClientId;
use lettre::smtp::commands::*;
fn main() {
let mut email_client: Client<NetworkStream> = Client::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,30 +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::StubEmailTransport;
use lettre::{SimpleSendableEmail, EmailTransport};
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let mut sender = StubEmailTransport::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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB