Compare commits

...

603 Commits

Author SHA1 Message Date
Manuel Pelloni
2ac6a72a8e chore(all): Remove rarely used top level re-exports 2020-10-21 22:08:56 +02:00
Alexis Mousset
f1e86c809d chore(all): Prepare 0.10.0-alpha.3 release 2020-10-21 22:08:56 +02:00
Manuel Pelloni
0b8d5d20ad Refactor address module and move Envelope into it (#488) 2020-10-21 17:44:51 +02:00
Paolo Barbolini
313eb74ea5 Fix MSRV in docs 2020-10-20 09:31:27 +02:00
ghizzo01
6afc078545 Avoid boxing the rustls stream (#486) 2020-10-19 21:21:22 +02:00
Paolo Barbolini
697da9f7db Tokio 0.3 support (#485)
* Tokio 0.3 support

* Tokio 0.3 TLS support

* Tokio 0.3 sendmail transport

* Tokio 0.3 file transport

* Forgotten re-exports

* Tokio 0.3 examples

* fix tokio 0.2 file-transport

* It works
2020-10-19 10:07:36 +02:00
Paolo Barbolini
1ea562b15a Stop exposing internal encoder implementation 2020-10-13 23:52:45 +02:00
Paolo Barbolini
b43c69af47 Allow changing defaults for the connection pool configuration 2020-10-13 09:33:00 +02:00
RotationMatrix
449f317246 Add doc_cfg attributes (#483)
* docs(Cargo.toml): Add `cfg(docsrs)` to `rustdoc` args

* docs(all): Enable doc_cfg crate feature

* docs(transport): Add doc_cfg to Transport traits

* docs(all): Add doc_cfg to public modules

* docs(transport-async): Add doc_cfg to Tokio02Connector

* docs(transport-smtp): Add doc_cfg to Smtp Error

* docs(transport-smtp): Add doc_cfg to TlsParameters
2020-10-13 09:18:41 +02:00
Paolo Barbolini
17d644181a Fix building with no default features 2020-10-13 09:10:46 +02:00
Paolo Barbolini
583df6af18 Fix Rust 2018 Idioms 2020-10-13 09:03:32 +02:00
Paolo Barbolini
ed9ca92de8 Bump our MSRV to 1.45.2 2020-10-13 09:03:32 +02:00
dvermd
0174a29a45 bump base64 version to 0.13.0 2020-10-06 19:23:02 +02:00
Paolo Barbolini
bfd3300df3 Improve comments in smtp selfsigned example 2020-10-04 10:17:09 +02:00
Paolo Barbolini
6526eff5b2 Allow configuring a custom root certificate in TlsParametersBuilder 2020-10-04 10:17:09 +02:00
dvermd
e00eff8b2a Implement From for Credentials 2020-10-02 08:52:56 +02:00
Alexander Jackson
5beef57c18 fix(email): Make doctest rustc-1.40 compatible 2020-09-29 19:48:10 +02:00
Alexander Jackson
b10b04ada8 docs(email): Add doctests/examples
Add doctests/examples for `Address`, `Mailbox`, `Mailboxes` and
`Envelope`.
2020-09-29 19:48:10 +02:00
Paolo Barbolini
30a8797acf Add TlsParametersBuilder with dangerous options 2020-09-23 19:18:24 +02:00
Julien Blatecky
c5fef28ac9 Add Content-ID header for attachments 2020-09-23 19:17:34 +02:00
Paolo Barbolini
bf32554e51 chore: fix unused mut warning 2020-09-09 13:12:26 +02:00
Paolo Barbolini
9983bb53c3 chore(Cargo.toml): try to tidy up the list of features and dependencies 2020-09-09 13:12:26 +02:00
Paolo Barbolini
8e49c60ff8 chore: fix benches
Makes running cargo check --all-features --all-targets work again
2020-09-09 13:12:26 +02:00
Paolo Barbolini
e156520feb clippy: fix warnings in tests
Makes running 'cargo clippy --all-features --tests' work
2020-09-09 13:12:26 +02:00
Paolo Barbolini
ec7d63c8de chore: AsRef is part of Rust's prelude 2020-09-09 13:12:26 +02:00
Paolo Barbolini
e5460c4ba1 Implement Clone for SendmailTransport 2020-09-09 13:12:26 +02:00
Paolo Barbolini
d2912a3e3f fix: Default impl for SendmailTransport 2020-09-09 13:12:26 +02:00
Alexis Mousset
9b3bd00a61 chore(all): Move from lettre.at to lettre.rs 2020-09-09 10:19:39 +02:00
Alexis Mousset
990de687aa chore(all): Prepare 0.10.0-alpha.2 release 2020-09-09 09:53:54 +02:00
Alexis Mousset
31a3be1cba run cargo fmt 2020-09-09 09:46:53 +02:00
Alexis Mousset
47dfdf7ee8 Add back removed examples 2020-09-09 09:46:53 +02:00
Manuel Pelloni
47c4077b14 Improve documentation
Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2020-09-09 09:46:53 +02:00
Manuel Pelloni
8869c7fdb4 Remove no_run from examples 2020-09-09 09:46:53 +02:00
Manuel Pelloni
3cf89935af Improve documentation 2020-09-09 09:46:53 +02:00
Manuel Pelloni
ce957ee346 Remove unused unstable feature 2020-09-09 09:46:53 +02:00
Paolo Barbolini
f87c80e05c Update rustdoc html_favicon_url to use the new lettre.rs domain 2020-09-08 22:48:37 +02:00
Paolo Barbolini
1c4a3f0fb3 chore: remove SmtpClient from the public API
We don't provide a method to construct it anyway, so it doesn't make sense
to expose it.
2020-09-08 22:48:37 +02:00
Paolo Barbolini
393d414700 Improve documentation for SmtpTransport methods 2020-09-08 22:48:37 +02:00
Paolo Barbolini
a83f927109 pool: document pool defaults 2020-09-08 21:49:09 +02:00
Paolo Barbolini
c59f67d808 pool: use better defaults
* increases the max_size to r2d2' default of 10
* decreases the min_idle number of connections to 0 (was equal to max_size before)
* decreases the idle timeout from 10 minutes to 1 minute
2020-09-08 21:49:09 +02:00
Paolo Barbolini
087e1e9c31 ci: skip async-std build on 1.40.0
async-mutex 1.3.0 broke our MSRV
2020-09-08 12:12:31 +02:00
Paolo Barbolini
69d48c4be7 Don't require Transport::{Ok, Error} to inherit specific traits 2020-09-07 22:35:46 +02:00
Paolo Barbolini
04b42879b0 refactor: optimize parts of the code
Uses the much faster `slice::is_ascii` implementation and avoids copying `v` in `utf8_b.rs`
2020-09-01 15:36:27 +02:00
Alexis Mousset
d5c1ab8dd1 chore(all): Update copyright information 2020-08-28 15:43:43 +02:00
Paolo Barbolini
42a34175ac Disable tracing attribute feature 2020-08-28 12:36:58 +02:00
Paolo Barbolini
542ea4ffd2 refactor(logging): move from log to tracing 2020-08-28 12:36:58 +02:00
Alexis Mousset
41d68616e0 Remove ClientId::new_domain 2020-08-28 11:56:22 +02:00
Alexis Mousset
36aab20086 Mark ClientId::new as deprecated 2020-08-28 11:56:22 +02:00
Alexis Mousset
98f09117f7 fix(transport-smtp): Use 127.0.0.1 literal as EHLO parameter when we have no hostname
Also fix formatting of address literals

Comes from 2275fd8d13
with a different approach for default value.
2020-08-28 11:56:22 +02:00
Paolo Barbolini
c0ef9a38a1 Implement async smtp via tokio 0.2 2020-08-22 18:44:36 +02:00
Alexis Mousset
6b6f130070 chore(all): Fix ci tests 2020-08-22 18:41:38 +02:00
Paolo Barbolini
694a6d2852 Optimize Address implementation
This reduces the mem::size_of::<Address>() from 72 to 32 and removes
two heap allocations of String when constructing a new instance of Address.
2020-08-22 15:29:11 +02:00
Manuel Pelloni
f865fc1bce Implement creating SmtpTransport using STARTTLS 2020-08-19 12:12:59 +02:00
Paolo Barbolini
60e3a0b7cb refactor: backport improvements from Tokio02 support 2020-08-13 23:37:30 +02:00
Paolo Barbolini
c8ec8984b8 refactor: move SmtpTransport and SmtpClient to it's own module 2020-08-13 23:37:30 +02:00
Paolo Barbolini
1b45c6dd58 refactor: move SmtpClient to it's own module 2020-08-13 23:37:30 +02:00
Paolo Barbolini
470b8c3ca7 docs.rs: build with all features
Now that we can, why not
2020-08-07 19:26:08 +02:00
Paolo Barbolini
c8d73dd940 refactor: stop exporting TlsParameters and Tls as top-level
Most users probably won't need it, after all we made the builder "dangerous"
2020-08-07 19:26:08 +02:00
Paolo Barbolini
bcbdbecd95 refactor: TlsParameters to not expose the inner tls library
Also made it compile with both TLS libraries enabled
2020-08-07 19:26:08 +02:00
Paolo Barbolini
d75fb5956b Merge pull request #445 from paolobarbolini/docs-010
Update docs and examples for 0.10
2020-08-04 15:27:52 +02:00
Paolo Barbolini
211aa389d7 Merge pull request #447 from paolobarbolini/improve
Simplify parts of the code
2020-08-04 12:34:09 +02:00
Paolo Barbolini
49787e0c41 chore: avoid collecting iterators when possible 2020-08-04 11:44:47 +02:00
Paolo Barbolini
3e62efb46a chore: simplify ClientId::hostname 2020-08-04 11:44:47 +02:00
Paolo Barbolini
6c440bda73 chore: minor improvements 2020-08-04 11:44:47 +02:00
Paolo Barbolini
cedfd8bfbb chore: simplify Error and Display implementations 2020-08-04 11:44:47 +02:00
Paolo Barbolini
889ef0ba6a Merge pull request #435 from paolobarbolini/bufstream
Replace unmaintained bufstream crate with std::io::BufReader
2020-08-04 10:30:50 +02:00
Paolo Barbolini
e7f07c5ce8 Merge pull request #446 from paolobarbolini/vec-write
chore: replace Vec::write_all usage with Vec::extend_from_slice
2020-08-04 10:21:39 +02:00
Paolo Barbolini
4b238829c7 chore: replace Vec::write_all usage with Vec::extend_from_slice
The underlying implementation simply calls extends_from_slice anyway,
but this removes the error that would never happen
https://doc.rust-lang.org/1.45.2/src/std/io/impls.rs.html#389-393
2020-08-04 10:09:25 +02:00
Paolo Barbolini
fbbd015109 Update docs and examples for 0.10 2020-08-02 22:39:25 +02:00
Paolo Barbolini
8fa66c1e0f clippy: fix warning from #444 2020-08-02 21:48:26 +02:00
Paolo Barbolini
72015da467 Add support for encrypted and signed multipart emails (#444)
Co-authored-by: Tim Anderson <tim@claritynetworks.com.au>
2020-08-02 21:47:37 +02:00
Paolo Barbolini
4b693e2ae3 Enable tokio02 feature on docs.rs
I forgot to do this in #440
2020-07-26 20:25:56 +02:00
Alexis Mousset
ff2baacc3d Create SECURITY.md 2020-07-26 17:41:24 +00:00
Paolo Barbolini
2173bc5f43 Add tokio ^0.2 support (#440)
* Use fs::write for writing files
* Fix running tests without tokio
2020-07-26 15:11:54 +00:00
Alexis Mousset
df6169bc98 Merge pull request #436 from paolobarbolini/line-wrap
Remove line-wrap crate and replace it with slice.chunks
2020-07-26 15:32:23 +02:00
Alexis Mousset
598abcc589 Merge pull request #439 from paolobarbolini/async-01
Refactor async-std support to prepare for more async runtimes
2020-07-26 14:44:44 +02:00
Paolo Barbolini
213fe1dc4e Update ci configuration for async feature rename 2020-07-26 14:38:29 +02:00
Paolo Barbolini
75e309731e Update async-std tests 2020-07-26 14:28:15 +02:00
Paolo Barbolini
95bc3e6745 Enable the async-std1 feature when building on docs.rs 2020-07-26 14:28:15 +02:00
Paolo Barbolini
e2b641ae89 Refactor async-std support to prepare for more async runtimes
Renames the async feature to async-std1

Moves things out of mod async since it makes things have to be written as r#async
and makes them feel less important.
2020-07-26 14:28:03 +02:00
Alexis Mousset
b3ad137691 Merge pull request #438 from paolobarbolini/dangerous-builder
Better document that SmtpTransport::builder shouldn't be used
2020-07-26 13:36:20 +02:00
Paolo Barbolini
f86c544792 Better document that SmtpTransport::builder shouldn't be used 2020-07-26 12:06:28 +02:00
Alexis Mousset
f8ea0c384d Merge pull request #437 from paolobarbolini/docs-links
Fix broken docs links
2020-07-26 11:47:05 +02:00
Paolo Barbolini
9e24786f67 Fix broken docs links 2020-07-26 11:40:22 +02:00
Alexis Mousset
8a5dc32578 fix(builder): Replace textnonce by rand in features 2020-07-22 23:49:21 +02:00
Alexis Mousset
f4a580bb90 Merge pull request #429 from dbrgn/remove-textnonce
Replace textnonce with rand (reduce dependencies)
2020-07-22 23:45:50 +02:00
Alexis Mousset
cbef830df9 Merge branch 'master' into remove-textnonce 2020-07-22 23:45:40 +02:00
Alexis Mousset
fba900daa5 Merge pull request #433 from paolobarbolini/default-tls
Use native-tls as the default tls backend
2020-07-22 23:44:53 +02:00
Alexis Mousset
f39f0d1527 Merge pull request #434 from paolobarbolini/rustls-bump
Bump rustls and webpki-roots
2020-07-22 23:44:07 +02:00
Paolo Barbolini
c41948ccd8 Remove line-wrap crate and replace it with slice.chunks 2020-07-22 18:52:51 +02:00
Paolo Barbolini
43adb0fb11 Replace unmaintained bufstream crate with std::io::BufReader 2020-07-21 19:00:59 +02:00
Paolo Barbolini
7765a97e7d Bump rustls and webpki-roots 2020-07-21 18:48:28 +02:00
Paolo Barbolini
03cbed9b05 Use native-tls as the default tls backend 2020-07-21 18:27:26 +02:00
Danilo Bargen
427fb4e35c Replace textnonce with rand
The textnonce dependency pulls in quite a few transitive dependencies.
However, we only use the dependency in a single location, to generate
MIME boundaries. For this, we can use `rand` directly (which is already
a transitive dependency anyways, since it's required by uuid).

This reduces the dependency count for a standard build from 117 to 105.
2020-07-03 16:07:02 +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
Alexis Mousset
a3d6722e7e Merge pull request #253 from Eijebong/skeptic
tests: Replace skeptic by some custom logic code and rustdoc calls
2018-04-11 22:09:52 +02:00
Bastien Orivel
fd56ec8877 test(lettre_email): Replace skeptic by some custom rustdoc invocations 2018-04-11 22:00:52 +02:00
Bastien Orivel
81bad13175 test(lettre): Replace skeptic by some custom rustdoc invocations
Unfortunately skeptic pulls all its dependencies in projects using
lettre (see https://github.com/budziq/rust-skeptic/issues/60).
2018-04-11 22:00:20 +02:00
Alexis Mousset
27c8e206cf Merge pull request #252 from amousset/add-changelog-sections
docs(all): Add changelog sections for style and docs
2018-04-08 15:14:45 +02:00
Alexis Mousset
b4d03ead8c docs(all): Add changelog sections for style and docs 2018-04-08 14:56:19 +02:00
Alexis Mousset
d692a9488f Merge pull request #251 from amousset/avoid-empty-format-strings
style(transport-smtp): Avoid useless empty format strings
2018-04-08 14:46:47 +02:00
Alexis Mousset
f3271715ec style(transport-smtp): Avoid useless empty format strings 2018-04-08 14:31:50 +02:00
Alexis Mousset
ee51cf7454 Merge pull request #250 from amousset/formal-changelog
Use clog to generate changelogs
2018-04-08 14:23:23 +02:00
Alexis Mousset
8981a7758c docs(all): Use clog to generate changelogs
Changelog will be unique, and generated from commit
messages using clog-cli.
Commit message should follow guidelines in CONTRIBUTING.md,
and GitCop has been (re)enabled.

Fixes #233.
2018-04-08 13:58:09 +02:00
Alexis Mousset
b91cb0770d Update contact email 2018-04-01 19:41:51 +02:00
Alexis Mousset
0cff889ace Merge pull request #247 from amousset/improve-readme
Add lettre.at link to README
2018-04-01 19:37:23 +02:00
Alexis Mousset
d00568cbd6 Add lettre.at link to README 2018-04-01 19:36:55 +02:00
Alexis Mousset
32cace1252 Merge pull request #246 from amousset/fix-base-url
Fix site base url
2018-04-01 19:29:32 +02:00
Alexis Mousset
a86cc3328e Fix site base url 2018-04-01 19:28:54 +02:00
Alexis Mousset
b51b2843f4 Create CNAME 2018-04-01 17:58:49 +02:00
Alexis Mousset
91a17ae281 Merge pull request #243 from amousset/prepare-0-8
Prepare 0.8 release
2018-03-31 21:11:13 +02:00
Alexis Mousset
bd752daf85 Prepare 0.8 release 2018-03-31 20:38:40 +02:00
Alexis Mousset
ed01efd890 Merge pull request #242 from amousset/more-lints
feat(all): Add more compiler lints
2018-03-31 20:00:12 +02:00
Alexis Mousset
088db45e41 feat(all): Add more compiler lints 2018-03-31 19:36:51 +02:00
Alexis Mousset
955a453df9 Merge pull request #241 from amousset/add-serde-impls
feat(transport): Add serde derive when possible
2018-03-31 17:49:14 +02:00
Alexis Mousset
71eda4b174 feat(transport): Add serde derive when possible 2018-03-31 17:30:36 +02:00
Alexis Mousset
d283254b1a Merge pull request #239 from amousset/move-envelope-lettre
feat(all): Move Envelope from lettre_email to lettre
2018-03-31 16:38:40 +02:00
Alexis Mousset
f3f963c6a5 feat(all): Move Envelope from lettre_email to lettre 2018-03-31 16:25:33 +02:00
Alexis Mousset
bef45c48f7 Merge pull request #237 from amousset/fix-style
style(all): rustfmt and clippy
2018-03-20 11:03:40 +01:00
Alexis Mousset
e024806402 style(all): rustfmt and clippy 2018-03-20 10:44:41 +01:00
Alexis Mousset
d7a8574464 Merge pull request #236 from amousset/prepare-0-8
Prepare changelog for 0.8
2018-03-11 15:39:54 +01:00
Alexis Mousset
9b22f5867e Prepare changelog for 0.8 2018-03-11 15:25:26 +01:00
Alexis Mousset
17abeb3957 Merge pull request #235 from amousset/html-utf8
fix(builder): Specify utf-8 charset for html
2018-03-11 10:49:50 +01:00
Alexis Mousset
e6a5c158da fix(builder): Specify utf-8 charset for html 2018-03-11 10:36:58 +01:00
Alexis Mousset
f4fc427a03 Merge pull request #234 from amousset/fix-attachment-multipart
fix(builder): Use parts for text and html methods to fix attachment i…
2018-03-11 01:19:24 +01:00
Alexis Mousset
662072e692 fix(builder): Use parts for text and html methods to fix attachment inclusion 2018-03-10 21:16:01 +01:00
Alexis Mousset
4f16d9ee69 Merge pull request #231 from amousset/clippy-redundant-field-name
style(all): Run stable rustfmt and remove redundant field names in st…
2018-03-03 07:52:27 +01:00
Alexis Mousset
9d68629bb6 style(all): Run stable rustfmt and remove redundant field names in structs 2018-03-03 00:28:45 +01:00
Alexis Mousset
96e4f845ec Merge pull request #230 from amousset/missing-bcc
fix(builder): Add bcc headers in builder
2018-03-01 07:38:12 +01:00
Alexis Mousset
4dc95281ad fix(builder): Add bcc headers in builder 2018-03-01 00:33:17 +01:00
Alexis Mousset
f3311456ad Merge pull request #228 from SpiderPigSpy/master
feat(email): Support binary file as attachment
2018-02-25 21:52:59 +01:00
Alex
98a250f015 feat(email): Support binary file as attachment
A pretty easy fix by using base64 encoding
worked pretty well in my project
934fb660b7/src/bot/email.rs

Closes issue
https://github.com/lettre/lettre/issues/224
2018-02-25 21:59:54 +03:00
Alexis Mousset
f2f2f98905 Merge pull request #227 from amousset/clippt-fix
feat(all): Apply clippy advice
2018-02-21 00:01:19 +01:00
Alexis Mousset
53e79d9620 feat(all): Apply clippy advice 2018-02-20 23:47:53 +01:00
Alexis Mousset
4f11ae61ef Merge pull request #225 from amousset/update-depends
feat(all): Update uuid to 0.6
2018-02-17 10:19:45 +01:00
Alexis Mousset
9344ff7e5c feat(all): Update uuid to 0.6 2018-02-17 09:53:58 +01:00
Alexis Mousset
36d20bc7b6 Merge pull request #223 from amousset/update-ci-conf
feat(all): Do not install OpenSSL for Windows tests
2018-01-27 16:20:20 +01:00
Alexis Mousset
620c3e96dc feat(all): Do not install OpenSSL for Windows tests 2018-01-27 16:08:05 +01:00
Alexis Mousset
7cab860cde Merge pull request #222 from amousset/prepare-doc-0-8
feat(all): Move doc to website and test it
2018-01-27 15:55:53 +01:00
Alexis Mousset
f10e4e81d0 feat(all): Move doc to website and test it 2018-01-27 15:44:18 +01:00
Alexis Mousset
5face8614b Merge pull request #220 from amousset/update-env-logger-0-5
feat(all): Update env_logger to 0.5
2018-01-18 09:02:20 +01:00
Alexis Mousset
480ed11785 feat(all): Update env_logger to 0.5 2018-01-18 00:14:14 +01:00
Alexis Mousset
a082da6ea4 Merge pull request #219 from amousset/fix-doc
fix(all): Fix documentation issues
2018-01-15 19:59:25 +01:00
Alexis Mousset
2a847c1b3b fix(all): Fix documentation issues 2018-01-15 19:50:47 +01:00
Alexis Mousset
f6c07f0720 Merge pull request #216 from amousset/update-log-04
feat(transport): Update log to 0.4
2017-12-28 21:01:51 +01:00
Alexis Mousset
bff687f55c feat(transport): Update log to 0.4 2017-12-28 20:17:22 +01:00
Alexis Mousset
66cd6fe3ac Merge pull request #215 from amousset/update-base64
feat(transport): Update base64 to 0.9
2017-12-23 08:24:59 +01:00
Alexis Mousset
bc4714a2c8 feat(transport): Update base64 to 0.9 2017-12-23 08:12:35 +01:00
Alexis Mousset
547be305c5 Merge pull request #214 from amousset/detail-type-enum
feat(transport-smtp): Make detail in response an enum
2017-12-10 19:12:07 +01:00
Alexis Mousset
ba719f7255 feat(transport-smtp): Make detail in response an enum 2017-12-10 18:56:35 +01:00
Alexis Mousset
4005fc88bc Merge pull request #212 from amousset/update-readme
feat(All): Update README and add CoC
2017-12-06 01:26:02 +01:00
Alexis Mousset
173f8aa2dd Merge branch 'master' into update-readme 2017-12-06 01:09:40 +01:00
Alexis Mousset
aa9e9dd96e feat(All): Update README and add CoC 2017-12-06 01:08:33 +01:00
Alexis Mousset
dd6601b9e5 Merge pull request #211 from lettre/add-code-of-conduct-1
Add a code of conduct
2017-12-06 01:03:51 +01:00
Alexis Mousset
78d8f9afb7 Add a code of conduct 2017-12-06 00:52:34 +01:00
Alexis Mousset
487bee0769 Merge pull request #210 from amousset/transport-public-methods
feat(transport-smtp): get_ehlo and reset in SmtpTransport should not …
2017-12-05 23:56:01 +01:00
Alexis Mousset
ab35bac204 feat(transport-smtp): get_ehlo and reset in SmtpTransport should not be public 2017-12-05 23:41:57 +01:00
Alexis Mousset
30ea70edab Merge pull request #209 from amousset/cleanup
Change rustfmt style
2017-11-25 12:02:09 +01:00
Alexis Mousset
eb4e7f9829 Change rustfmt style 2017-11-25 11:55:05 +01:00
Alexis Mousset
104935b443 Merge pull request #208 from amousset/use-hostname-clientid
feat(transport-smtp): Use hostname as clientid when available
2017-11-19 22:33:17 +01:00
Alexis Mousset
1936211f8e feat(transport-smtp): Use hostname as clientid when available 2017-11-19 22:21:13 +01:00
Alexis Mousset
87d0dbdf70 Merge pull request #207 from amousset/code-cleanup
feat(all): Add html_root_url
2017-11-19 14:59:38 +01:00
Alexis Mousset
7bc28caf27 feat(all): Add html_root_url 2017-11-19 14:54:04 +01:00
Alexis Mousset
7498bed378 Merge pull request #205 from amousset/update-openssl-windows-tests
fix(all): Update openssl for windows tests
2017-11-19 11:08:39 +01:00
Alexis Mousset
d2475ae1aa fix(all): Update openssl for windows tests 2017-11-19 11:00:25 +01:00
Alexis Mousset
12174676d3 Merge pull request #204 from amousset/improve-response-parsing
feat(transport-smtp): Add tests for response parsing and clean from_str
2017-11-19 01:44:26 +01:00
Alexis Mousset
1850d56ec1 feat(transport-smtp): Add tests for response parsing and clean from_str 2017-11-19 01:36:34 +01:00
Alexis Mousset
92134e22a4 Merge pull request #203 from amousset/response-parsing-with-nom
feat(transport): Use nom for parsing smtp responses
2017-11-19 01:11:12 +01:00
Alexis Mousset
01fde07a48 feat(transport): Use nom for parsing smtp responses 2017-11-19 00:42:07 +01:00
Alexis Mousset
16223ee9c3 Merge pull request #202 from amousset/update-dependencies
Update dependencies and improve style
2017-11-18 19:45:30 +01:00
Alexis Mousset
b010126c19 Update dependencies and improve style 2017-11-18 19:39:57 +01:00
Alexis Mousset
166178b011 Merge pull request #197 from jacobbudin/website-hugo-theme
fix(doc): Update Hugo theme
2017-10-16 23:02:35 +02:00
Jacob Budin
aecbce50e3 fix(doc): Update Hugo theme
Updated Hugo Learn Theme and regenerated web docs.
2017-10-15 18:17:26 -04:00
Alexis Mousset
cc324b4705 Merge pull request #196 from amousset/master
Add a version to lettre dependency in lettre_email
2017-10-08 17:59:02 +02:00
Alexis Mousset
2785f14f31 Add a version to lettre dependency in lettre_email 2017-10-08 17:41:01 +02:00
Alexis Mousset
0d3988d499 Merge pull request #195 from amousset/master
feat(all): Prepare 0.7.0 release
2017-10-08 17:28:49 +02:00
Alexis Mousset
44a1c40d41 feat(all): Prepare 0.7.0 release 2017-10-08 17:21:09 +02:00
Alexis Mousset
bef224105c Merge pull request #194 from amousset/update-base64
feat(transport): Update base64 crate to 0.7
2017-10-08 17:17:15 +02:00
Alexis Mousset
776f12c99b feat(transport): Update base64 crate to 0.7 2017-10-08 17:04:19 +02:00
Alexis Mousset
1305277ba2 Merge pull request #191 from jethrogb/transport-features
Add features for SMTP and sendmail transport
2017-09-23 18:48:29 +02:00
Alexis Mousset
85b5bbaae0 Merge pull request #192 from jethrogb/fix_ci
fix(letter): Fix beta/nightly CI
2017-09-22 12:12:44 +02:00
Jethro Beekman
8519f6881d fix(email): Don't use transport features in lettre_email 2017-09-18 22:41:08 -07:00
Jethro Beekman
60ac8ae0f3 feat(transport): Add features for SMTP and sendmail transport 2017-09-18 22:41:05 -07:00
Jethro Beekman
4e9a5575a6 fix(transport-stub): Make Stub transport independent from SMTP 2017-09-18 22:41:00 -07:00
Jethro Beekman
1f11a3ae94 fix(letter): Fix beta/nightly CI 2017-09-18 22:39:39 -07:00
Alexis Mousset
9be980ce0b Merge pull request #190 from amousset/fix-benchmark
fix(transport): Update benchmark code
2017-08-21 15:23:39 +02:00
Alexis Mousset
2838174c65 fix(transport): Update benchmark code 2017-08-21 15:00:58 +02:00
Alexis Mousset
445623db30 Merge pull request #189 from amousset/update-website
fix(doc): Update doc and specify the target version
2017-08-20 22:29:11 +02:00
Alexis Mousset
3216a3f0b9 fix(doc): Update doc and specify the target version 2017-08-20 20:28:34 +02:00
Alexis Mousset
87e25490b2 Merge pull request #188 from amousset/disable-tls-10-default
feat(transport): Disallow TLS 1.0 by default
2017-08-20 03:42:04 +02:00
Alexis Mousset
b4250036c6 feat(transport): Disallow TLS 1.0 by default 2017-08-20 03:32:24 +02:00
Alexis Mousset
4012d58dca Merge pull request #187 from amousset/fix-message-streaming
fix(transport): Fix message streaming
2017-08-20 02:40:40 +02:00
Alexis Mousset
75c184c5c3 fix(transport): Fix message streaming 2017-08-20 02:33:04 +02:00
Alexis Mousset
b087dec2d9 feat(email): Add attachments support (#186) 2017-08-20 02:31:12 +02:00
Alexis Mousset
f07fe8687d Codec from tokio smtp (#185)
feat(transport): Allow streaming emails
2017-08-20 00:42:45 +02:00
Alexis Mousset
e81351bfa8 Merge pull request #184 from amousset/missing-env-logger
fix(transport-stub): Explain that a logger is needed to get logs, and…
2017-08-06 18:09:29 +02:00
Alexis Mousset
c34f3443f5 fix(transport-stub): Explain that a logger is needed to get logs, and add env_logger to the example (fixes #181) 2017-08-06 18:02:13 +02:00
Alexis Mousset
9f4ae7b8dc Merge pull request #180 from amousset/import-client-security-tokio-smtp
feat(transport): Use structured types for transports parameters
2017-07-21 14:26:19 +02:00
Alexis Mousset
8bfee207b4 feat(transport): Use structured types for transports parameters 2017-07-21 14:19:07 +02:00
Alexis Mousset
9bf5adc052 Merge pull request #178 from amousset/mailbox-public
feat(email): Export email_format types (fixes #148)
2017-07-19 09:09:03 +02:00
Alexis Mousset
a18e219000 feat(email): Export email_format types (fixes #148) 2017-07-19 08:48:27 +02:00
Alexis Mousset
b7e4bfb375 Merge pull request #177 from amousset/fix-formatting
style(all): Move to rustfmt-nightly
2017-07-18 16:21:59 +02:00
Alexis Mousset
66836b0522 style(all): Move to rustfmt-nightly 2017-07-18 15:50:24 +02:00
Alexis Mousset
59d47dfdf5 Merge pull request #176 from amousset/method-builder
Add methods to create builder and reexport Transport types
2017-07-18 10:37:44 +02:00
Alexis Mousset
75e6c0d115 Add methods to create builder and reexport Transport types 2017-07-18 00:35:16 +02:00
Alexis Mousset
a093e38f7e Merge pull request #175 from amousset/rename-ssl-tls
style(all): Rename ssl to tls
2017-07-17 17:23:44 +02:00
Alexis Mousset
7d535f29a8 style(all): Rename ssl to tls 2017-07-17 17:13:27 +02:00
Alexis Mousset
2bfd67273b Merge pull request #174 from amousset/crammd5-option
feat(transport): Make use of hex and rust-crypto for crammd5 an option
2017-07-17 16:35:11 +02:00
Alexis Mousset
e656e9e325 feat(transport): Make use of hex and rust-crypto for crammd5 an optionnal feature 2017-07-17 16:28:43 +02:00
Alexis Mousset
e90fe50943 Merge pull request #173 from amousset/test-no-features
feat(transport): Run tests when no features are enabled
2017-07-17 15:06:40 +02:00
Alexis Mousset
2aa3cd0670 feat(transport): Run tests when no features are enabled 2017-07-17 15:00:56 +02:00
Alexis Mousset
a2143caf02 Merge pull request #172 from amousset/fix-benches
feat(transport): Fix benches
2017-07-17 14:49:11 +02:00
Alexis Mousset
04c83fc20d feat(transport): Fix benches 2017-07-17 14:30:42 +02:00
Alexis Mousset
c7c42cb207 Merge pull request #171 from amousset/serde-option
feat(transport): Make serde optionnal
2017-07-17 13:51:40 +02:00
Alexis Mousset
8a90f8f7b6 feat(transport): Make serde optionnal 2017-07-17 13:44:39 +02:00
Alexis Mousset
0e01820ea4 Merge pull request #170 from amousset/try-macro
style(all): Replace try! by ?
2017-07-17 12:24:58 +02:00
Alexis Mousset
e2d0e31453 style(all): Replace try! by ? 2017-07-17 12:19:56 +02:00
Alexis Mousset
669c558120 Merge pull request #169 from amousset/email-type
feat(transport): Use command types for mail and rcpt
2017-07-17 12:04:40 +02:00
Alexis Mousset
12794d36b3 feat(transport): Use command types for mail and rcpt 2017-07-17 11:58:58 +02:00
Alexis Mousset
0d7cba9657 Merge pull request #168 from amousset/allow-chosing-stub-response
feat(transport): Allow specifying a response for stub transport
2017-07-16 22:22:40 +02:00
Alexis Mousset
215c9d5136 feat(transport): Allow specifying a response for stub transport 2017-07-16 22:16:01 +02:00
Alexis Mousset
77210d1b5e Merge pull request #167 from amousset/use-native-tls
feat(transport): Use tls native
2017-07-16 21:45:36 +02:00
Alexis Mousset
441c4e8228 feat(transport): Use tls native 2017-07-16 21:38:18 +02:00
Alexis Mousset
858cecfdde Merge pull request #166 from amousset/run-clippy
style(all): Style improvement with clippy
2017-07-16 18:46:45 +02:00
Alexis Mousset
1047e84962 style(all): Style improvement with clippy 2017-07-16 18:41:33 +02:00
Alexis Mousset
7fc0ed5c56 Merge pull request #165 from amousset/add-smtp-command-types
feat(transport): Use types for SMTP commands
2017-07-16 18:31:11 +02:00
Alexis Mousset
6840555473 feat(transport): Use types for SMTP commands 2017-07-16 18:21:52 +02:00
Alexis Mousset
b31fd465ad Merge pull request #162 from amousset/extension-copy
feat(transport): Implement Copy for Extension
2017-06-18 00:39:13 +02:00
Alexis Mousset
b1d2a89b2e feat(transport): Implement Copy for Extension 2017-06-18 00:34:30 +02:00
Alexis Mousset
5b2a1be24c Merge pull request #161 from amousset/interface-response
feat(transport): Avoid useless methods in response and code
2017-06-18 00:19:45 +02:00
Alexis Mousset
7e69f90a6f feat(transport): Avoid useless methods in response and code 2017-06-18 00:15:06 +02:00
Alexis Mousset
f95bbff997 Merge pull request #160 from amousset/import-tokio-smtp-structs
feat(transport): Use ClientId and Detail from tokio-smtp
2017-06-17 23:22:34 +02:00
Alexis Mousset
339f65f618 feat(transport): Use ClientId and Detail from tokio-smtp 2017-06-17 23:17:19 +02:00
Alexis Mousset
1ae8c6370c Merge pull request #159 from amousset/fix-error-display-other-transports
feat(transport): Improve description of all transport error types
2017-06-17 18:21:58 +02:00
Alexis Mousset
eb3b88288a Merge branch 'master' into fix-error-display-other-transports 2017-06-17 18:10:39 +02:00
Alexis Mousset
9b8449a908 Merge pull request #158 from amousset/add-utf8-example
feat(transport): Use utf-8 chars in example email
2017-06-17 18:10:24 +02:00
Alexis Mousset
ff87e4c595 feat(transport): Improve description of all transport error types 2017-06-17 18:10:06 +02:00
Alexis Mousset
918c679d94 feat(transport): Use utf-8 chars in example email 2017-06-17 17:55:24 +02:00
Alexis Mousset
04e9a824b3 Merge pull request #156 from amousset/file-serde
feat(transport): Use serde to serialize email in the file transport
2017-06-17 17:52:24 +02:00
Alexis Mousset
63f35f78a6 feat(transport): Use serde to serialize email in the file transport 2017-06-17 17:45:49 +02:00
Alexis Mousset
1378b8959d Merge pull request #155 from amousset/improve-variables
style(all): Fix LOGIN auth detection and improve response tests
2017-06-17 16:10:46 +02:00
Alexis Mousset
c5034324d2 style(all): Fix LOGIN auth detection and improve response tests 2017-06-17 16:05:59 +02:00
Alexis Mousset
7eb0828bca Merge pull request #154 from amousset/run-clippy
style(all): Style improvement with clippy
2017-06-17 13:03:05 +02:00
Alexis Mousset
80fb92161d style(all): Style improvement with clippy 2017-06-17 12:57:54 +02:00
Alexis Mousset
fc741b7390 Merge pull request #153 from amousset/fix-error-display
feat(transport): More precise error descriptions
2017-06-17 12:38:05 +02:00
Alexis Mousset
153af016e7 feat(transport): More precise error descriptions 2017-06-17 12:33:46 +02:00
Alexis Mousset
d3a4e353b1 Merge pull request #151 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 22:28:19 +02:00
Alexis Mousset
4a49db92c1 Merge branch 'master' into master 2017-06-14 22:24:04 +02:00
Alexis Mousset
eea9354e2f feat(all): Fix AppVeyor tests 2017-06-14 22:20:00 +02:00
Alexis Mousset
e6d62a5e64 Merge pull request #150 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 22:05:27 +02:00
Alexis Mousset
782962ce5a feat(all): Fix AppVeyor tests 2017-06-14 21:50:41 +02:00
Alexis Mousset
53f1d07f00 Merge pull request #149 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 21:45:38 +02:00
Alexis Mousset
a1de7a2b24 Merge branch 'master' into master 2017-06-14 21:37:42 +02:00
Alexis Mousset
bae92bcf08 feat(all): Fix AppVeyor tests 2017-06-14 21:36:51 +02:00
Alexis Mousset
df91f38323 Merge pull request #147 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 21:08:42 +02:00
Alexis Mousset
622c4a8ff0 Merge branch 'master' into master 2017-06-14 21:01:36 +02:00
Alexis Mousset
b27013765a feat(all): Fix AppVeyor tests 2017-06-14 21:01:03 +02:00
Alexis Mousset
53072e2c89 Merge pull request #146 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 20:55:35 +02:00
Alexis Mousset
c3a2409957 feat(all): Fix AppVeyor tests 2017-06-14 20:51:01 +02:00
Alexis Mousset
093e16cad0 Merge pull request #145 from amousset/fix-appveyor
feat(all): Fix AppVeyor tests
2017-06-14 20:47:01 +02:00
Alexis Mousset
28fb7961df feat(all): Fix AppVeyor tests 2017-06-14 20:42:36 +02:00
Alexis Mousset
9c3991af6d Merge pull request #144 from amousset/upgrade-mime-crate
feat(email): Upgrade mime crate to 0.3
2017-06-14 20:35:04 +02:00
Alexis Mousset
912e0579a6 feat(email): Upgrade mime crate to 0.3 2017-06-14 20:28:05 +02:00
Alexis Mousset
0c1b440f8b Merge pull request #142 from amousset/formatting
style(all): Run last rustfmt
2017-06-14 00:53:45 +02:00
Alexis Mousset
3b46c56bbd style(all): Run last rustfmt 2017-06-14 00:48:32 +02:00
Alexis Mousset
bcf2110804 Merge pull request #141 from amousset/improve-network
feat(transport): Add a mock network stream
2017-06-14 00:32:14 +02:00
Alexis Mousset
7270d0807f feat(transport): Add a mock network stream 2017-06-14 00:12:07 +02:00
Alexis Mousset
d26a771207 Merge pull request #139 from amousset/master
Fix logos
2017-05-26 00:55:14 +02:00
Alexis Mousset
ebf75c2f00 Merge branch 'master' into master 2017-05-26 00:44:48 +02:00
Alexis Mousset
30175d941d Fix logos 2017-05-26 00:44:16 +02:00
Alexis Mousset
b4161afe14 Merge pull request #138 from amousset/master
Fix logos
2017-05-26 00:43:41 +02:00
Alexis Mousset
6f09f1c52c Merge branch 'master' into master 2017-05-26 00:36:05 +02:00
Alexis Mousset
aabc07b0a9 Fix logos 2017-05-26 00:34:40 +02:00
Alexis Mousset
dca316deae Merge pull request #137 from amousset/master
Add logo
2017-05-26 00:25:11 +02:00
Alexis Mousset
d429763b85 Merge branch 'master' into master 2017-05-26 00:16:48 +02:00
Alexis Mousset
bae531e264 Add logo 2017-05-26 00:15:56 +02:00
Alexis Mousset
c63eba4d1c Merge pull request #136 from amousset/master
Add search to website
2017-05-25 23:44:53 +02:00
Alexis Mousset
0db90c0997 Merge branch 'master' into master 2017-05-25 23:40:52 +02:00
Alexis Mousset
d0b038ac72 Add search to website 2017-05-25 23:35:32 +02:00
Alexis Mousset
691e7ff4f8 Merge pull request #135 from amousset/master
Add redirection for index page
2017-05-25 23:21:29 +02:00
Alexis Mousset
d860051d1a Merge branch 'master' into master 2017-05-25 23:12:55 +02:00
Alexis Mousset
a166b2e6a4 Add redirection for index page 2017-05-25 23:12:03 +02:00
Alexis Mousset
85a81ca383 Merge pull request #134 from amousset/master
Add redirection for index page
2017-05-25 23:08:36 +02:00
Alexis Mousset
e521f9bb0f Merge branch 'master' into master 2017-05-25 22:57:04 +02:00
Alexis Mousset
d2400de365 Add redirection for index page 2017-05-25 22:56:12 +02:00
Alexis Mousset
5a76aaf839 Merge pull request #133 from amousset/master
Add website
2017-05-25 22:41:48 +02:00
Alexis Mousset
1dddbde053 Merge branch 'master' into master 2017-05-25 22:28:03 +02:00
Alexis Mousset
3368872f9f Add website 2017-05-25 22:27:44 +02:00
Alexis Mousset
cb15e32454 Merge pull request #132 from amousset/master
Add website
2017-05-25 22:24:36 +02:00
Alexis Mousset
e85c3a4d70 add website 2017-05-25 22:18:31 +02:00
Alexis Mousset
ea99f66a5e Merge pull request #131 from callahad/error-typo
Fix typo in error string
2017-05-25 09:49:35 +02:00
Dan Callahan
6757fadee3 Fix typo in error string 2017-05-24 21:52:49 -05:00
Alexis Mousset
843f6b9a39 Merge pull request #130 from amousset/style
style(all): Run clippy without errors
2017-05-21 13:26:26 +02:00
Alexis Mousset
aa7a6dfcac Merge branch 'master' into style 2017-05-21 13:10:01 +02:00
Alexis Mousset
73c5630634 style(all): Run clippy without errors 2017-05-21 13:09:31 +02:00
Alexis Mousset
9fe4d4ad84 Merge pull request #129 from amousset/master
style(all): Split changelogs
2017-05-21 09:45:10 +02:00
Alexis Mousset
3b4434467a style(all): Split changelogs 2017-05-21 02:27:38 +02:00
Alexis Mousset
e50f287598 Merge pull request #128 from amousset/split-crates-transport
Split crates
2017-05-21 01:26:52 +02:00
Alexis Mousset
ae640da631 refactor(all): split email and transport into different crates 2017-05-21 00:59:39 +02:00
Alexis Mousset
87aa3ca701 Merge pull request #127 from mpietrzak/master
feat(all): Allow uuid 0.4 and 0.5 in dependencies.
2017-05-19 23:14:52 +02:00
Alexis Mousset
12ecad34ba Merge branch 'master' into master 2017-05-19 23:04:44 +02:00
Alexis Mousset
11a983f078 Merge pull request #125 from chills42/no-more-rustc-serialize
refactor(transport-smtp): migrate away from rustc-serialize
2017-05-19 23:03:14 +02:00
Alexis Mousset
332e05278c Merge branch 'master' into no-more-rustc-serialize 2017-05-19 22:54:09 +02:00
Alexis Mousset
9942acf8ff Merge pull request #126 from amousset/master
style(all): Fix tests on travis
2017-05-19 22:53:57 +02:00
Alexis Mousset
66c214c2b7 style(all): Fix tests on some platforms 2017-05-19 22:47:22 +02:00
Craig Hills
dfa01dbb7a remove rustc-serialize dependency 2017-05-19 11:49:32 -04:00
Maciej Pietrzak
9e2f0af9a6 feat(all): Allow uuid 0.4 and 0.5 in dependencies. 2017-05-03 12:57:18 +02:00
Craig Hills
3178db04e2 refactor(transport-smtp): migrate away from rustc-serialize
This drops the deprecated rustc-serialize dependency in favor of the
base64 and hex crates.
2017-04-26 21:43:26 -04:00
Alexis Mousset
4f41eef936 Merge pull request #123 from amousset/disallow-unencrypted-by-default
feat(transport-smtp): Disallow unencrypted connection by default
2017-03-26 22:54:29 +02:00
Alexis Mousset
8069b9e9ae feat(transport-smtp): Disallow unencrypted connection by default
By default, do not silently use unencrypted transport.
2017-03-26 22:34:49 +02:00
Alexis Mousset
4d879dabba Merge pull request #122 from amousset/add-login-auth
feat(transport-smtp): Add support for LOGIN auth mechanism
2017-03-26 22:00:42 +02:00
Alexis Mousset
20f6c5db3f feat(transport-smtp): Add support for LOGIN auth mechanism 2017-03-26 21:54:28 +02:00
Alexis Mousset
9953820174 Merge pull request #120 from amousset/changelog-0.6.2
feat(chore): Bump to v0.6.2
2017-02-18 18:53:09 +01:00
Alexis Mousset
1c8b78066f feat(chore): Bump to v0.6.2 2017-02-18 18:45:58 +01:00
Alexis Mousset
4d3e51d115 Merge pull request #118 from amousset/update-uuid
feat(all) Update uuid crate to 0.4
2017-02-18 18:07:20 +01:00
Alexis Mousset
4fc73cdde0 feat(all): Update uuid crate to 0.4 2017-02-18 17:58:40 +01:00
Alexis Mousset
caeb6b807c Merge pull request #116 from amousset/master
feat(email): Use Enveloppe directly
2017-02-18 17:54:30 +01:00
Alexis Mousset
315e248d63 feat(all): Update env_logger 2017-02-12 23:40:21 +01:00
Alexis Mousset
e068c2d41f feat(email): Use Enveloppe directly 2017-02-12 23:39:27 +01:00
Alexis Mousset
0488e3f943 Merge pull request #112 from amousset/zsck-master
feat(transport): Upgrade to OpenSSL ^0.9
2017-01-01 13:08:26 +01:00
Zack Mullaly
2ed25fdbb4 feat(transport): Upgrade to OpenSSL ^0.9 2017-01-01 13:02:10 +01:00
Alexis Mousset
90b00ae4ff Merge pull request #107 from amousset/stream-timeout
feat(transport-smtp): Add timeout suppor to SMTP transport
2016-11-06 22:20:49 +01:00
Alexis Mousset
bd8b1265c4 feat(transport-smtp): Add timeout suppor to SMTP transport
Fixes #106
2016-11-06 22:10:46 +01:00
Alexis Mousset
f8f024ae7c Merge pull request #105 from amousset/style-improvement
style(all): Improve coding style (using rust-clippy)
2016-10-23 21:14:38 +02:00
Alexis Mousset
0f71490c61 style(all): Improve coding style (using rust-clippy) 2016-10-23 21:08:59 +02:00
Alexis Mousset
6fe2ef679b Merge pull request #104 from amousset/sendmail
test(transport): add sendmail transport
2016-10-23 20:57:43 +02:00
Alexis Mousset
73e7aa3639 test(transport): add sendmail transport 2016-10-23 20:50:30 +02:00
Alexis Mousset
cc6ca7633d Merge branch 'sendmail' of https://github.com/paradoxix/lettre 2016-10-23 18:37:19 +02:00
Alexis Mousset
64d2f2e81c Merge pull request #103 from amousset/improve-api
style(all): Incompatible improvements in API
2016-10-23 18:19:11 +02:00
Alexis Mousset
e572892a48 Merge branch 'master' into improve-api 2016-10-23 17:19:20 +02:00
Alexis Mousset
3b4f4a739e style(all): Incompatible improvements in API 2016-10-23 17:16:54 +02:00
Alexis Mousset
8400e47cfc Merge pull request #101 from amousset/add-examples
docs(all): Add an example for simple builder usage and smtp transport
2016-10-23 12:58:29 +02:00
Alexis Mousset
b7cb4e88c4 Merge branch 'master' into add-examples 2016-10-23 12:41:30 +02:00
Alexis Mousset
73c957e350 docs(all): Add an example for simple builder usage and smtp transport 2016-10-23 12:37:30 +02:00
Alexis Mousset
783918a403 Merge pull request #100 from amousset/use-docsrs-links-for-doc
docs(all): Change doc links to use docs.rs
2016-10-22 23:44:40 +02:00
Alexis Mousset
d0bf2327e3 docs(all): Change doc links to use docs.rs 2016-10-22 23:36:03 +02:00
Alexis Mousset
e63730d960 Merge pull request #99 from stephank/master
Implement basic traits for SecurityLevel.
2016-10-22 19:42:44 +02:00
Stéphan Kochen
d50bb404b9 feat(transport-smtp): Add derives to SecurityLevel 2016-10-22 19:32:22 +02:00
Alexis Mousset
747d8cabc5 Merge pull request #94 from ConnyOnny/master
feat(email): support for a custom envelope
2016-10-22 19:28:43 +02:00
Constantin Berhard
b415edcfe0 refactor(email): requested changes for PR #94
renamed variables meaningfully
return more errors in email building
modified some comments

PR #94
2016-10-22 18:11:35 +02:00
Constantin
0b01211a34 Merge branch 'master' into master 2016-10-22 12:02:22 +02:00
Lars Reichardt
13ee61d5cf test(transport): add sendmail transport test 2016-10-21 15:54:53 +02:00
Lars Reichardt
a302df61d4 feat(transport): add sendmail transport 2016-10-21 14:22:47 +02:00
Alexis Mousset
5be0f86c83 Merge pull request #97 from amousset/bump-to-0.6.1
chore(all): Bump to v0.6.1
2016-10-19 23:24:58 +02:00
Constantin Berhard
7788498762 feat(email): support for a custom envelope
The EmailBuilder now has a function to add a preconfigured envelope,
overriding the auto generated one.

fixes #84
2016-10-19 13:00:41 +02:00
78 changed files with 9470 additions and 2900 deletions

View File

@@ -1,24 +0,0 @@
environment:
OPENSSL_INCLUDE_DIR: C:\OpenSSL\include
OPENSSL_LIB_DIR: C:\OpenSSL\lib
OPENSSL_LIBS: ssleay32:libeay32
matrix:
- TARGET: i686-pc-windows-gnu
BITS: 32
# - TARGET: x86_64-pc-windows-msvc
# BITS: 64
install:
- ps: Start-FileDownload "http://slproweb.com/download/Win${env:BITS}OpenSSL-1_0_2g.exe"
- Win%BITS%OpenSSL-1_0_2g.exe /SILENT /VERYSILENT /SP- /DIR="C:\OpenSSL"
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-beta-${env:TARGET}.exe"
- rust-beta-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
- SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
- SET PATH=%PATH%;C:\MinGW\bin
- rustc -V
- cargo -V
build: false
test_script:
- cargo build --verbose

7
.clog.toml Normal file
View File

@@ -0,0 +1,7 @@
[clog]
repository = "https://github.com/lettre/lettre"
changelog = "CHANGELOG.md"
[sections]
Style = ["style"]
Documentation = ["docs"]

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

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

@@ -0,0 +1,124 @@
name: Continuous integration
on: [push, pull_request]
jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
- beta
- 1.45.2
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
- run: sudo DEBIAN_FRONTEND=noninteractive apt-get update
- run: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install postfix
- run: smtp-sink 2525 1000&
- uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=native-tls,builder,r2d2,smtp-transport,file-transport,sendmail-transport
- run: rm target/debug/deps/liblettre-*
- uses: actions-rs/cargo@v1
with:
command: test
- run: rm target/debug/deps/liblettre-*
- uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=builder,smtp-transport,file-transport,sendmail-transport
- run: rm target/debug/deps/liblettre-*
- uses: actions-rs/cargo@v1
with:
command: test
args: --features=async-std1
- uses: actions-rs/cargo@v1
with:
command: test
args: --features=tokio02
- uses: actions-rs/cargo@v1
with:
command: test
args: --features=tokio03
check:
name: Check
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
- beta
- 1.45.2
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 }}

8
.gitignore vendored
View File

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

View File

@@ -1,50 +0,0 @@
language: rust
rust:
- stable
- beta
- nightly
matrix:
allow_failures:
- rust: nightly
sudo: false
cache:
apt: true
pip: true
directories:
- target/debug/deps
- target/debug/build
- target/release/deps
- target/release/build
install:
- pip install 'travis-cargo<0.2' --user
- export PATH=$HOME/.local/bin:$PATH
addons:
apt:
packages:
- postfix
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev
before_script:
- smtp-sink 2525 1000&
script:
- travis-cargo build
- travis-cargo test
- travis-cargo doc
after_success:
- ./.travis/doc.sh
- ./.travis/coverage.sh
- travis-cargo --only nightly bench
env:
global:
secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc="

View File

@@ -1,20 +0,0 @@
#!/bin/bash
set -o errexit
if [ "$TRAVIS_RUST_VERSION" != "stable" ]; then
exit 0
fi
cargo test --no-run
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz
tar xzf master.tar.gz
mkdir kcov-master/build
cd kcov-master/build
cmake ..
make
make install DESTDIR=../tmp
cd ../..
ls target/debug
./kcov-master/tmp/usr/local/bin/kcov --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov target/debug/lettre-*

View File

@@ -1,38 +0,0 @@
#!/bin/bash
set -o errexit
if [ "$TRAVIS_RUST_VERSION" != "stable" ] || [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
exit 0
fi
cargo clean
cargo doc --no-deps
git clone --branch gh-pages "https://$GH_TOKEN@github.com/${TRAVIS_REPO_SLUG}.git" deploy_docs
cd deploy_docs
git config user.email "contact@amousset.me"
git config user.name "Alexis Mousset"
if [ "$TRAVIS_BRANCH" == "master" ]; then
rm -rf master
mv ../target/doc ./master
echo "<meta http-equiv=refresh content=0;url=lettre/index.html>" > ./master/index.html
elif [ "$TRAVIS_TAG" != "" ]; then
rm -rf $TRAVIS_TAG
mv ../target/doc ./$TRAVIS_TAG
echo "<meta http-equiv=refresh content=0;url=lettre/index.html>" > ./$TRAVIS_TAG/index.html
latest=$(echo * | tr " " "\n" | sort -V -r | head -n1)
if [ "$TRAVIS_TAG" == "$latest" ]; then
echo "<meta http-equiv=refresh content=0;url=$latest/lettre/index.html>" > index.html
fi
else
exit 0
fi
git add -A .
git commit -m "Rebuild pages at ${TRAVIS_COMMIT}"
git push --quiet origin gh-pages

View File

@@ -1,30 +1,213 @@
<a name="v0.10.0"></a>
### v0.10.0 (unreleased)
#### Upgrade notes
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::builder`
and make sure to enable the `builder` feature (it's enabled by default).
* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
rename `SendableEmail` to `Email`.
* The `serde-impls` feature has been renamed to `serde`. To migrate, rename the feature.
#### Features
* Add `rustls` support ([29e4829](https://github.com/lettre/lettre/commit/29e4829), [39a0686](https://github.com/lettre/lettre/commit/39a0686))
* Allow providing a custom message id ([50d96ad](https://github.com/lettre/lettre/commit/50d96ad))
* Add `EmailAddress::is_valid` and `into_inner` ([e5a1248](https://github.com/lettre/lettre/commit/e5a1248))
* Accept `Into<SendableEmail>` ([86e5181](https://github.com/lettre/lettre/commit/86e5181))
* Allow forcing of a specific auth ([bf2adca](https://github.com/lettre/lettre/commit/bf2adca))
* Add `build_body` ([e927d0b](https://github.com/lettre/lettre/commit/e927d0b))
#### Changes
* Move CI to Github Actions ([3eef024](https://github.com/lettre/lettre/commit/3eef024))
* MSRV is now 1.36 ([d227cd4](https://github.com/lettre/lettre/commit/d227cd4))
* Merged `lettre_email` into `lettre` ([0f3f27f](https://github.com/lettre/lettre/commit/0f3f27f))
* Rename `serde-impls` feature to `serde` ([aac3e00](https://github.com/lettre/lettre/commit/aac3e00))
* Use criterion for benchmarks ([eda7fc1](https://github.com/lettre/lettre/commit/eda7fc1))
* Update to nom 5 ([5bc1cba](https://github.com/lettre/lettre/commit/5bc1cba))
* Change website url schemes to https ([6014f5c](https://github.com/lettre/lettre/commit/6014f5c))
* Use serde's `derive` feature instead of the `serde_derive` crate ([4fbe700](https://github.com/lettre/lettre/commit/4fbe700))
* Merge `Email` and `SendableEmail` into `lettre::Email` ([ce37464](https://github.com/lettre/lettre/commit/ce37464))
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as
EHLO parameter (for better RFC compliance and mail server compatibility)
* The `new` method of `ClientId` is deprecated
#### Bug Fixes
* 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)
#### Bug Fixes
* **transport:** Write timeout is not set in smtp transport ([cc3580a8](https://github.com/lettre/lettre/commit/cc3580a8942e11c2addf6677f05e16fb451c7ea0))
#### Style
* **all:** Fix typos ([360c42ff](https://github.com/lettre/lettre/commit/360c42ffb8f706222eaad14e72619df1e4857814))
#### Features
* **all:**
* Add set -xe option to build scripts ([57bbabaa](https://github.com/lettre/lettre/commit/57bbabaa6a10cc1a4de6f379e25babfee7adf6ad))
* Move post-success scripts to separate files ([3177b58c](https://github.com/lettre/lettre/commit/3177b58c6d11ffae73c958713f6f0084173924e1))
* Add website upload to travis build script ([a5294df6](https://github.com/lettre/lettre/commit/a5294df63728e14e24eeb851bb4403abd6a7bd36))
* Add codecov upload in travis ([a03bfa00](https://github.com/lettre/lettre/commit/a03bfa008537b1d86ff789d0823e89ad5d99bd79))
* Update README to put useful links at the top ([1ebbe660](https://github.com/lettre/lettre/commit/1ebbe660f5e142712f702c02d5d1e45211763b42))
* Update badges in README and Cargo.toml ([f7ee5c42](https://github.com/lettre/lettre/commit/f7ee5c427ad71e4295f2f1d8e3e9e2dd850223e8))
* Move docs from hugo to gitbook ([27935e32](https://github.com/lettre/lettre/commit/27935e32ef097db8db004569f35cad1d6cd30eca))
* **transport:** Use md-5 and hmac instead of rust-crypto ([0cf018a8](https://github.com/lettre/lettre/commit/0cf018a85e4ea1ad16c7216670da560cc915ec32))
<a name="v0.8.1"></a>
### v0.8.1 (2018-04-11)
#### Fix
* **all:**
* Replace skeptic by some custom rustdoc invocations ([81bad131](https://github.com/lettre/lettre/commit/81bad1317519d330c46ea02f2b7a266b97cc00dd))
#### Documentation
* **all:**
* Add changelog sections for style and docs ([b4d03ead](https://github.com/lettre/lettre/commit/b4d03ead8cce04e0c3d65a30e7a07acca9530f30))
* Use clog to generate changelogs ([8981a775](https://github.com/lettre/lettre/commit/8981a7758c89be69974ef204c4390744aea94e4f), closes [#233](https://github.com/lettre/lettre/issues/233))
#### Style
* **transport-smtp:** Avoid useless empty format strings ([f3271715](https://github.com/lettre/lettre/commit/f3271715ecaf2793c9064462184867e4f22b0ead))
<a name="v0.8.0"></a>
### v0.8.0 (2018-03-31)
#### Added
* Support binary files as attachment
* Move doc to a dedicated website
* Add tests for the doc using skeptic
* Added a code of conduct
* Use hostname as `ClientId` when available
#### Changed
* Detail in SMTP Response is now an enum
* Use nom for parsing smtp responses
* `Envelope` was moved from `lettre_email` to `lettre`
* `EmailAddress::new()` now returns a `Result`
* `SendableEmail` replaces `from` and `to` by `envelope` that returns an `Envelope`
* `File` transport storage format has changed
#### Fixed
* Add missing "Bcc" headers when building the email
* Specify utf-8 charset for html
* Use parts for text and html methods to work with attachments
#### Removed
* `get_ehlo` and `reset` in SmtpTransport are now private
<a name="v0.7.0"></a>
### v0.7.0 (2017-10-08)
#### Added
* Allow validating server certificate
* Initial (incomplete) attachments support
#### Changed
* Split into the *lettre* and *lettre_email* crates
* A lot of small improvements
* Use *tls-native* instead of *openssl* in smtp transport
<a name="v0.6.2"></a>
### v0.6.2 (2017-02-18)
#### Changed
* Update env-logger crate to 0.4
* Update openssl crate to 0.9
* Update uuid crate to 0.4
<a name="v0.6.1"></a>
### v0.6.1 (2016-10-19)
#### Features
#### Changes
* **documentation**
* #91: Build seperate docs for each release
* #91: Build separate docs for each release
* #96: Add complete documentation information to README
#### Bugfixes
#### Fixed
* **email**
* #85: Use address-list for "To", "From" etc.
* **tests**
* #93: Force building tests before coverage computing
<a name="v0.6.0"></a>
### v0.6.0 (2016-05-05)
#### Features
#### Changes
* **email**
* multipart support
* add non-consuming methods for Email builders
#### Beaking Change
* **email**
* `add_header` does not return the builder anymore,
for consistency with other methods. Use the `header`
method instead

46
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.rs. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
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]: https://www.contributor-covenant.org
[version]: https://www.contributor-covenant.org/version/1/4/

View File

@@ -22,10 +22,7 @@ Any line of the commit message cannot be longer 72 characters.
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
refactor: A code change that neither fixes a bug or adds a feature
perf: A code change that improves performance
test: Adding missing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
**scope** is the lettre part that is being touched. Examples:
@@ -36,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,28 +1,133 @@
[package]
name = "lettre"
version = "0.6.1"
version = "0.10.0-alpha.3" # remember to update html_root_url and README.md
description = "Email client"
readme = "README.md"
documentation = "https://lettre.github.io/lettre/"
homepage = "https://lettre.rs"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>"]
keywords = ["email", "smtp", "mailer"]
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2018"
[badges]
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
[dependencies]
bufstream = "0.1"
email = "0.0"
log = "0.3"
mime = "0.2"
openssl = "0.8"
rustc-serialize = "0.3"
rust-crypto = "0.2"
time = "0.1"
uuid = { version = "0.3", features = ["v4"] }
idna = "0.2"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder
hyperx = { version = "1", optional = true, features = ["headers"] }
mime = { version = "0.3", optional = true }
uuid = { version = "0.8", features = ["v4"] }
rand = { version = "0.7", optional = true }
quoted_printable = { version = "0.4", optional = true }
base64 = { version = "0.13", optional = true }
once_cell = "1"
regex = "1"
# file transport
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
# smtp
nom = { version = "5", optional = true }
r2d2 = { version = "0.8", optional = true } # feature
hostname = { version = "0.3", optional = true } # feature
## tls
native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.18", features = ["dangerous_configuration"], optional = true }
webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.20", optional = true }
# async
futures-io = { version = "0.3", optional = true }
futures-util = { version = "0.3", features = ["io"], optional = true }
## async-std
async-attributes = { version = "1.1", optional = true }
async-std = { version = "1.5", optional = true, features = ["unstable"] }
async-trait = { version = "0.1", optional = true }
## tokio
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["fs", "process", "tcp", "dns", "io-util"], optional = true }
tokio02_native_tls_crate = { package = "tokio-native-tls", version = "0.1", optional = true }
tokio02_rustls = { package = "tokio-rustls", version = "0.14", optional = true }
tokio03_crate = { package = "tokio", version = "0.3", features = ["fs", "process", "net", "io-util"], optional = true }
tokio03_native_tls_crate = { package = "tokio-native-tls", version = "0.2", optional = true }
tokio03_rustls = { package = "tokio-rustls", version = "0.20", optional = true }
[dev-dependencies]
env_logger = "0.3"
criterion = "0.3"
tracing-subscriber = "0.2.10"
glob = "0.3"
walkdir = "2"
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
tokio03_crate = { package = "tokio", version = "0.3", features = ["macros", "rt-multi-thread"] }
[[bench]]
harness = false
name = "transport_smtp"
[features]
unstable = []
default = ["file-transport", "smtp-transport", "native-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
# transports
file-transport = ["serde", "serde_json"]
sendmail-transport = []
smtp-transport = ["base64", "nom"]
rustls-tls = ["webpki", "webpki-roots", "rustls"]
# async
async-std1 = ["async-std", "async-trait", "async-attributes"]
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"]
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"]
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"]
tokio03 = ["tokio03_crate", "async-trait", "futures-io", "futures-util"]
tokio03-native-tls = ["tokio03", "native-tls", "tokio03_native_tls_crate"]
tokio03-rustls-tls = ["tokio03", "rustls-tls", "tokio03_rustls"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[[example]]
name = "smtp"
required-features = ["smtp-transport"]
[[example]]
name = "smtp_tls"
required-features = ["smtp-transport", "native-tls"]
[[example]]
name = "smtp_starttls"
required-features = ["smtp-transport", "native-tls"]
[[example]]
name = "smtp_selfsigned"
required-features = ["smtp-transport", "native-tls"]
[[example]]
name = "tokio02_smtp_tls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
[[example]]
name = "tokio02_smtp_starttls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"]
[[example]]
name = "tokio03_smtp_tls"
required-features = ["smtp-transport", "tokio03", "tokio03-native-tls"]
[[example]]
name = "tokio03_smtp_starttls"
required-features = ["smtp-transport", "tokio03", "tokio03-native-tls"]

View File

@@ -1,4 +1,6 @@
Copyright (c) 2014 Alexis Mousset
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me>
Copyright (c) 2019-2020 Paolo Barbolini <paolo@paolo565.org>
Copyright (c) 2018 K. <kayo@illumium.org>
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated

104
README.md
View File

@@ -1,12 +1,41 @@
# lettre
[![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)
[![Coverage Status](https://coveralls.io/repos/github/lettre/lettre/badge.svg?branch=master)](https://coveralls.io/github/lettre/lettre?branch=master)
[![Crate](https://img.shields.io/crates/v/lettre.svg)](https://crates.io/crates/lettre)
[![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)
<h1 align="center">lettre</h1>
<div align="center">
<strong>
A mailer library for Rust
</strong>
</div>
This is an email library written in Rust.
<br />
<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.rs">
<img src="https://img.shields.io/badge/visit-website-blueviolet"
alt="website" />
</a>
</div>
---
**NOTE**: this readme refers to the 0.10 version of lettre, which is
still being worked on. The master branch and the alpha releases will see
API breaking changes and some features may be missing or incomplete until
the stable 0.10.0 release is out.
Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x) branch for stable releases.
---
## Features
@@ -16,35 +45,66 @@ Lettre provides the following features:
* Unicode support (for email content and addresses)
* Secure delivery with SMTP using encryption and authentication
* Easy email builders
* Async support (incomplete)
## Documentation
Lettre does not provide (for now):
Released versions:
* Email parsing
* [latest](https://lettre.github.io/lettre/)
* [v0.6.1](https://lettre.github.io/lettre/v0.6.1/lettre/)
* [v0.6.0](https://lettre.github.io/lettre/v0.6.0/lettre/)
* [v0.5.1](https://lettre.github.io/lettre/v0.5.1/lettre/)
Development version:
* [master](https://lettre.github.io/lettre/master/lettre/)
## Install
## Example
This library requires Rust 1.45 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.6"
lettre = "0.10.0-alpha.3"
```
```rust,no_run
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
```
## Testing
The tests require an open mail server listening locally on port 25.
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
Alternatively only unit tests can be run by doing `cargo test --lib`.
## Code of conduct
Anyone who interacts with Lettre in any space, including but not limited to
this GitHub repository, must follow our [code of conduct](https://github.com/lettre/lettre/blob/master/CODE_OF_CONDUCT.md).
## License
This program is distributed under the terms of the MIT license.
The builder comes from [emailmessage-rs](https://github.com/katyo/emailmessage-rs) by
Kayo, under MIT license.
See [LICENSE](./LICENSE) for details.

9
SECURITY.md Normal file
View File

@@ -0,0 +1,9 @@
## Report a security issue
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
should not be reported via the public Github Issue tracker.
## Security advisories
Security issues will be announced via the [RustSec advisory database](https://github.com/RustSec/advisory-db).

View File

@@ -1,44 +1,44 @@
#![feature(test)]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::{Message, SmtpTransport, Transport};
extern crate lettre;
extern crate test;
use lettre::transport::smtp::SmtpTransportBuilder;
use lettre::transport::EmailTransport;
use lettre::email::EmailBuilder;
#[bench]
fn bench_simple_send(b: &mut test::Bencher) {
let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525").unwrap().build();
b.iter(|| {
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
.unwrap();
let result = sender.send(email);
assert!(result.is_ok());
});
}
#[bench]
fn bench_reuse_send(b: &mut test::Bencher) {
let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525")
.unwrap()
.connection_reuse(true)
fn bench_simple_send(c: &mut Criterion) {
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
c.bench_function("send email", move |b| {
b.iter(|| {
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
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);
let result = black_box(sender.send(&email));
assert!(result.is_ok());
})
});
sender.close()
}
fn bench_reuse_send(c: &mut Criterion) {
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
c.bench_function("send email with connection reuse", move |b| {
b.iter(|| {
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
let result = black_box(sender.send(&email));
assert!(result.is_ok());
})
});
}
criterion_group!(benches, bench_simple_send, bench_reuse_send);
criterion_main!(benches);

22
examples/smtp.rs Normal file
View File

@@ -0,0 +1,22 @@
use lettre::{Message, SmtpTransport, Transport};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
// Open a local connection on port 25
let mailer = SmtpTransport::unencrypted_localhost();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -0,0 +1,41 @@
use std::fs;
use lettre::{
transport::smtp::authentication::Credentials,
transport::smtp::client::{Certificate, Tls, TlsParameters},
Message, SmtpTransport, Transport,
};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
// Use a custom certificate stored on disk to securely verify the server's certificate
let pem_cert = fs::read("certificate.pem").unwrap();
let cert = Certificate::from_pem(&pem_cert).unwrap();
let mut tls = TlsParameters::builder("smtp.server.com".to_string());
tls.add_root_certificate(cert);
let tls = tls.build().unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to the smtp server
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
.port(465)
.tls(Tls::Wrapper(tls))
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

27
examples/smtp_starttls.rs Normal file
View File

@@ -0,0 +1,27 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

27
examples/smtp_tls.rs Normal file
View File

@@ -0,0 +1,27 @@
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -0,0 +1,36 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio02_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
Tokio02Transport,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer = AsyncSmtpTransport::<Tokio02Connector>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -0,0 +1,36 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio02_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
Tokio02Transport,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer = AsyncSmtpTransport::<Tokio02Connector>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -0,0 +1,36 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio03_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio03Connector,
Tokio03Transport,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer = AsyncSmtpTransport::<Tokio03Connector>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -0,0 +1,36 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio03_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio03Connector,
Tokio03Transport,
};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer = AsyncSmtpTransport::<Tokio03Connector>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
}

View File

@@ -1 +0,0 @@
reorder_imports = true

134
src/address/envelope.rs Normal file
View File

@@ -0,0 +1,134 @@
#[cfg(feature = "builder")]
use std::convert::TryFrom;
use super::Address;
#[cfg(feature = "builder")]
use crate::message::header::{self, Headers};
#[cfg(feature = "builder")]
use crate::message::{Mailbox, Mailboxes};
use crate::Error;
/// Simple email envelope representation
///
/// We only accept mailboxes, and do not support source routes (as per RFC).
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Envelope {
/// The envelope 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.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// let sender = Address::from_str("from@email.com").unwrap();
/// let recipients = vec![Address::from_str("to@email.com").unwrap()];
///
/// let envelope = Envelope::new(Some(sender), recipients);
/// ```
///
/// # Errors
///
/// If `to` has no elements in it.
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
if to.is_empty() {
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
reverse_path: from,
})
}
/// Gets the destination addresses of the envelope.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// let sender = Address::from_str("from@email.com").unwrap();
/// let recipients = vec![Address::from_str("to@email.com").unwrap()];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone()).unwrap();
/// assert_eq!(envelope.to(), recipients.as_slice());
/// ```
pub fn to(&self) -> &[Address] {
self.forward_path.as_slice()
}
/// Gets the sender of the envelope.
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// let sender = Address::from_str("from@email.com").unwrap();
/// let recipients = vec![Address::from_str("to@email.com").unwrap()];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone()).unwrap();
/// assert!(envelope.from().is_some());
///
/// let senderless = Envelope::new(None, recipients.clone()).unwrap();
/// assert!(senderless.from().is_none());
/// ```
pub fn from(&self) -> Option<&Address> {
self.reverse_path.as_ref()
}
}
#[cfg(feature = "builder")]
impl TryFrom<&Headers> for Envelope {
type Error = Error;
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() {
// If there is a Sender, use it
Some(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)
}
}

8
src/address/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
#[cfg(feature = "serde")]
mod serde;
mod envelope;
mod types;
pub use self::envelope::Envelope;
pub use self::types::{Address, AddressError};

112
src/address/serde.rs Normal file
View File

@@ -0,0 +1,112 @@
use std::fmt::{Formatter, Result as FmtResult};
use serde::{
de::{Deserializer, Error as DeError, MapAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use super::Address;
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
User,
Domain,
};
const FIELDS: &[&str] = &["user", "domain"];
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("'user' or 'domain'")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: DeError,
{
match value {
"user" => Ok(Field::User),
"domain" => Ok(Field::Domain),
_ => Err(DeError::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct AddressVisitor;
impl<'de> Visitor<'de> for AddressVisitor {
type Value = Address;
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("email address string or object")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut user = None;
let mut domain = None;
while let Some(key) = map.next_key()? {
match key {
Field::User => {
if user.is_some() {
return Err(DeError::duplicate_field("user"));
}
let val = map.next_value()?;
Address::check_user(val).map_err(DeError::custom)?;
user = Some(val);
}
Field::Domain => {
if domain.is_some() {
return Err(DeError::duplicate_field("domain"));
}
let val = map.next_value()?;
Address::check_domain(val).map_err(DeError::custom)?;
domain = Some(val);
}
}
}
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
Ok(Address::new(user, domain).unwrap())
}
}
deserializer.deserialize_any(AddressVisitor)
}
}

231
src/address/types.rs Normal file
View File

@@ -0,0 +1,231 @@
//! 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,
};
/// Represents an email address with a user and a domain name.
///
/// This type contains email in canonical form (_user@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
/// You can create an `Address` from a user and a domain:
///
/// ```
/// # use lettre::Address;
/// let address = Address::new("example", "email.com").unwrap();
/// ```
///
/// You can also create an `Address` from a string literal by parsing it:
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// let address = Address::from_str("example@email.com").unwrap();
/// ```
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Address {
/// Complete address
serialized: String,
/// Index into `serialized` before the '@'
at_start: usize,
}
impl<U, D> TryFrom<(U, D)> for Address
where
U: AsRef<str>,
D: AsRef<str>,
{
type Error = AddressError;
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
let user = user.as_ref();
Address::check_user(user)?;
let domain = domain.as_ref();
Address::check_domain(domain)?;
let serialized = format!("{}@{}", user, domain);
Ok(Address {
serialized,
at_start: user.len(),
})
}
}
// Regex from the specs
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
// It will mark esoteric email addresses like quoted string as invalid
static USER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
)
.unwrap()
});
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
impl Address {
/// Creates a new email address from a user and domain.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// let address = Address::new("example", "email.com").unwrap();
/// let expected: Address = "example@email.com".parse().unwrap();
/// assert_eq!(expected, address);
/// ```
pub fn new<U: AsRef<str>, D: AsRef<str>>(user: U, domain: D) -> Result<Self, AddressError> {
(user, domain).try_into()
}
/// Gets the user portion of the `Address`.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// let address = Address::new("example", "email.com").unwrap();
/// assert_eq!("example", address.user());
/// ```
pub fn user(&self) -> &str {
&self.serialized[..self.at_start]
}
/// Gets the domain portion of the `Address`.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// let address = Address::new("example", "email.com").unwrap();
/// assert_eq!("email.com", address.domain());
/// ```
pub fn domain(&self) -> &str {
&self.serialized[self.at_start + 1..]
}
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
if USER_RE.is_match(user) {
Ok(())
} else {
Err(AddressError::InvalidUser)
}
}
pub(super) fn check_domain(domain: &str) -> Result<(), AddressError> {
Address::check_domain_ascii(domain).or_else(|_| {
domain_to_ascii(domain)
.map_err(|_| AddressError::InvalidDomain)
.and_then(|domain| Address::check_domain_ascii(&domain))
})
}
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
if DOMAIN_RE.is_match(domain) {
return Ok(());
}
if let Some(caps) = LITERAL_RE.captures(domain) {
if let Some(cap) = caps.get(1) {
if cap.as_str().parse::<IpAddr>().is_ok() {
return Ok(());
}
}
}
Err(AddressError::InvalidDomain)
}
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str(&self.serialized)
}
}
impl FromStr for Address {
type Err = AddressError;
fn from_str(val: &str) -> Result<Self, AddressError> {
let mut parts = val.rsplitn(2, '@');
let domain = parts.next().ok_or(AddressError::MissingParts)?;
let user = parts.next().ok_or(AddressError::MissingParts)?;
Address::check_user(user)?;
Address::check_domain(domain)?;
Ok(Address {
serialized: val.into(),
at_start: user.len(),
})
}
}
impl AsRef<str> for Address {
fn as_ref(&self) -> &str {
&self.serialized
}
}
impl AsRef<OsStr> for Address {
fn as_ref(&self) -> &OsStr {
self.serialized.as_ref()
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum AddressError {
MissingParts,
Unbalanced,
InvalidUser,
InvalidDomain,
InvalidUtf8b,
}
impl Error for AddressError {}
impl Display for AddressError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
AddressError::MissingParts => f.write_str("Missing domain or user"),
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidUtf8b => f.write_str("Invalid UTF8b data"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_address() {
let addr_str = "something@example.com";
let addr = Address::from_str(addr_str).unwrap();
let addr2 = Address::new("something", "example.com").unwrap();
assert_eq!(addr, addr2);
assert_eq!(addr.user(), "something");
assert_eq!(addr.domain(), "example.com");
assert_eq!(addr2.user(), "something");
assert_eq!(addr2.domain(), "example.com");
}
}

View File

@@ -1,31 +0,0 @@
//! Error and result type for emails
use self::Error::*;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Missinf sender
MissingFrom,
/// Missing recipient
MissingTo,
}
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 {
MissingFrom => "the sender is missing",
MissingTo => "the recipient is missing",
}
}
}

View File

@@ -1,799 +0,0 @@
//! Simple email representation
pub mod error;
use email::error::Error;
use email_format::{Header, Mailbox, Address, MimeMessage, MimeMultipartType};
use mime::Mime;
use std::fmt;
use std::fmt::{Display, Formatter};
use time::{Tm, now};
use uuid::Uuid;
/// Converts an address or an address with an alias to a `Header`
pub trait ToHeader {
/// Converts to a `Header` struct
fn to_header(&self) -> Header;
}
impl ToHeader for Header {
fn to_header(&self) -> Header {
self.clone()
}
}
impl<'a> ToHeader for (&'a str, &'a str) {
fn to_header(&self) -> Header {
let (name, value) = *self;
Header::new(name.to_string(), value.to_string())
}
}
/// Converts an adress or an address with an alias to a `Mailbox`
pub trait ToMailbox {
/// Converts to a `Mailbox` struct
fn to_mailbox(&self) -> Mailbox;
}
impl ToMailbox for Mailbox {
fn to_mailbox(&self) -> Mailbox {
(*self).clone()
}
}
impl<'a> ToMailbox for &'a str {
fn to_mailbox(&self) -> Mailbox {
Mailbox::new(self.to_string())
}
}
impl<'a> ToMailbox for (&'a str, &'a str) {
fn to_mailbox(&self) -> Mailbox {
let (address, alias) = *self;
Mailbox::new_with_name(alias.to_string(), address.to_string())
}
}
/// 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.as_ref().unwrap().as_str().to_mailbox());
}
for to_address in self.to.as_slice() {
builder.add_to(to_address.as_str().to_mailbox());
}
for cc_address in self.cc.as_slice() {
builder.add_cc(cc_address.as_str().to_mailbox());
}
// No bcc for now
if self.reply_to.is_some() {
builder.add_reply_to(self.reply_to.as_ref().unwrap().as_str().to_mailbox());
}
if self.subject.is_some() {
builder.set_subject(self.subject.as_ref().unwrap().as_str());
}
// No date for now
match (self.text.as_ref(), self.html.as_ref()) {
(Some(text), Some(html)) => builder.set_alternative(html.as_str(), text.as_str()),
(Some(text), None) => builder.set_text(text.as_str()),
(None, Some(html)) => builder.set_html(html.as_str()),
(None, None) => (),
}
for header in self.headers.as_slice() {
builder.add_header(header.to_header());
}
Ok(builder.build().unwrap())
}
}
/// Simple representation of an email, useful for some transports
#[derive(PartialEq,Eq,Clone,Debug,Default)]
pub struct SimpleEmail {
from: Option<String>,
to: Vec<String>,
cc: Vec<String>,
// bcc: Vec<String>,
reply_to: Option<String>,
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: ToHeader>(mut self, header: A) -> SimpleEmail {
self.add_header(header);
self
}
/// Adds a generic header
pub fn add_header<A: ToHeader>(&mut self, header: A) {
self.headers.push(header.to_header());
}
/// Adds a `From` header and stores the sender address
pub fn from<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
self.add_from(address);
self
}
/// Adds a `From` header and stores the sender address
pub fn add_from<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.from = Some(mailbox.address);
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
self.add_to(address);
self
}
/// Adds a `To` header and stores the recipient address
pub fn add_to<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.to.push(mailbox.address);
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
self.add_cc(address);
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn add_cc<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.cc.push(mailbox.address);
}
/// Adds a `Reply-To` header
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
self.add_reply_to(address);
self
}
/// Adds a `Reply-To` header
pub fn add_reply_to<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.reply_to = Some(mailbox.address);
}
/// Adds a `Subject` header
pub fn subject(mut self, subject: &str) -> SimpleEmail {
self.set_subject(subject);
self
}
/// Adds a `Subject` header
pub fn set_subject(&mut self, subject: &str) {
self.subject = Some(subject.to_string());
}
/// 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.clone());
}
/// Sets the email body to plain text content
pub fn text(mut self, body: &str) -> SimpleEmail {
self.set_text(body);
self
}
/// Sets the email body to plain text content
pub fn set_text(&mut self, body: &str) {
self.text = Some(body.to_string());
}
/// Sets the email body to HTML content
pub fn html(mut self, body: &str) -> SimpleEmail {
self.set_html(body);
self
}
/// Sets the email body to HTML content
pub fn set_html(&mut self, body: &str) {
self.html = Some(body.to_string());
}
}
/// Builds a `MimeMessage` structure
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct PartBuilder {
/// Message
message: MimeMessage,
}
/// Builds an `Email` structure
#[derive(PartialEq,Eq,Clone,Debug)]
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 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 recipients' addresses
to: Vec<String>,
/// The envelope sender address
from: Option<String>,
/// Date issued
date_issued: bool,
}
/// Simple email enveloppe representation
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Envelope {
/// The envelope recipients' addresses
to: Vec<String>,
/// The envelope sender address
from: String,
}
/// Simple email representation
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Email {
/// Message
message: MimeMessage,
/// Envelope
envelope: Envelope,
/// Message-ID
message_id: Uuid,
}
impl Display for Email {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.message.as_string())
}
}
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: ToHeader>(mut self, header: A) -> PartBuilder {
self.add_header(header);
self
}
/// Adds a generic header
pub fn add_header<A: ToHeader>(&mut self, header: A) {
self.message.headers.insert(header.to_header());
}
/// Sets the body
pub fn body(mut self, body: &str) -> PartBuilder {
self.set_body(body);
self
}
/// Sets the body
pub fn set_body(&mut self, body: &str) {
self.message.body = body.to_string();
}
/// 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 builded `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![],
reply_to_header: vec![],
sender_header: None,
to: vec![],
from: None,
date_issued: false,
}
}
/// Sets the email body
pub fn body(mut self, body: &str) -> EmailBuilder {
self.message.set_body(body);
self
}
/// Sets the email body
pub fn set_body(&mut self, body: &str) {
self.message.set_body(body);
}
/// Add a generic header
pub fn header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
self.message.add_header(header);
self
}
/// Add a generic header
pub fn add_header<A: ToHeader>(&mut self, header: A) {
self.message.add_header(header);
}
/// Adds a `From` header and stores the sender address
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.add_from(address);
self
}
/// Adds a `From` header and stores the sender address
pub fn add_from<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.from = Some(mailbox.address.clone());
self.from_header.push(Address::Mailbox(mailbox));
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.add_to(address);
self
}
/// Adds a `To` header and stores the recipient address
pub fn add_to<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.to.push(mailbox.address.clone());
self.to_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.add_cc(address);
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn add_cc<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.to.push(mailbox.address.clone());
self.cc_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Reply-To` header
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.add_reply_to(address);
self
}
/// Adds a `Reply-To` header
pub fn add_reply_to<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.reply_to_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Sender` header
pub fn sender<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.set_sender(address);
self
}
/// Adds a `Sender` header
pub fn set_sender<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.from = Some(mailbox.address.clone());
self.sender_header = Some(mailbox);
}
/// Adds a `Subject` header
pub fn subject(mut self, subject: &str) -> EmailBuilder {
self.set_subject(subject);
self
}
/// Adds a `Subject` header
pub fn set_subject(&mut self, subject: &str) {
self.message.add_header(("Subject", subject));
}
/// 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().as_ref()));
self.date_issued = true;
}
/// 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(mut self, body: &str) -> EmailBuilder {
self.set_text(body);
self
}
/// Sets the email body to plain text content
pub fn set_text(&mut self, body: &str) {
self.message.set_body(body);
self.message
.add_header(("Content-Type", format!("{}", mime!(Text/Plain; Charset=Utf8)).as_ref()));
}
/// Sets the email body to HTML content
pub fn html(mut self, body: &str) -> EmailBuilder {
self.set_html(body);
self
}
/// Sets the email body to HTML content
pub fn set_html(&mut self, body: &str) {
self.message.set_body(body);
self.message
.add_header(("Content-Type", format!("{}", mime!(Text/Html; Charset=Utf8)).as_ref()));
}
/// Sets the email content
pub fn alternative(mut self, body_html: &str, body_text: &str) -> EmailBuilder {
self.set_alternative(body_html, body_text);
self
}
/// Sets the email content
pub fn set_alternative(&mut self, body_html: &str, body_text: &str) {
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; Charset=Utf8)).as_ref()))
.build();
let html = PartBuilder::new()
.body(body_html)
.header(("Content-Type", format!("{}", mime!(Text/Html; Charset=Utf8)).as_ref()))
.build();
alternate.add_child(text);
alternate.add_child(html);
self.set_message_type(MimeMultipartType::Mixed);
self.add_child(alternate.build());
}
/// Builds the Email
pub fn build(mut self) -> Result<Email, Error> {
if self.from.is_none() {
return Err(Error::MissingFrom);
}
if self.to.is_empty() {
return Err(Error::MissingTo);
}
// 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.iter() {
// 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(v) = self.sender_header {
self.message.add_header(("Sender", v.to_string().as_ref()));
}
// Add the collected addresses as mailbox-list all at once.
// The unwraps are fine because the conversions for Vec<Address> never errs.
self.message.add_header(Header::new_with_value("To".into(), self.to_header).unwrap());
self.message.add_header(Header::new_with_value("From".into(), self.from_header).unwrap());
if !self.cc_header.is_empty() {
self.message.add_header(Header::new_with_value("Cc".into(), self.cc_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(),
envelope: Envelope {
to: self.to,
from: self.from.unwrap(),
},
message_id: message_id,
})
}
}
/// Email sendable by an SMTP client
pub trait SendableEmail {
/// From address
fn from_address(&self) -> String;
/// To addresses
fn to_addresses(&self) -> Vec<String>;
/// Message content
fn message(&self) -> String;
/// Message ID
fn message_id(&self) -> String;
}
/// Minimal email structure
pub struct SimpleSendableEmail {
/// From address
from: String,
/// To addresses
to: Vec<String>,
/// Message
message: String,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(from_address: &str, to_address: Vec<String>, message: &str) -> SimpleSendableEmail {
SimpleSendableEmail {
from: from_address.to_string(),
to: to_address,
message: message.to_string(),
}
}
}
impl SendableEmail for SimpleSendableEmail {
fn from_address(&self) -> String {
self.from.clone()
}
fn to_addresses(&self) -> Vec<String> {
self.to.clone()
}
fn message(&self) -> String {
self.message.clone()
}
fn message_id(&self) -> String {
format!("{}", Uuid::new_v4())
}
}
impl SendableEmail for Email {
fn to_addresses(&self) -> Vec<String> {
self.envelope.to.clone()
}
fn from_address(&self) -> String {
self.envelope.from.clone()
}
fn message(&self) -> String {
format!("{}", self)
}
fn message_id(&self) -> String {
format!("{}", self.message_id)
}
}
#[cfg(test)]
mod test {
use email_format::{Header, MimeMessage};
use super::{Email, EmailBuilder, Envelope, SendableEmail};
use time::now;
use uuid::Uuid;
#[test]
fn test_email_display() {
let current_message = Uuid::new_v4();
let mut email = Email {
message: MimeMessage::new_blank_message(),
envelope: Envelope {
to: vec![],
from: "".to_string(),
},
message_id: current_message,
};
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>",
current_message))
.unwrap());
email.message
.headers
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
.unwrap());
email.message.body = "body".to_string();
assert_eq!(format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
current_message));
assert_eq!(current_message.to_string(), email.message_id());
}
#[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!("{}", email),
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_simple_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"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap();
assert_eq!(format!("{}", email),
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\nReply-To: \
<reply@localhost>\r\nMIME-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"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap();
assert_eq!(email.from_address(), "sender@localhost".to_string());
assert_eq!(email.to_addresses(),
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
assert_eq!(email.message(), format!("{}", email));
}
}

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

View File

@@ -1,267 +1,223 @@
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
//! Lettre is an email library that allows creating and sending messages. It provides:
//!
//! ## Overview
//! * An easy to use email builder
//! * Pluggable email transports
//! * Unicode support
//! * Secure defaults
//!
//! This mailer is divided into:
//! Lettre requires Rust 1.45 or newer.
//!
//! * An `email` part: builds the email message
//! * A `transport` part: contains the available transports for your emails. To be sendable, the
//! emails have to implement `SendableEmail`.
//! ## Optional features
//!
//! ## Creating messages
//!
//! 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
//! use lettre::email::EmailBuilder;
//!
//! // 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.
//!
//! Below is a more complete example, not using method chaining:
//!
//! ```rust
//! use lettre::email::EmailBuilder;
//!
//! 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.
//!
//! ## Sending messages
//!
//! The following sections describe the available transport methods to handle emails.
//!
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
//! the prefered way of sending emails.
//! * 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.
//!
//! ### SMTP transport
//!
//! This SMTP follows [RFC
//! 5321](https://tools.ietf.org/html/rfc5321), but is still
//! a work in progress. It 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 and
//! CRAM-MD5 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
//! #### Simple example
//!
//! This is the most basic example of usage:
//!
//! ```rust
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
//! use lettre::email::EmailBuilder;
//! use lettre::transport::EmailTransport;
//!
//! let email = EmailBuilder::new()
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .unwrap();
//!
//! // Open a local connection on port 25
//! let mut mailer =
//! SmtpTransportBuilder::localhost().unwrap().build();
//! // Send the email
//! let result = mailer.send(email);
//!
//! assert!(result.is_ok());
//! ```
//!
//! #### Complete example
//!
//! ```rust,no_run
//! use lettre::email::EmailBuilder;
//! use lettre::transport::smtp::{SecurityLevel, SmtpTransport,
//! SmtpTransportBuilder};
//! use lettre::transport::smtp::authentication::Mechanism;
//! use lettre::transport::smtp::SUBMISSION_PORT;
//! use lettre::transport::EmailTransport;
//!
//! let email = EmailBuilder::new()
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .unwrap();
//!
//! // Connect to a remote server on a custom port
//! let mut mailer = SmtpTransportBuilder::new(("server.tld",
//! SUBMISSION_PORT)).unwrap()
//! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name("my.hostname.tld")
//! // Add credentials for authentication
//! .credentials("username", "password")
//! // Specify a TLS security level. You can also specify an SslContext with
//! // .ssl_context(SslContext::Ssl23)
//! .security_level(SecurityLevel::AlwaysEncrypt)
//! // Enable SMTPUTF8 if the server supports it
//! .smtp_utf8(true)
//! // Configure expected authentication mechanism
//! .authentication_mechanism(Mechanism::CramMd5)
//! // Enable connection reuse
//! .connection_reuse(true).build();
//!
//! let result_1 = mailer.send(email.clone());
//! 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
//! use lettre::transport::smtp::SMTP_PORT;
//! use lettre::transport::smtp::client::Client;
//! use lettre::transport::smtp::client::net::NetworkStream;
//!
//! let mut email_client: Client<NetworkStream> = Client::new();
//! let _ = email_client.connect(&("localhost", SMTP_PORT), None);
//! let _ = email_client.ehlo("my_hostname");
//! let _ = email_client.mail("user@example.com", None);
//! let _ = email_client.rcpt("user@example.org");
//! let _ = email_client.data();
//! let _ = email_client.message("Test email");
//! let _ = email_client.quit();
//! ```
//!
//! ### Stub transport
//!
//! The stub transport only logs message envelope and drops the content. It can be useful for
//! testing purposes.
//!
//! ```rust
//! use lettre::transport::stub::StubEmailTransport;
//! use lettre::transport::EmailTransport;
//! use lettre::email::EmailBuilder;
//!
//! let email = EmailBuilder::new()
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .unwrap();
//!
//! let mut sender = StubEmailTransport;
//! let result = sender.send(email);
//! assert!(result.is_ok());
//! ```
//!
//! Will log the line:
//!
//! ```text
//! b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
//! ```
//!
//! ### 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
//! use std::env::temp_dir;
//!
//! use lettre::transport::file::FileEmailTransport;
//! use lettre::transport::EmailTransport;
//! use lettre::email::{EmailBuilder, SendableEmail};
//!
//! // Write to the local temp directory
//! let mut sender = FileEmailTransport::new(temp_dir());
//! let email = EmailBuilder::new()
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .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!
//! ```
//! * **builder**: Message builder
//! * **file-transport**: Transport that write messages into a file
//! * **smtp-transport**: Transport over SMTP
//! * **sendmail-transport**: Transport over SMTP
//! * **rustls-tls**: TLS support with the `rustls` crate
//! * **native-tls**: TLS support with the `native-tls` crate
//! * **tokio02**: Allow to asyncronously send emails using tokio 0.2.x
//! * **tokio02-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.2
//! * **tokio02-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.2
//! * **tokio03**: Allow to asyncronously send emails using tokio 0.3.x
//! * **tokio03-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.3
//! * **tokio03-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.3
//! * **async-std1**: Allow to asyncronously send emails using async-std 1.x (SMTP isn't supported yet)
//! * **r2d2**: Connection pool for SMTP transport
//! * **tracing**: Logging using the `tracing` crate
//! * **serde**: Serialization/Deserialization of entities
//! * **hostname**: Ability to try to use actual hostname in SMTP transaction
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-alpha.3")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
#![deny(
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
rust_2018_idioms
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs, unsafe_code, unstable_features)]
#[macro_use]
extern crate log;
#[macro_use]
extern crate mime;
extern crate rustc_serialize;
extern crate crypto;
extern crate time;
extern crate uuid;
extern crate email as email_format;
extern crate bufstream;
extern crate openssl;
pub mod address;
pub mod error;
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub mod message;
pub mod transport;
pub mod email;
#[cfg(feature = "builder")]
#[macro_use]
extern crate hyperx;
pub use crate::address::Address;
use crate::address::Envelope;
use crate::error::Error;
#[cfg(feature = "builder")]
pub use crate::message::Message;
#[cfg(feature = "file-transport")]
pub use crate::transport::file::FileTransport;
#[cfg(feature = "sendmail-transport")]
pub use crate::transport::sendmail::SendmailTransport;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio03")
))]
pub use crate::transport::smtp::AsyncSmtpTransport;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
#[cfg(all(feature = "smtp-transport", feature = "tokio02"))]
pub use crate::transport::smtp::Tokio02Connector;
#[cfg(all(feature = "smtp-transport", feature = "tokio03"))]
pub use crate::transport::smtp::Tokio03Connector;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use async_trait::async_trait;
/// Blocking Transport method for emails
pub trait Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
self.send_raw(message.envelope(), &raw)
}
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
/// async-std 1.x based Transport method for emails
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
#[async_trait]
pub trait AsyncStd1Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
/// tokio 0.2.x based Transport method for emails
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
#[async_trait]
pub trait Tokio02Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
/// tokio 0.3.x based Transport method for emails
#[cfg(feature = "tokio03")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio03")))]
#[async_trait]
pub trait Tokio03Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
#[cfg(test)]
mod test {
use super::*;
use crate::message::{header, Mailbox, Mailboxes};
use hyperx::header::Headers;
use std::convert::TryFrom;
#[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(),);
}
}

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

@@ -0,0 +1,247 @@
use crate::message::header::ContentTransferEncoding;
/// Encoder trait
pub trait EncoderCodec: Send {
/// Encode all data
fn encode(&mut self, input: &[u8]) -> Vec<u8>;
}
/// 7bit codec
///
/// WARNING: Panics when passed non-ascii chars
struct SevenBitCodec {
line_wrapper: EightBitCodec,
}
impl SevenBitCodec {
pub fn new() -> Self {
SevenBitCodec {
line_wrapper: EightBitCodec::new(),
}
}
}
impl EncoderCodec for SevenBitCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
assert!(input.is_ascii(), "input must be valid ascii");
self.line_wrapper.encode(input)
}
}
/// Quoted-Printable codec
///
struct QuotedPrintableCodec();
impl QuotedPrintableCodec {
pub fn new() -> Self {
QuotedPrintableCodec()
}
}
impl EncoderCodec for QuotedPrintableCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
quoted_printable::encode(input)
}
}
/// Base64 codec
///
struct Base64Codec {
line_wrapper: EightBitCodec,
}
impl Base64Codec {
pub fn new() -> Self {
Base64Codec {
// TODO probably 78, 76 is for qp
line_wrapper: EightBitCodec::new().with_limit(78 - 2),
}
}
}
impl EncoderCodec for Base64Codec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
self.line_wrapper.encode(base64::encode(input).as_bytes())
}
}
/// 8bit codec
///
struct EightBitCodec {
max_length: usize,
}
const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
impl EightBitCodec {
pub fn new() -> Self {
EightBitCodec {
max_length: DEFAULT_MAX_LINE_LENGTH,
}
}
pub fn with_limit(mut self, max_length: usize) -> Self {
self.max_length = max_length;
self
}
}
impl EncoderCodec for EightBitCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
let ending = b"\r\n";
let endings_len = input.len() / self.max_length * ending.len();
let mut out = Vec::with_capacity(input.len() + endings_len);
for chunk in input.chunks(self.max_length) {
// write the line ending after every chunk, except the last one
if !out.is_empty() {
out.extend_from_slice(ending);
}
out.extend_from_slice(chunk);
}
out
}
}
/// Binary codec
///
struct BinaryCodec;
impl BinaryCodec {
pub fn new() -> Self {
BinaryCodec
}
}
impl EncoderCodec for BinaryCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
input.into()
}
}
pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec> {
use self::ContentTransferEncoding::*;
match encoding {
Some(SevenBit) => Box::new(SevenBitCodec::new()),
Some(QuotedPrintable) => Box::new(QuotedPrintableCodec::new()),
Some(Base64) => Box::new(Base64Codec::new()),
Some(EightBit) => Box::new(EightBitCodec::new()),
Some(Binary) | None => Box::new(BinaryCodec::new()),
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn seven_bit_encode() {
let mut c = SevenBitCodec::new();
assert_eq!(
&String::from_utf8(c.encode(b"Hello, world!")).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(b"Chunk.")).unwrap(), "Q2h1bmsu");
}
#[test]
fn eight_bit_encode() {
let mut c = EightBitCodec::new();
assert_eq!(
&String::from_utf8(c.encode(b"Hello, world!")).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(b"Hello, world!")).unwrap(),
"Hello, world!"
);
assert_eq!(
&String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
"Hello, мир!"
);
}
}

View File

@@ -0,0 +1,123 @@
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},
};
header! { (ContentId, "Content-ID") => [String] }
#[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`][self::Mailbox] associated with sender.
```no_test
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
```
*/
(Sender, "Sender")
}
mailboxes_header! {
/**
`From` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(From, "From")
}
mailboxes_header! {
/**
`Reply-To` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(ReplyTo, "Reply-To")
}
mailboxes_header! {
/**
`To` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(To, "To")
}
mailboxes_header! {
/**
`Cc` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(Cc, "Cc")
}
mailboxes_header! {
/**
`Bcc` header
This header contains [`Mailboxes`][self::Mailboxes].
*/
(Bcc, "Bcc")
}
fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
if let Ok(src) = from_utf8(raw) {
if let Ok(mbs) = src.parse() {
return Ok(mbs);
}
}
Err(HeaderError::Header)
}
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&Mailboxes::from(
mbs.map(|mb| mb.recode_name(utf8_b::encode))
.collect::<Vec<_>>(),
))
}
#[cfg(test)]
mod test {
use super::{From, Mailbox, Mailboxes};
use hyperx::header::Headers;
#[test]
fn format_single_without_name() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n");
}
#[test]
fn format_single_with_name() {
let from = Mailboxes::new().with("K. <kayo@example.com>".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n");
}
#[test]
fn format_multi_without_name() {
let from = Mailboxes::new()
.with("kayo@example.com".parse().unwrap())
.with("pony@domain.tld".parse().unwrap());
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(
format!("{}", headers),
"From: kayo@example.com, pony@domain.tld\r\n"
);
}
#[test]
fn format_multi_with_name() {
let from = vec![
"K. <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.set(From(from.into()));
assert_eq!(
format!("{}", headers),
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
);
}
#[test]
fn format_single_with_utf8_name() {
let from = vec!["Кайо <kayo@example.com>".parse().unwrap()];
let mut headers = Headers::new();
headers.set(From(from.into()));
assert_eq!(
format!("{}", headers),
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
);
}
#[test]
fn parse_single_without_name() {
let from = vec!["kayo@example.com".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com");
assert_eq!(headers.get::<From>(), Some(&From(from)));
}
#[test]
fn parse_single_with_name() {
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>");
assert_eq!(headers.get::<From>(), Some(&From(from)));
}
#[test]
fn parse_multi_without_name() {
let from: Vec<Mailbox> = vec![
"kayo@example.com".parse().unwrap(),
"pony@domain.tld".parse().unwrap(),
];
let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com, pony@domain.tld");
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
}
#[test]
fn parse_multi_with_name() {
let from: Vec<Mailbox> = vec![
"K. <kayo@example.com>".parse().unwrap(),
"Pony P. <pony@domain.tld>".parse().unwrap(),
];
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
}
#[test]
fn parse_single_with_utf8_name() {
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()];
let mut headers = Headers::new();
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>");
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
}
}

17
src/message/header/mod.rs Normal file
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,83 @@
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{fmt::Result as FmtResult, str::from_utf8};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MimeVersion {
pub major: u8,
pub minor: u8,
}
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
impl MimeVersion {
pub fn new(major: u8, minor: u8) -> Self {
MimeVersion { major, minor }
}
}
impl Default for MimeVersion {
fn default() -> Self {
MIME_VERSION_1_0
}
}
impl Header for MimeVersion {
fn header_name() -> &'static str {
"MIME-Version"
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one().ok_or(HeaderError::Header).and_then(|r| {
let mut s = from_utf8(r).map_err(|_| HeaderError::Header)?.split('.');
let major = s.next().ok_or(HeaderError::Header)?;
let minor = s.next().ok_or(HeaderError::Header)?;
let major = major.parse().map_err(|_| HeaderError::Header)?;
let minor = minor.parse().map_err(|_| HeaderError::Header)?;
Ok(MimeVersion::new(major, minor))
})
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}.{}", self.major, self.minor))
}
}
#[cfg(test)]
mod test {
use super::{MimeVersion, MIME_VERSION_1_0};
use hyperx::header::Headers;
#[test]
fn format_mime_version() {
let mut headers = Headers::new();
headers.set(MIME_VERSION_1_0);
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
headers.set(MimeVersion::new(0, 1));
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
}
#[test]
fn parse_mime_version() {
let mut headers = Headers::new();
headers.set_raw("MIME-Version", "1.0");
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0));
headers.set_raw("MIME-Version", "0.1");
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
}
}

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,407 @@
use crate::{
address::{Address, AddressError},
message::utf8_b,
};
use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult, Write},
slice::Iter,
str::FromStr,
};
/// Represents an email address with an optional name for the sender/recipient.
///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
/// You can create a `Mailbox` from a string and an [`Address`]:
///
/// ```
/// # use lettre::{Address, message::Mailbox};
/// let address = Address::new("example", "email.com").unwrap();
/// let mailbox = Mailbox::new(None, address);
/// ```
///
/// You can also create one from a string literal:
///
/// ```
/// # use lettre::message::Mailbox;
/// let mailbox: Mailbox = "John Smith <example@email.com>".parse().unwrap();
/// ```
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailbox {
/// The name associated with the address.
pub name: Option<String>,
/// The email address itself.
pub email: Address,
}
impl Mailbox {
/// Creates a new `Mailbox` using an email address and the name of the recipient if there is one.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::Mailbox};
///
/// let address = Address::new("example", "email.com").unwrap();
/// let mailbox = Mailbox::new(None, address);
/// ```
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))
}
}
}
}
/// Represents a sequence of [`Mailbox`] instances.
///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
///
/// **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 {
/// Creates a new list of [`Mailbox`] instances.
///
/// # Examples
///
/// ```
/// use lettre::message::Mailboxes;
/// let mailboxes = Mailboxes::new();
/// ```
pub fn new() -> Self {
Mailboxes(Vec::new())
}
/// Adds a new [`Mailbox`] to the list, in a builder style pattern.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
///
/// let address = Address::new("example", "email.com").unwrap();
/// let mut mailboxes = Mailboxes::new().with(Mailbox::new(None, address));
/// ```
pub fn with(mut self, mbox: Mailbox) -> Self {
self.0.push(mbox);
self
}
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
///
/// let address = Address::new("example", "email.com").unwrap();
/// let mut mailboxes = Mailboxes::new();
/// mailboxes.push(Mailbox::new(None, address));
/// ```
pub fn push(&mut self, mbox: Mailbox) {
self.0.push(mbox);
}
/// Extracts the first [`Mailbox`] if it exists.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
///
/// let empty = Mailboxes::new();
/// assert!(empty.into_single().is_none());
///
/// let mut mailboxes = Mailboxes::new();
/// let address = Address::new("example", "email.com").unwrap();
///
/// mailboxes.push(Mailbox::new(None, address));
/// assert!(mailboxes.into_single().is_some());
/// ```
pub fn into_single(self) -> Option<Mailbox> {
self.into()
}
/// Creates an iterator over the [`Mailbox`] instances that are currently stored.
///
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
///
/// let mut mailboxes = Mailboxes::new();
///
/// let address = Address::new("example", "email.com").unwrap();
/// mailboxes.push(Mailbox::new(None, address));
///
/// let address = Address::new("example", "email.com").unwrap();
/// mailboxes.push(Mailbox::new(None, address));
///
/// let mut iter = mailboxes.iter();
///
/// assert!(iter.next().is_some());
/// assert!(iter.next().is_some());
///
/// assert!(iter.next().is_none());
/// ```
pub fn iter(&self) -> Iter<'_, Mailbox> {
self.0.iter()
}
}
impl Default for Mailboxes {
fn default() -> Self {
Self::new()
}
}
impl From<Mailbox> for Mailboxes {
fn from(single: Mailbox) -> Self {
Mailboxes(vec![single])
}
}
impl Into<Option<Mailbox>> for Mailboxes {
fn into(self) -> Option<Mailbox> {
self.into_iter().next()
}
}
impl From<Vec<Mailbox>> for Mailboxes {
fn from(list: Vec<Mailbox>) -> Self {
Mailboxes(list)
}
}
impl Into<Vec<Mailbox>> for Mailboxes {
fn into(self) -> Vec<Mailbox> {
self.0
}
}
impl IntoIterator for Mailboxes {
type Item = Mailbox;
type IntoIter = ::std::vec::IntoIter<Mailbox>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
for elem in iter {
self.0.push(elem);
}
}
}
impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut iter = self.iter();
if let Some(mbox) = iter.next() {
mbox.fmt(f)?;
for mbox in iter {
f.write_str(", ")?;
mbox.fmt(f)?;
}
}
Ok(())
}
}
impl FromStr for Mailboxes {
type Err = AddressError;
fn from_str(src: &str) -> Result<Self, Self::Err> {
src.split(',')
.map(|m| {
m.trim().parse().and_then(|Mailbox { name, email }| {
if let Some(name) = name {
if let Some(name) = utf8_b::decode(&name) {
Ok(Mailbox::new(Some(name), email))
} else {
Err(AddressError::InvalidUtf8b)
}
} else {
Ok(Mailbox::new(None, email))
}
})
})
.collect::<Result<Vec<_>, _>>()
.map(Mailboxes)
}
}
#[cfg(test)]
mod test {
use super::Mailbox;
use std::convert::TryInto;
#[test]
fn mailbox_format_address_only() {
assert_eq!(
format!(
"{}",
Mailbox::new(None, "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
}
#[test]
fn mailbox_format_address_with_name() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
),
"K. <kayo@example.com>"
);
}
#[test]
fn format_address_with_empty_name() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("".into()), "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
}
#[test]
fn format_address_with_name_trim() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
),
"K. <kayo@example.com>"
);
}
#[test]
fn parse_address_only() {
assert_eq!(
"kayo@example.com".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_name() {
assert_eq!(
"K. <kayo@example.com>".parse(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_address_with_empty_name() {
assert_eq!(
"<kayo@example.com>".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_empty_name_trim() {
assert_eq!(
" <kayo@example.com>".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_from_tuple() {
assert_eq!(
("K.".to_string(), "kayo@example.com".to_string()).try_into(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
}

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

@@ -0,0 +1,779 @@
use crate::message::{
encoder::codec,
header::{ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat,
};
use mime::Mime;
use rand::Rng;
/// MIME part variants
///
#[derive(Debug, Clone)]
pub enum Part {
/// Single part with content
///
Single(SinglePart),
/// Multiple parts of content
///
Multi(MultiPart),
}
impl EmailFormat for Part {
fn format(&self, out: &mut Vec<u8>) {
match self {
Part::Single(part) => part.format(out),
Part::Multi(part) => part.format(out),
}
}
}
impl Part {
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
/// Parts of multipart body
///
pub type Parts = Vec<Part>;
/// Creates builder for single part
///
#[derive(Debug, Clone)]
pub struct SinglePartBuilder {
headers: Headers,
}
impl SinglePartBuilder {
/// Creates a default singlepart builder
pub fn new() -> Self {
Self {
headers: Headers::new(),
}
}
/// Set the header to singlepart
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Set the Content-Type header of the singlepart
pub fn content_type(mut self, content_type: ContentType) -> Self {
self.headers.set(content_type);
self
}
/// Build singlepart using body
pub fn body<T: Into<Vec<u8>>>(self, body: T) -> SinglePart {
SinglePart {
headers: self.headers,
body: body.into(),
}
}
}
impl Default for SinglePartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Single part
///
/// # Example
///
/// ```
/// use lettre::message::{SinglePart, header};
///
/// let part = SinglePart::builder()
/// .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
/// .header(header::ContentTransferEncoding::Binary)
/// .body("Текст письма в уникоде");
/// ```
///
#[derive(Debug, Clone)]
pub struct SinglePart {
headers: Headers,
body: Vec<u8>,
}
impl SinglePart {
/// Creates a default builder for singlepart
pub fn builder() -> SinglePartBuilder {
SinglePartBuilder::new()
}
/// Creates a singlepart builder with 7bit encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::SevenBit)`.
pub fn seven_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::SevenBit)
}
/// Creates a singlepart builder with quoted-printable encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::QuotedPrintable)`.
pub fn quoted_printable() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::QuotedPrintable)
}
/// Creates a singlepart builder with base64 encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Base64)`.
pub fn base64() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::Base64)
}
/// Creates a singlepart builder with 8-bit encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::EightBit)`.
pub fn eight_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::EightBit)
}
/// Creates a singlepart builder with binary encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::Binary)`.
pub fn binary() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::Binary)
}
/// Get the headers from singlepart
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Read the body from singlepart
pub fn body_ref(&self) -> &[u8] {
&self.body
}
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
impl EmailFormat for SinglePart {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
out.extend_from_slice(b"\r\n");
let encoding = self.headers.get::<ContentTransferEncoding>();
let mut encoder = codec(encoding);
out.extend_from_slice(&encoder.encode(&self.body));
out.extend_from_slice(b"\r\n");
}
}
/// The kind of multipart
///
#[derive(Debug, Clone)]
pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts
///
/// For example this kind can be used to mix email message and attachments.
Mixed,
/// Alternative kind to join several variants of same email contents.
///
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message.
Alternative,
/// Related kind to mix content and related resources.
///
/// For example, you can include images into HTML content using that.
Related,
/// Encrypted kind for encrypted messages
Encrypted { protocol: String },
/// Signed kind for signed messages
Signed { protocol: String, micalg: String },
}
/// Create a random MIME boundary.
fn make_boundary() -> String {
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(68)
.collect()
}
impl MultiPartKind {
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
let boundary = boundary.map_or_else(make_boundary, |s| s.into());
use self::MultiPartKind::*;
format!(
"multipart/{}; boundary=\"{}\"{}",
match self {
Mixed => "mixed",
Alternative => "alternative",
Related => "related",
Encrypted { .. } => "encrypted",
Signed { .. } => "signed",
},
boundary,
match self {
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
Signed { protocol, micalg } =>
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
_ => String::new(),
}
)
.parse()
.unwrap()
}
fn from_mime(m: &Mime) -> Option<Self> {
use self::MultiPartKind::*;
match m.subtype().as_ref() {
"mixed" => Some(Mixed),
"alternative" => Some(Alternative),
"related" => Some(Related),
"signed" => m.get_param("protocol").and_then(|p| {
m.get_param("micalg").map(|micalg| Signed {
protocol: p.as_str().to_owned(),
micalg: micalg.as_str().to_owned(),
})
}),
"encrypted" => m.get_param("protocol").map(|p| Encrypted {
protocol: p.as_str().to_owned(),
}),
_ => None,
}
}
}
impl From<MultiPartKind> for Mime {
fn from(m: MultiPartKind) -> Self {
m.to_mime::<String>(None)
}
}
/// Multipart builder
///
#[derive(Debug, Clone)]
pub struct MultiPartBuilder {
headers: Headers,
}
impl MultiPartBuilder {
/// Creates default multipart builder
pub fn new() -> Self {
Self {
headers: Headers::new(),
}
}
/// Set a header
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Set `Content-Type` header using [`MultiPartKind`]
pub fn kind(self, kind: MultiPartKind) -> Self {
self.header(ContentType(kind.into()))
}
/// Set custom boundary
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
let kind = {
let mime = &self.headers.get::<ContentType>().unwrap().0;
MultiPartKind::from_mime(mime).unwrap()
};
let mime = kind.to_mime(Some(boundary.as_ref()));
self.header(ContentType(mime))
}
/// Creates multipart without parts
pub fn build(self) -> MultiPart {
MultiPart {
headers: self.headers,
parts: Vec::new(),
}
}
/// Creates multipart using part
pub fn part(self, part: Part) -> MultiPart {
self.build().part(part)
}
/// Creates multipart using singlepart
pub fn singlepart(self, part: SinglePart) -> MultiPart {
self.build().singlepart(part)
}
/// Creates multipart using multipart
pub fn multipart(self, part: MultiPart) -> MultiPart {
self.build().multipart(part)
}
}
impl Default for MultiPartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Multipart variant with parts
///
#[derive(Debug, Clone)]
pub struct MultiPart {
headers: Headers,
parts: Parts,
}
impl MultiPart {
/// Creates multipart builder
pub fn builder() -> MultiPartBuilder {
MultiPartBuilder::new()
}
/// Creates mixed multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)`
pub fn mixed() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Mixed)
}
/// Creates alternative multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)`
pub fn alternative() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Alternative)
}
/// Creates related multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)`
pub fn related() -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Related)
}
/// Creates encrypted multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Encrypted{ protocol })`
pub fn encrypted(protocol: String) -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Encrypted { protocol })
}
/// Creates signed multipart builder
///
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Signed{ protocol, micalg })`
pub fn signed(protocol: String, micalg: String) -> MultiPartBuilder {
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
}
/// Add part to multipart
pub fn part(mut self, part: Part) -> Self {
self.parts.push(part);
self
}
/// Add single part to multipart
pub fn singlepart(mut self, part: SinglePart) -> Self {
self.parts.push(Part::Single(part));
self
}
/// Add multi part to multipart
pub fn multipart(mut self, part: MultiPart) -> Self {
self.parts.push(Part::Multi(part));
self
}
/// Get the boundary of multipart contents
pub fn boundary(&self) -> String {
let content_type = &self.headers.get::<ContentType>().unwrap().0;
content_type.get_param("boundary").unwrap().as_str().into()
}
/// Get the headers from the multipart
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Get a mutable reference to the headers
pub fn headers_mut(&mut self) -> &mut Headers {
&mut self.headers
}
/// Get the parts from the multipart
pub fn parts(&self) -> &Parts {
&self.parts
}
/// Get a mutable reference to the parts
pub fn parts_mut(&mut self) -> &mut Parts {
&mut self.parts
}
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
impl EmailFormat for MultiPart {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
out.extend_from_slice(b"\r\n");
let boundary = self.boundary();
for part in &self.parts {
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"\r\n");
part.format(out);
}
out.extend_from_slice(b"--");
out.extend_from_slice(boundary.as_bytes());
out.extend_from_slice(b"--\r\n");
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::message::header;
#[test]
fn single_part_binary() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n"
)
);
}
#[test]
fn single_part_quoted_printable() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::QuotedPrintable)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
"\r\n",
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\r\n"
)
);
}
#[test]
fn single_part_base64() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Base64)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
)
);
}
#[test]
fn multi_part_mixed() {
let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
))
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"example.c".into(),
)],
})
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")),
);
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"int main() { return 0; }\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
}
#[test]
fn multi_part_encrypted() {
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"application/pgp-encrypted".parse().unwrap(),
))
.body(String::from("Version: 1")),
))
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/octet-stream; name=\"encrypted.asc\""
.parse()
.unwrap(),
))
.header(header::ContentDisposition {
disposition: header::DispositionType::Inline,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"encrypted.asc".into(),
)],
})
.body(String::from(concat!(
"-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
"...\r\n",
"-----END PGP MESSAGE-----\r\n"
))),
);
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/encrypted;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
" protocol=\"application/pgp-encrypted\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/pgp-encrypted\r\n",
"\r\n",
"Version: 1\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
"\r\n",
"-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
"...\r\n",
"-----END PGP MESSAGE-----\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
}
#[test]
fn multi_part_signed() {
let part = MultiPart::signed(
"application/pgp-signature".to_owned(),
"pgp-sha256".to_owned(),
)
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType("text/plain".parse().unwrap()))
.body(String::from("Test email for signature")),
))
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/pgp-signature; name=\"signature.asc\""
.parse()
.unwrap(),
))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"signature.asc".into(),
)],
})
.body(String::from(concat!(
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
"=3FYZ\r\n",
"-----END PGP SIGNATURE-----\r\n",
))),
);
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/signed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
" protocol=\"application/pgp-signature\";",
" micalg=\"pgp-sha256\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain\r\n",
"\r\n",
"Test email for signature\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
"\r\n",
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
"=3FYZ\r\n",
"-----END PGP SIGNATURE-----\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
}
#[test]
fn multi_part_alternative() {
let part = MultiPart::alternative()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"))))
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/alternative;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/html; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
}
#[test]
fn multi_part_mixed_related() {
let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.multipart(MultiPart::related()
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
.singlepart(SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap()))
.header(header::ContentLocation("/image.png".into()))
.header(header::ContentTransferEncoding::Base64)
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
.singlepart(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".into())]
})
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: multipart/related;",
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
"\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
"Content-Type: text/html; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
"Content-Type: image/png\r\n",
"Content-Location: /image.png\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"int main() { return 0; }\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
}
#[test]
fn test_make_boundary() {
let mut boundaries = std::collections::HashSet::with_capacity(10);
for _ in 0..1000 {
boundaries.insert(make_boundary());
}
// Ensure there are no duplicates
assert_eq!(1000, boundaries.len());
// Ensure correct length
for boundary in boundaries {
assert_eq!(68, boundary.len());
}
}
}

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

@@ -0,0 +1,547 @@
//! Provides a strongly typed way to build emails
//!
//! ### Creating messages
//!
//! This section explains how to create emails.
//!
//! ## Usage
//!
//! ### Format email messages
//!
//! #### With string body
//!
//! The easiest way how we can create email message with simple string.
//!
//! ```rust
//! use lettre::message::Message;
//!
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//! ```
//!
//! Will produce:
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//!
//! Be happy!
//! ```
//!
//! The unicode header data will be encoded using _UTF8-Base64_ encoding.
//!
//! ### With MIME body
//!
//! ##### Single part
//!
//! The more complex way is using MIME contents.
//!
//! ```rust
//! use lettre::message::{header, Message, SinglePart, Part};
//!
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/plain; charset=utf8".parse().unwrap(),
//! )).header(header::ContentTransferEncoding::QuotedPrintable)
//! .body("Привет, мир!"),
//! )
//! .unwrap();
//! ```
//!
//! The body will be encoded using selected `Content-Transfer-Encoding`.
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! MIME-Version: 1.0
//! Content-Type: text/plain; charset=utf8
//! Content-Transfer-Encoding: quoted-printable
//!
//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
//!
//! ```
//!
//! ##### Multiple parts
//!
//! And more advanced way of building message by using multipart MIME contents.
//!
//! ```rust
//! use lettre::message::{header, Message, MultiPart, SinglePart, Part};
//!
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .multipart(
//! MultiPart::mixed()
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::quoted_printable()
//! .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
//! .body("Привет, мир!")
//! )
//! .multipart(
//! MultiPart::related()
//! .singlepart(
//! SinglePart::eight_bit()
//! .header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
//! .body("<p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>")
//! )
//! .singlepart(
//! SinglePart::base64()
//! .header(header::ContentType("image/png".parse().unwrap()))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
//! })
//! .body("<smile-raw-image-data>")
//! )
//! )
//! )
//! .singlepart(
//! SinglePart::seven_bit()
//! .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Attachment,
//! parameters: vec![
//! header::DispositionParam::Filename(
//! header::Charset::Ext("utf-8".into()),
//! None, "example.c".as_bytes().into()
//! )
//! ]
//! })
//! .body("int main() { return 0; }")
//! )
//! ).unwrap();
//! ```
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! MIME-Version: 1.0
//! Content-Type: multipart/mixed; boundary="RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m"
//!
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
//! Content-Type: multipart/alternative; boundary="qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy"
//!
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
//! Content-Transfer-Encoding: quoted-printable
//! Content-Type: text/plain; charset=utf8
//!
//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
//! Content-Type: multipart/related; boundary="BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8"
//!
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
//! Content-Transfer-Encoding: 8bit
//! Content-Type: text/html; charset=utf8
//!
//! <p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
//! Content-Transfer-Encoding: base64
//! Content-Type: image/png
//! Content-Disposition: inline
//!
//! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg==
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8--
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy--
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
//! Content-Transfer-Encoding: 7bit
//! Content-Type: text/plain; charset=utf8
//! Content-Disposition: attachment; filename="example.c"
//!
//! int main() { return 0; }
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m--
//!
//! ```
pub use mailbox::*;
pub use mimebody::*;
pub use mime;
mod encoder;
pub mod header;
mod mailbox;
mod mimebody;
mod utf8_b;
use crate::{
address::Envelope,
message::header::{EmailDate, Header, Headers, MailboxesHeader},
Error as EmailError,
};
use std::{convert::TryFrom, time::SystemTime};
use uuid::Uuid;
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
pub trait EmailFormat {
// Use a writer?
fn format(&self, out: &mut Vec<u8>);
}
/// A builder for messages
#[derive(Debug, Clone)]
pub struct MessageBuilder {
headers: Headers,
envelope: Option<Envelope>,
}
impl MessageBuilder {
/// Creates a new default message builder
pub fn new() -> Self {
Self {
headers: Headers::new(),
envelope: None,
}
}
/// Set custom header to message
pub fn header<H: Header>(mut self, header: H) -> Self {
self.headers.set(header);
self
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
if self.headers.has::<H>() {
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
self
} else {
self.header(header)
}
}
/// Add `Date` header to message
///
/// Shortcut for `self.header(header::Date(date))`.
pub fn date(self, date: EmailDate) -> Self {
self.header(header::Date(date))
}
/// Set `Date` header using current date/time
///
/// Shortcut for `self.date(SystemTime::now())`.
pub fn date_now(self) -> Self {
self.date(SystemTime::now().into())
}
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
self.header(header::Subject(subject.into()))
}
/// Set `Mime-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
/// Not exposed as it is set by body methods
fn mime_1_0(self) -> Self {
self.header(header::MIME_VERSION_1_0)
}
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.header(header::Sender(mbox))`.
pub fn sender(self, mbox: Mailbox) -> Self {
self.header(header::Sender(mbox))
}
/// Set or add mailbox to `From` header
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self {
self.mailbox(header::From(mbox.into()))
}
/// Set or add mailbox to `ReplyTo` header
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
///
/// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
pub fn reply_to(self, mbox: Mailbox) -> Self {
self.mailbox(header::ReplyTo(mbox.into()))
}
/// Set or add mailbox to `To` header
///
/// Shortcut for `self.mailbox(header::To(mbox))`.
pub fn to(self, mbox: Mailbox) -> Self {
self.mailbox(header::To(mbox.into()))
}
/// Set or add mailbox to `Cc` header
///
/// Shortcut for `self.mailbox(header::Cc(mbox))`.
pub fn cc(self, mbox: Mailbox) -> Self {
self.mailbox(header::Cc(mbox.into()))
}
/// Set or add mailbox to `Bcc` header
///
/// Shortcut for `self.mailbox(header::Bcc(mbox))`.
pub fn bcc(self, mbox: Mailbox) -> Self {
self.mailbox(header::Bcc(mbox.into()))
}
/// Set or add message id to [`In-Reply-To`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn in_reply_to(self, id: String) -> Self {
self.header(header::InReplyTo(id))
}
/// Set or add message id to [`References`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn references(self, id: String) -> Self {
self.header(header::References(id))
}
/// Set [Message-Id
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
///
/// Should generally be inserted by the mail relay.
///
/// If `None` is provided, an id will be generated in the
/// `<UUID@HOSTNAME>`.
pub fn message_id(self, id: Option<String>) -> Self {
match id {
Some(i) => self.header(header::MessageId(i)),
None => {
#[cfg(feature = "hostname")]
let hostname = hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_MESSAGE_ID_DOMAIN.to_string());
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
self.header(header::MessageId(
// https://tools.ietf.org/html/rfc5322#section-3.6.4
format!("<{}@{}>", Uuid::new_v4(), hostname),
))
}
}
}
/// Set [User-Agent
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
pub fn user_agent(self, id: String) -> Self {
self.header(header::UserAgent(id))
}
/// Force specific envelope (by default it is derived from headers)
pub fn envelope(mut self, envelope: Envelope) -> Self {
self.envelope = Some(envelope);
self
}
// TODO: High-level methods for attachments and embedded files
/// Create message from body
fn build(self, body: Body) -> Result<Message, EmailError> {
// Check for missing required headers
// https://tools.ietf.org/html/rfc5322#section-3.6
// Insert Date if missing
let res = if self.headers.get::<header::Date>().is_none() {
self.date_now()
} else {
self
};
// Fail is missing correct originator (Sender or From)
match res.headers.get::<header::From>() {
Some(header::From(f)) => {
let from: Vec<Mailbox> = f.clone().into();
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
return Err(EmailError::TooManyFrom);
}
}
None => {
return Err(EmailError::MissingFrom);
}
}
let envelope = match res.envelope {
Some(e) => e,
None => Envelope::try_from(&res.headers)?,
};
Ok(Message {
headers: res.headers,
body,
envelope,
})
}
// In theory having a body is optional
/// Plain ASCII body
///
/// *WARNING*: Generally not what you want
pub fn body<T: Into<String>>(self, body: T) -> Result<Message, EmailError> {
// 998 chars by line
// CR and LF MUST only occur together as CRLF; they MUST NOT appear
// independently in the body.
let body = body.into();
if !&body.is_ascii() {
return Err(EmailError::NonAsciiChars);
}
self.build(Body::Raw(body))
}
/// Create message using mime body ([`MultiPart`][self::MultiPart])
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Multi(part)))
}
/// Create message using mime body ([`SinglePart`][self::SinglePart])
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Single(part)))
}
}
/// Email message which can be formatted
#[derive(Clone, Debug)]
pub struct Message {
headers: Headers,
body: Body,
envelope: Envelope,
}
#[derive(Clone, Debug)]
enum Body {
Mime(Part),
Raw(String),
}
impl Message {
/// Create a new message builder without headers
pub fn builder() -> MessageBuilder {
MessageBuilder::new()
}
/// Get the headers from the Message
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Get `Message` envelope
pub fn envelope(&self) -> &Envelope {
&self.envelope
}
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
impl EmailFormat for Message {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
match &self.body {
Body::Mime(p) => p.format(out),
Body::Raw(r) => {
out.extend_from_slice(b"\r\n");
out.extend(r.as_bytes())
}
}
}
}
impl Default for MessageBuilder {
fn default() -> Self {
MessageBuilder::new()
}
}
#[cfg(test)]
mod test {
use crate::message::{header, mailbox::Mailbox, Message};
#[test]
fn email_missing_originator() {
assert!(Message::builder().body("Happy new year!").is_err());
}
#[test]
fn email_miminal_message() {
assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("NoBody <nobody@domain.tld>".parse().unwrap())
.body("Happy new year!")
.is_ok());
}
#[test]
fn email_missing_sender() {
assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.from("AnyBody <anybody@domain.tld>".parse().unwrap())
.body("Happy new year!")
.is_err());
}
#[test]
fn email_message() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
let email = Message::builder()
.date(date)
.header(header::From(
vec![Mailbox::new(
Some("Каи".into()),
"kayo@example.com".parse().unwrap(),
)]
.into(),
))
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject("яңа ел белән!".into()))
.body("Happy new year!")
.unwrap();
assert_eq!(
String::from_utf8(email.formatted()).unwrap(),
concat!(
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: Pony O.P. <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"\r\n",
"Happy new year!"
)
);
}
}

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

@@ -0,0 +1,60 @@
// 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> {
s.strip_prefix("=?utf-8?b?")
.and_then(|stripped| stripped.strip_suffix("?="))
.map_or_else(
|| Some(s.into()),
|stripped| {
let decoded = base64::decode(stripped).ok()?;
let decoded = String::from_utf8(decoded).ok()?;
Some(decoded)
},
)
}
#[cfg(test)]
mod test {
use super::{decode, encode};
#[test]
fn encode_ascii() {
assert_eq!(&encode("Kayo. ?"), "Kayo. ?");
}
#[test]
fn decode_ascii() {
assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into()));
}
#[test]
fn encode_utf8() {
assert_eq!(
&encode("Привет, мир!"),
"=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="
);
}
#[test]
fn decode_utf8() {
assert_eq!(
decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="),
Some("Привет, мир!".into())
);
}
}

View File

@@ -1,11 +1,11 @@
//! Error and result type for file transport
use self::Error::*;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
};
/// An enum of all error kinds.
#[derive(Debug)]
@@ -14,25 +14,25 @@ pub enum Error {
Client(&'static str),
/// IO error
Io(io::Error),
/// JSON serialization error
JsonSerialization(serde_json::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
JsonSerialization(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn description(&self) -> &str {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Client(_) => "an unknown error occured",
Io(_) => "an I/O error occured",
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
Io(ref err) => Some(&*err as &StdError),
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
_ => None,
}
}
@@ -40,15 +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 {
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>;

View File

@@ -1,51 +1,220 @@
//! This transport creates a file for each email, containing the envelope information and the email
//! itself.
//! The file transport writes the emails to the given directory. The name of the file will be
//! `message_id.json`.
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//!
//! ## Sync example
//!
//! ```rust
//! use std::env::temp_dir;
//! use lettre::{Transport, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//!
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! ```
//!
//! ## Async tokio 0.2
//!
//! ```rust
//! # #[cfg(feature = "tokio02")]
//! # async fn run() {
//! use std::env::temp_dir;
//! use lettre::{Tokio02Transport, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//!
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # }
//! ```
//!
//! ## Async async-std 1.x
//!
//! ```rust
//! # #[cfg(feature = "async-std1")]
//! # async fn run() {
//! use std::env::temp_dir;
//! use lettre::{AsyncStd1Transport, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//!
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # }
//! ```
//!
//! ---
//!
//! Example result
//!
//! ```json
//! {
//! "envelope": {
//! "forward_path": [
//! "hei@domain.tld"
//! ],
//! "reverse_path": "nobody@domain.tld"
//! },
//! "raw_message": null,
//! "message": "From: NoBody <nobody@domain.tld>\r\nReply-To: Yuin <yuin@domain.tld>\r\nTo: Hei <hei@domain.tld>\r\nSubject: Happy new year\r\nDate: Tue, 18 Aug 2020 22:50:17 GMT\r\n\r\nBe happy!"
//! }
//! ```
use email::SendableEmail;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
pub use self::error::Error;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use async_trait::async_trait;
use std::{
path::{Path, PathBuf},
str,
};
use uuid::Uuid;
use transport::EmailTransport;
use transport::file::error::FileResult;
mod error;
pub mod error;
type Id = String;
/// Writes the content and the envelope information to a file
pub struct FileEmailTransport {
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FileTransport {
path: PathBuf,
}
impl FileEmailTransport {
impl FileTransport {
/// 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 }
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport {
path: PathBuf::from(path.as_ref()),
}
}
}
impl EmailTransport<FileResult> for FileEmailTransport {
fn send<T: SendableEmail>(&mut self, email: T) -> FileResult {
let mut file = self.path.clone();
file.push(format!("{}.txt", email.message_id()));
let mut f = try!(File::create(file.as_path()));
let log_line = format!("{}: from=<{}> to=<{}>\n",
email.message_id(),
email.from_address(),
email.to_addresses().join("> to=<"));
try!(f.write_all(log_line.as_bytes()));
try!(f.write_all(email.message().clone().as_bytes()));
info!("{} status=<written>", log_line);
Ok(())
#[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>,
}
fn close(&mut self) {
()
impl FileTransport {
fn send_raw_impl(
&self,
envelope: &Envelope,
email: &[u8],
) -> Result<(Uuid, PathBuf, String), serde_json::Error> {
let email_id = Uuid::new_v4();
let file = self.path.join(format!("{}.json", email_id));
let serialized = match str::from_utf8(email) {
// Serialize as UTF-8 string if possible
Ok(m) => serde_json::to_string(&SerializableEmail {
envelope: envelope.clone(),
message: Some(m),
raw_message: None,
}),
Err(_) => serde_json::to_string(&SerializableEmail {
envelope: envelope.clone(),
message: None,
raw_message: Some(email),
}),
}?;
Ok((email_id, file, serialized))
}
}
impl Transport for FileTransport {
type Ok = Id;
type Error = Error;
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use std::fs;
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
fs::write(file, serialized)?;
Ok(email_id.to_string())
}
}
#[cfg(feature = "async-std1")]
#[async_trait]
impl AsyncStd1Transport for FileTransport {
type Ok = Id;
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use async_std::fs;
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
fs::write(file, serialized).await?;
Ok(email_id.to_string())
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for FileTransport {
type Ok = Id;
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio02_crate::fs;
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
fs::write(file, serialized).await?;
Ok(email_id.to_string())
}
}
#[cfg(feature = "tokio03")]
#[async_trait]
impl Tokio03Transport for FileTransport {
type Ok = Id;
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio03_crate::fs;
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
fs::write(file, serialized).await?;
Ok(email_id.to_string())
}
}

View File

@@ -1,14 +1,28 @@
//! Represents an Email transport
//! ### 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")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
pub mod file;
#[cfg(feature = "sendmail-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
pub mod sendmail;
#[cfg(feature = "smtp-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
pub mod smtp;
pub mod stub;
pub mod file;
use email::SendableEmail;
/// Transport method for emails
pub trait EmailTransport<U> {
/// Sends the email
fn send<T: SendableEmail>(&mut self, email: T) -> U;
/// Close the transport explicitly
fn close(&mut self);
}

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,249 @@
//! The sendmail transport sends the email using the local sendmail command.
//!
//! ## Sync example
//!
//! ```rust
//! use lettre::{Message, Transport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//!
//! let sender = SendmailTransport::new();
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! ```
//!
//! ## Async tokio 0.2 example
//!
//! ```rust
//! # #[cfg(feature = "tokio02")]
//! # async fn run() {
//! use lettre::{Message, Tokio02Transport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//!
//! let sender = SendmailTransport::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # }
//! ```
//!
//! ## Async async-std 1.x example
//!
//!```rust
//! # #[cfg(feature = "async-std1")]
//! # async fn run() {
//! use lettre::{Message, AsyncStd1Transport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//!
//! let sender = SendmailTransport::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # }
//! ```
pub use self::error::Error;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use async_trait::async_trait;
use std::{
ffi::OsString,
io::prelude::*,
process::{Command, Stdio},
};
mod error;
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
/// Sends an email using the `sendmail` command
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SendmailTransport {
command: OsString,
}
impl SendmailTransport {
/// Creates a new transport with the default `/usr/sbin/sendmail` command
pub fn new() -> SendmailTransport {
SendmailTransport {
command: DEFAUT_SENDMAIL.into(),
}
}
/// Creates a new transport to the given sendmail command
pub fn new_with_command<S: Into<OsString>>(command: S) -> SendmailTransport {
SendmailTransport {
command: command.into(),
}
}
fn command(&self, envelope: &Envelope) -> Command {
let mut c = Command::new(&self.command);
c.arg("-i")
.arg("-f")
.arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\""))
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped());
c
}
#[cfg(feature = "tokio02")]
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
use tokio02_crate::process::Command;
let mut c = Command::new(&self.command);
c.kill_on_drop(true);
c.arg("-i")
.arg("-f")
.arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\""))
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped());
c
}
#[cfg(feature = "tokio03")]
fn tokio03_command(&self, envelope: &Envelope) -> tokio03_crate::process::Command {
use tokio03_crate::process::Command;
let mut c = Command::new(&self.command);
c.kill_on_drop(true);
c.arg("-i")
.arg("-f")
.arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\""))
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped());
c
}
}
impl Default for SendmailTransport {
fn default() -> Self {
Self::new()
}
}
impl Transport for SendmailTransport {
type Ok = ();
type Error = Error;
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
// Spawn the sendmail command
let mut process = self.command(envelope).spawn()?;
process.stdin.as_mut().unwrap().write_all(email)?;
let output = process.wait_with_output()?;
if output.status.success() {
Ok(())
} else {
Err(error::Error::Client(String::from_utf8(output.stderr)?))
}
}
}
#[cfg(feature = "async-std1")]
#[async_trait]
impl AsyncStd1Transport for SendmailTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut command = self.command(envelope);
let email = email.to_vec();
// TODO: Convert to real async, once async-std has a process implementation.
let output = async_std::task::spawn_blocking(move || {
// Spawn the sendmail command
let mut process = command.spawn()?;
process.stdin.as_mut().unwrap().write_all(&email)?;
process.wait_with_output()
})
.await?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
}
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for SendmailTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio02_crate::io::AsyncWriteExt;
let mut command = self.tokio02_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
}
}
}
#[cfg(feature = "tokio03")]
#[async_trait]
impl Tokio03Transport for SendmailTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio03_crate::io::AsyncWriteExt;
let mut command = self.tokio03_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
}
}
}

View File

@@ -0,0 +1,328 @@
use async_trait::async_trait;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
use super::Tls;
use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
};
use crate::Envelope;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct AsyncSmtpTransport<C> {
// TODO: pool
inner: AsyncSmtpClient<C>,
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
type Ok = Response;
type Error = Error;
/// Sends an email
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut conn = self.inner.connection().await?;
let result = conn.send(envelope, email).await?;
conn.quit().await?;
Ok(result)
}
}
#[cfg(feature = "tokio03")]
#[async_trait]
impl Tokio03Transport for AsyncSmtpTransport<Tokio03Connector> {
type Ok = Response;
type Error = Error;
/// Sends an email
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut conn = self.inner.connection().await?;
let result = conn.send(envelope, email).await?;
conn.quit().await?;
Ok(result)
}
}
impl<C> AsyncSmtpTransport<C>
where
C: AsyncSmtpConnector,
{
/// Simple and secure transport, using TLS connections to comunicate with the SMTP server
///
/// The right option for most SMTP servers.
///
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
))]
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{TlsParameters, SUBMISSIONS_PORT};
let tls_parameters = TlsParameters::new(relay.into())?;
Ok(Self::builder_dangerous(relay)
.port(SUBMISSIONS_PORT)
.tls(Tls::Wrapper(tls_parameters)))
}
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
///
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections.
///
/// Creates an encrypted transport over submissions port, by first connecting using
/// an unencrypted connection and then upgrading it with STARTTLS. The provided
/// domain is used to validate TLS certificates.
///
/// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
))]
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{TlsParameters, SUBMISSION_PORT};
let tls_parameters = TlsParameters::new(relay.into())?;
Ok(Self::builder_dangerous(relay)
.port(SUBMISSION_PORT)
.tls(Tls::Required(tls_parameters)))
}
/// Creates a new local SMTP client to port 25
///
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
pub fn unencrypted_localhost() -> AsyncSmtpTransport<C> {
Self::builder_dangerous("localhost").build()
}
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No authentication
/// * No TLS
/// * Port 25
///
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
let mut new = SmtpInfo::default();
new.server = server.into();
AsyncSmtpTransportBuilder { info: new }
}
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct AsyncSmtpTransportBuilder {
info: SmtpInfo,
}
/// Builder for the SMTP `AsyncSmtpTransport`
impl AsyncSmtpTransportBuilder {
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name;
self
}
/// Set the authentication mechanism to use
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.info.credentials = Some(credentials);
self
}
/// Set the authentication mechanism to use
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
self.info.authentication = mechanisms;
self
}
/// Set the port to use
pub fn port(mut self, port: u16) -> Self {
self.info.port = port;
self
}
/// Set the TLS settings to use
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
))]
pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls;
self
}
/// Build the transport (with default pool if enabled)
pub fn build<C>(self) -> AsyncSmtpTransport<C>
where
C: AsyncSmtpConnector,
{
let connector = Default::default();
let client = AsyncSmtpClient {
connector,
info: self.info,
};
AsyncSmtpTransport { inner: client }
}
}
/// Build client
#[derive(Clone)]
pub struct AsyncSmtpClient<C> {
connector: C,
info: SmtpInfo,
}
impl<C> AsyncSmtpClient<C>
where
C: AsyncSmtpConnector,
{
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
let mut conn = C::connect(
&self.info.server,
self.info.port,
&self.info.hello_name,
&self.info.tls,
)
.await?;
if let Some(credentials) = &self.info.credentials {
conn.auth(&self.info.authentication, &credentials).await?;
}
Ok(conn)
}
}
#[async_trait]
pub trait AsyncSmtpConnector: Default + private::Sealed {
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error>;
}
#[derive(Debug, Copy, Clone, Default)]
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
pub struct Tokio02Connector;
#[async_trait]
#[cfg(feature = "tokio02")]
impl AsyncSmtpConnector for Tokio02Connector {
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
.await?;
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
}
#[derive(Debug, Copy, Clone, Default)]
#[cfg(feature = "tokio03")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio03")))]
pub struct Tokio03Connector;
#[async_trait]
#[cfg(feature = "tokio03")]
impl AsyncSmtpConnector for Tokio03Connector {
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio03(hostname, port, hello_name, tls_parameters)
.await?;
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
}
mod private {
use super::*;
pub trait Sealed {}
#[cfg(feature = "tokio02")]
impl Sealed for Tokio02Connector {}
#[cfg(feature = "tokio03")]
impl Sealed for Tokio03Connector {}
}

View File

@@ -1,111 +1,170 @@
//! Provides authentication mechanisms
//! Provides limited SASL authentication mechanisms
use crypto::hmac::Hmac;
use crypto::mac::Mac;
use crypto::md5::Md5;
use crate::transport::smtp::error::Error;
use std::fmt::{self, Display, Formatter};
use rustc_serialize::base64::{self, FromBase64, ToBase64};
use rustc_serialize::hex::ToHex;
use std::fmt;
use std::fmt::{Display, Formatter};
/// Accepted authentication mechanisms
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
use transport::smtp::NUL;
use transport::smtp::error::Error;
/// 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,
}
}
}
impl<S, T> From<(S, T)> for Credentials
where
S: Into<String>,
T: Into<String>,
{
fn from((username, password): (S, T)) -> Self {
Credentials::new(username.into(), password.into())
}
}
/// 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,
/// CRAM-MD5 authentication mechanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5,
/// 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 {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
Mechanism::Plain => "PLAIN",
Mechanism::CramMd5 => "CRAM-MD5",
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 => true,
Mechanism::CramMd5 => false,
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,
username: &str,
password: &str,
challenge: Option<&str>)
-> Result<String, Error> {
match *self {
Mechanism::Plain => {
match challenge {
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, username, NUL, password)
.as_bytes()
.to_base64(base64::STANDARD))
}
}
}
Mechanism::CramMd5 => {
let encoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
None => Ok(format!(
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
)),
},
Mechanism::Login => {
let decoded_challenge =
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
let decoded_challenge = match encoded_challenge.from_base64() {
Ok(challenge) => challenge,
Err(error) => return Err(Error::ChallengeParsing(error)),
};
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
hmac.input(&decoded_challenge);
Ok(format!("{} {}", username, hmac.result().code().to_hex())
.as_bytes()
.to_base64(base64::STANDARD))
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::Mechanism;
use super::{Credentials, Mechanism};
#[test]
fn test_plain() {
let mechanism = Mechanism::Plain;
assert_eq!(mechanism.response("username", "password", None).unwrap(),
"AHVzZXJuYW1lAHBhc3N3b3Jk");
assert!(mechanism.response("username", "password", Some("test")).is_err());
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_cram_md5() {
let mechanism = Mechanism::CramMd5;
fn test_login() {
let mechanism = Mechanism::Login;
assert_eq!(mechanism.response("alice",
"wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
.unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
assert!(mechanism.response("alice", "wonderland", Some("tést")).is_err());
assert!(mechanism.response("alice", "wonderland", None).is_err());
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());
}
#[test]
fn test_from_user_pass_for_credentials() {
assert_eq!(
Credentials::new("alice".to_string(), "wonderland".to_string()),
Credentials::from(("alice", "wonderland"))
);
}
}

View File

@@ -0,0 +1,282 @@
use std::{fmt::Display, io};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
commands::*,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
Envelope,
};
#[cfg(feature = "tracing")]
use super::escape_crlf;
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort().await;
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client
pub struct AsyncSmtpConnection {
/// TCP stream between client and server
/// Value is None before connection
stream: BufReader<AsyncNetworkStream>,
/// Panic state
panic: bool,
/// Information about the server
server_info: ServerInfo,
}
impl AsyncSmtpConnection {
pub fn server_info(&self) -> &ServerInfo {
&self.server_info
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio02")]
pub async fn connect_tokio02(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio02(hostname, port, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio03")]
pub async fn connect_tokio03(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio03(hostname, port, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
async fn connect_impl(
stream: AsyncNetworkStream,
hello_name: &ClientId,
) -> Result<AsyncSmtpConnection, Error> {
let stream = BufReader::new(stream);
let mut conn = AsyncSmtpConnection {
stream,
panic: false,
server_info: ServerInfo::default(),
};
// TODO log
let _response = conn.read_response().await?;
conn.ehlo(hello_name).await?;
// Print server information
#[cfg(feature = "tracing")]
tracing::debug!("server {}", conn.server_info);
Ok(conn)
}
pub async fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
// Mail
let mut mail_options = vec![];
if self.server_info().supports_feature(Extension::EightBitMime) {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options))
.await,
self
);
// Recipient
for to_address in envelope.to() {
try_smtp!(
self.command(Rcpt::new(to_address.clone(), vec![])).await,
self
);
}
// Data
try_smtp!(self.command(Data).await, self);
// Message content
let result = try_smtp!(self.message(email).await, self);
Ok(result)
}
pub fn has_broken(&self) -> bool {
self.panic
}
pub fn can_starttls(&self) -> bool {
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
}
#[allow(unused_variables)]
pub async fn starttls(
&mut self,
tls_parameters: TlsParameters,
hello_name: &ClientId,
) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) {
try_smtp!(self.command(Starttls).await, self);
try_smtp!(
self.stream.get_mut().upgrade_tls(tls_parameters).await,
self
);
#[cfg(feature = "tracing")]
tracing::debug!("connection encrypted");
// Send EHLO again
try_smtp!(self.ehlo(hello_name).await, self);
Ok(())
} else {
Err(Error::Client("STARTTLS is not supported on this server"))
}
}
/// Send EHLO and update server info
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self);
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
Ok(())
}
pub async fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit).await, self))
}
pub async fn abort(&mut self) {
// Only try to quit if we are not already broken
if !self.panic {
self.panic = true;
let _ = self.command(Quit).await;
}
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: AsyncNetworkStream) {
self.stream = BufReader::new(stream);
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
self.stream.get_ref().is_encrypted()
}
/// Checks if the server is connected using the NOOP SMTP command
pub async fn test_connected(&mut self) -> bool {
self.command(Noop).await.is_ok()
}
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
pub async fn auth(
&mut self,
mechanisms: &[Mechanism],
credentials: &Credentials,
) -> Result<Response, Error> {
let mechanism = self
.server_info
.get_auth_mechanism(mechanisms)
.ok_or(Error::Client(
"No compatible authentication mechanism was found",
))?;
// Limit challenges to avoid blocking
let mut challenges = 10;
let mut response = self
.command(Auth::new(mechanism, credentials.clone(), None)?)
.await?;
while challenges > 0 && response.has_code(334) {
challenges -= 1;
response = try_smtp!(
self.command(Auth::new_from_response(
mechanism,
credentials.clone(),
&response,
)?)
.await,
self
);
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
} else {
Ok(response)
}
}
/// Sends the message content
pub async fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new();
codec.encode(message, &mut out_buf);
self.write(out_buf.as_slice()).await?;
self.write(b"\r\n.\r\n").await?;
self.read_response().await
}
/// Sends an SMTP command
pub async fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
self.write(command.to_string().as_bytes()).await?;
self.read_response().await
}
/// Writes a string to the server
async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_mut().write_all(string).await?;
self.stream.get_mut().flush().await?;
#[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
Ok(())
}
/// Gets the SMTP response
pub async fn read_response(&mut self) -> Result<Response, Error> {
let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer).await? > 0 {
#[cfg(feature = "tracing")]
tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) {
Ok((_remaining, response)) => {
if response.is_positive() {
return Ok(response);
}
return Err(response.into());
}
Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.1));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.1));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
}
}

View File

@@ -0,0 +1,415 @@
#[cfg(feature = "tokio02-rustls-tls")]
use std::sync::Arc;
use std::{
net::{Shutdown, SocketAddr},
pin::Pin,
task::{Context, Poll},
};
use futures_io::{Error as IoError, ErrorKind, Result as IoResult};
#[cfg(feature = "tokio02")]
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
#[cfg(feature = "tokio02")]
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
#[cfg(feature = "tokio03")]
use tokio03_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio03ReadBuf};
#[cfg(feature = "tokio03")]
use tokio03_crate::net::TcpStream as Tokio03TcpStream;
#[cfg(feature = "tokio02-native-tls")]
use tokio02_native_tls_crate::TlsStream as Tokio02TlsStream;
#[cfg(feature = "tokio03-native-tls")]
use tokio03_native_tls_crate::TlsStream as Tokio03TlsStream;
#[cfg(feature = "tokio02-rustls-tls")]
use tokio02_rustls::client::TlsStream as Tokio02RustlsTlsStream;
#[cfg(feature = "tokio03-rustls-tls")]
use tokio03_rustls::client::TlsStream as Tokio03RustlsTlsStream;
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
))]
use super::InnerTlsParameters;
use super::TlsParameters;
use crate::transport::smtp::Error;
/// A network stream
pub struct AsyncNetworkStream {
inner: InnerAsyncNetworkStream,
}
/// Represents the different types of underlying network streams
// usually only one TLS backend at a time is going to be enabled,
// so clippy::large_enum_variant doesn't make sense here
#[allow(clippy::large_enum_variant)]
#[allow(dead_code)]
enum InnerAsyncNetworkStream {
/// Plain Tokio 0.2 TCP stream
#[cfg(feature = "tokio02")]
Tokio02Tcp(Tokio02TcpStream),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-native-tls")]
Tokio02NativeTls(Tokio02TlsStream<Tokio02TcpStream>),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-rustls-tls")]
Tokio02RustlsTls(Tokio02RustlsTlsStream<Tokio02TcpStream>),
/// Plain Tokio 0.3 TCP stream
#[cfg(feature = "tokio03")]
Tokio03Tcp(Tokio03TcpStream),
/// Encrypted Tokio 0.3 TCP stream
#[cfg(feature = "tokio03-native-tls")]
Tokio03NativeTls(Tokio03TlsStream<Tokio03TcpStream>),
/// Encrypted Tokio 0.3 TCP stream
#[cfg(feature = "tokio03-rustls-tls")]
Tokio03RustlsTls(Tokio03RustlsTlsStream<Tokio03TcpStream>),
/// Can't be built
None,
}
impl AsyncNetworkStream {
fn new(inner: InnerAsyncNetworkStream) -> Self {
if let InnerAsyncNetworkStream::None = inner {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
}
AsyncNetworkStream { inner }
}
/// Returns peer's address
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Err(IoError::new(
ErrorKind::Other,
"InnerAsyncNetworkStream::None must never be built",
))
}
}
}
/// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> IoResult<()> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.shutdown(how),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().shutdown(how)
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.shutdown(how),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref s) => s.shutdown(how),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().shutdown(how)
}
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref s) => s.get_ref().0.shutdown(how),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Ok(())
}
}
}
#[cfg(feature = "tokio02")]
pub async fn connect_tokio02(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio02TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
}
#[cfg(feature = "tokio03")]
pub async fn connect_tokio03(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio03TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio03Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
}
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
match &self.inner {
#[cfg(all(
feature = "tokio02",
not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-rustls-tls feature");
}
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
Ok(())
}
#[cfg(all(
feature = "tokio03",
not(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio03Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio03-native-tls or the tokio03-rustls-tls feature");
}
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
InnerAsyncNetworkStream::Tokio03Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio03Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio03_tls(tcp_stream, tls_parameters).await?;
Ok(())
}
_ => Ok(()),
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
async fn upgrade_tokio02_tls(
tcp_stream: Tokio02TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio02-native-tls"))]
panic!("built without the tokio02-native-tls feature");
#[cfg(feature = "tokio02-native-tls")]
return {
use tokio02_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio02-rustls-tls"))]
panic!("built without the tokio02-rustls-tls feature");
#[cfg(feature = "tokio02-rustls-tls")]
return {
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
};
}
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
async fn upgrade_tokio03_tls(
tcp_stream: Tokio03TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio03-native-tls"))]
panic!("built without the tokio03-native-tls feature");
#[cfg(feature = "tokio03-native-tls")]
return {
use tokio03_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio03NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio03-rustls-tls"))]
panic!("built without the tokio03-rustls-tls feature");
#[cfg(feature = "tokio03-rustls-tls")]
return {
use tokio03_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio03RustlsTls(stream))
};
}
}
}
pub fn is_encrypted(&self) -> bool {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(_) => false,
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(_) => false,
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(_) => true,
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(_) => true,
InnerAsyncNetworkStream::None => false,
}
}
}
impl futures_io::AsyncRead for AsyncNetworkStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<IoResult<usize>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
}
}
}
impl futures_io::AsyncWrite for AsyncNetworkStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<IoResult<usize>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
}
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
}
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<IoResult<()>> {
Poll::Ready(self.shutdown(Shutdown::Write))
}
}

View File

@@ -0,0 +1,267 @@
use std::{
fmt::Display,
io::{self, BufRead, BufReader, Write},
net::ToSocketAddrs,
time::Duration,
};
use super::{ClientCodec, NetworkStream, TlsParameters};
use crate::address::Envelope;
use crate::transport::smtp::{
authentication::{Credentials, Mechanism},
commands::*,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
};
#[cfg(feature = "tracing")]
use super::escape_crlf;
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort();
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client
pub struct SmtpConnection {
/// TCP stream between client and server
/// Value is None before connection
stream: BufReader<NetworkStream>,
/// Panic state
panic: bool,
/// Information about the server
server_info: ServerInfo,
}
impl SmtpConnection {
pub fn server_info(&self) -> &ServerInfo {
&self.server_info
}
// FIXME add simple connect and rename this one
/// Connects to the configured server
///
/// Sends EHLO and parses server information
pub fn connect<A: ToSocketAddrs>(
server: A,
timeout: Option<Duration>,
hello_name: &ClientId,
tls_parameters: Option<&TlsParameters>,
) -> Result<SmtpConnection, Error> {
let stream = NetworkStream::connect(server, timeout, tls_parameters)?;
let stream = BufReader::new(stream);
let mut conn = SmtpConnection {
stream,
panic: false,
server_info: ServerInfo::default(),
};
conn.set_timeout(timeout)?;
// TODO log
let _response = conn.read_response()?;
conn.ehlo(hello_name)?;
// Print server information
#[cfg(feature = "tracing")]
tracing::debug!("server {}", conn.server_info);
Ok(conn)
}
pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
// Mail
let mut mail_options = vec![];
if self.server_info().supports_feature(Extension::EightBitMime) {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options)),
self
);
// Recipient
for to_address in envelope.to() {
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
}
// Data
try_smtp!(self.command(Data), self);
// Message content
let result = try_smtp!(self.message(email), self);
Ok(result)
}
pub fn has_broken(&self) -> bool {
self.panic
}
pub fn can_starttls(&self) -> bool {
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
}
#[allow(unused_variables)]
pub fn starttls(
&mut self,
tls_parameters: &TlsParameters,
hello_name: &ClientId,
) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
{
try_smtp!(self.command(Starttls), self);
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
#[cfg(feature = "tracing")]
tracing::debug!("connection encrypted");
// Send EHLO again
try_smtp!(self.ehlo(hello_name), self);
Ok(())
}
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
// This should never happen as `Tls` can only be created
// when a TLS library is enabled
unreachable!("TLS support required but not supported");
} else {
Err(Error::Client("STARTTLS is not supported on this server"))
}
}
/// Send EHLO and update server info
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
Ok(())
}
pub fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit), self))
}
pub fn abort(&mut self) {
// Only try to quit if we are not already broken
if !self.panic {
self.panic = true;
let _ = self.command(Quit);
}
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: NetworkStream) {
self.stream = BufReader::new(stream);
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
self.stream.get_ref().is_encrypted()
}
/// Set timeout
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
self.stream.get_mut().set_read_timeout(duration)?;
self.stream.get_mut().set_write_timeout(duration)
}
/// Checks if the server is connected using the NOOP SMTP command
pub fn test_connected(&mut self) -> bool {
self.command(Noop).is_ok()
}
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
pub fn auth(
&mut self,
mechanisms: &[Mechanism],
credentials: &Credentials,
) -> Result<Response, Error> {
let mechanism = self
.server_info
.get_auth_mechanism(mechanisms)
.ok_or(Error::Client(
"No compatible authentication mechanism was found",
))?;
// Limit challenges to avoid blocking
let mut challenges = 10;
let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
while challenges > 0 && response.has_code(334) {
challenges -= 1;
response = try_smtp!(
self.command(Auth::new_from_response(
mechanism,
credentials.clone(),
&response,
)?),
self
);
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
} else {
Ok(response)
}
}
/// Sends the message content
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new();
codec.encode(message, &mut out_buf);
self.write(out_buf.as_slice())?;
self.write(b"\r\n.\r\n")?;
self.read_response()
}
/// Sends an SMTP command
pub fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
self.write(command.to_string().as_bytes())?;
self.read_response()
}
/// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.get_mut().write_all(string)?;
self.stream.get_mut().flush()?;
#[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
Ok(())
}
/// Gets the SMTP response
pub fn read_response(&mut self) -> Result<Response, Error> {
let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer)? > 0 {
#[cfg(feature = "tracing")]
tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) {
Ok((_remaining, response)) => {
if response.is_positive() {
return Ok(response);
}
return Err(response.into());
}
Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.1));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.1));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
}
}

View File

@@ -0,0 +1,122 @@
#![allow(missing_docs)]
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
use std::{
io::{self, Cursor, Read, Write},
sync::{Arc, Mutex},
};
pub type MockCursor = Cursor<Vec<u8>>;
#[derive(Clone, Debug)]
pub struct MockStream {
reader: Arc<Mutex<MockCursor>>,
writer: Arc<Mutex<MockCursor>>,
}
impl Default for MockStream {
fn default() -> Self {
Self::new()
}
}
impl MockStream {
pub fn new() -> MockStream {
MockStream {
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
}
pub fn with_vec(vec: Vec<u8>) -> MockStream {
MockStream {
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
}
pub fn take_vec(&mut self) -> Vec<u8> {
let mut cursor = self.writer.lock().unwrap();
let vec = cursor.get_ref().to_vec();
cursor.set_position(0);
cursor.get_mut().clear();
vec
}
pub fn next_vec(&mut self, vec: &[u8]) {
let mut cursor = self.reader.lock().unwrap();
cursor.set_position(0);
cursor.get_mut().clear();
cursor.get_mut().extend_from_slice(vec);
}
pub fn swap(&mut self) {
let mut cur_write = self.writer.lock().unwrap();
let mut cur_read = self.reader.lock().unwrap();
let vec_write = cur_write.get_ref().to_vec();
let vec_read = cur_read.get_ref().to_vec();
cur_write.set_position(0);
cur_read.set_position(0);
cur_write.get_mut().clear();
cur_read.get_mut().clear();
// swap cursors
cur_read.get_mut().extend_from_slice(vec_write.as_slice());
cur_write.get_mut().extend_from_slice(vec_read.as_slice());
}
}
impl Write for MockStream {
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
self.writer.lock().unwrap().write(msg)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.lock().unwrap().flush()
}
}
impl Read for MockStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.lock().unwrap().read(buf)
}
}
#[cfg(test)]
mod test {
use super::MockStream;
use std::io::{Read, Write};
#[test]
fn write_take_test() {
let mut mock = MockStream::new();
// write to mock stream
mock.write_all(&[1, 2, 3]).unwrap();
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
}
#[test]
fn read_with_vec_test() {
let mut mock = MockStream::with_vec(vec![4, 5]);
let mut vec = Vec::new();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![4, 5]);
}
#[test]
fn clone_test() {
let mut mock = MockStream::new();
let mut cloned = mock.clone();
mock.write_all(&[6, 7]).unwrap();
assert_eq!(cloned.take_vec(), vec![6, 7]);
}
#[test]
fn swap_test() {
let mut mock = MockStream::new();
let mut vec = Vec::new();
mock.write_all(&[8, 9, 10]).unwrap();
mock.swap();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![8, 9, 10]);
}
}

View File

@@ -1,288 +1,137 @@
//! SMTP client
//!
//! `SmtpConnection` allows manually sending SMTP commands.
//!
//! ```rust,no_run
//! # #[cfg(feature = "smtp-transport")]
//! # {
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection};
//!
//! let hello = ClientId::Domain("my_hostname".to_string());
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None).unwrap();
//! client.command(
//! Mail::new(Some("user@example.com".parse().unwrap()), vec![])
//! ).unwrap();
//! client.command(
//! Rcpt::new("user@example.org".parse().unwrap(), vec![])
//! ).unwrap();
//! client.command(Data).unwrap();
//! client.message("Test email".as_bytes()).unwrap();
//! client.command(Quit).unwrap();
//! # }
//! ```
use bufstream::BufStream;
use openssl::ssl::SslContext;
#[cfg(feature = "serde")]
use std::fmt::Debug;
use std::io;
use std::io::{BufRead, Read, Write};
use std::net::ToSocketAddrs;
use std::string::String;
use transport::smtp::{CRLF, MESSAGE_ENDING};
use transport::smtp::authentication::Mechanism;
use transport::smtp::client::net::{Connector, NetworkStream};
use transport::smtp::error::{SmtpResult, Error};
use transport::smtp::response::ResponseParser;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
pub(crate) use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
pub(crate) use self::async_net::AsyncNetworkStream;
use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub(super) use self::tls::InnerTlsParameters;
pub use self::{
connection::SmtpConnection,
mock::MockStream,
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
};
pub mod net;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
mod async_connection;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
mod async_net;
mod connection;
mod mock;
mod net;
mod tls;
/// Returns the string after adding a dot at the beginning of each line starting with a dot
///
/// Reference : https://tools.ietf.org/html/rfc5321#page-62 (4.5.2. Transparency)
#[inline]
fn escape_dot(string: &str) -> String {
if string.starts_with('.') {
format!(".{}", string)
} else {
string.to_string()
/// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClientCodec {
escape_count: u8,
}
impl ClientCodec {
/// Creates a new client codec
pub fn new() -> Self {
ClientCodec::default()
}
/// Adds transparency
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
match frame.len() {
0 => {
match self.escape_count {
0 => buf.extend_from_slice(b"\r\n.\r\n"),
1 => buf.extend_from_slice(b"\n.\r\n"),
2 => buf.extend_from_slice(b".\r\n"),
_ => unreachable!(),
}
self.escape_count = 0;
}
_ => {
let mut start = 0;
for (idx, byte) in frame.iter().enumerate() {
match self.escape_count {
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
2 => self.escape_count = if *byte == b'.' { 3 } else { 0 },
_ => unreachable!(),
}
if self.escape_count == 3 {
self.escape_count = 0;
buf.extend_from_slice(&frame[start..idx]);
buf.extend_from_slice(b".");
start = idx;
}
}
buf.extend_from_slice(&frame[start..]);
}
}
}
.replace("\r.", "\r..")
.replace("\n.", "\n..")
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
#[inline]
fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CR><LF>")
}
/// Returns the string removing all the CRLF
#[inline]
fn remove_crlf(string: &str) -> String {
string.replace(CRLF, "")
}
/// Structure that implements the SMTP client
#[derive(Debug)]
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))
})
);
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 + Debug> Client<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.quit();
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, ssl_context: &SslContext) -> io::Result<()> {
match self.stream {
Some(ref mut stream) => stream.get_mut().upgrade_tls(ssl_context),
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,
}
}
/// Connects to the configured server
pub fn connect<A: ToSocketAddrs>(&mut self,
addr: &A,
ssl_context: Option<&SslContext>)
-> 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 = try!(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(try!(Connector::connect(&server_addr, ssl_context)));
self.get_reply()
}
/// Checks if the server is connected using the NOOP SMTP command
pub fn is_connected(&mut self) -> bool {
self.noop().is_ok()
}
/// Sends an SMTP command
pub fn command(&mut self, command: &str) -> SmtpResult {
self.send_server(command, CRLF)
}
/// Sends a EHLO command
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
self.command(&format!("EHLO {}", hostname))
}
/// Sends a MAIL command
pub fn mail(&mut self, address: &str, options: Option<&str>) -> SmtpResult {
match options {
Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)),
None => self.command(&format!("MAIL FROM:<{}>", address)),
}
}
/// Sends a RCPT command
pub fn rcpt(&mut self, address: &str) -> SmtpResult {
self.command(&format!("RCPT TO:<{}>", address))
}
/// Sends a DATA command
pub fn data(&mut self) -> SmtpResult {
self.command("DATA")
}
/// Sends a QUIT command
pub fn quit(&mut self) -> SmtpResult {
self.command("QUIT")
}
/// Sends a NOOP command
pub fn noop(&mut self) -> SmtpResult {
self.command("NOOP")
}
/// Sends a HELP command
pub fn help(&mut self, argument: Option<&str>) -> SmtpResult {
match argument {
Some(ref argument) => self.command(&format!("HELP {}", argument)),
None => self.command("HELP"),
}
}
/// Sends a VRFY command
pub fn vrfy(&mut self, address: &str) -> SmtpResult {
self.command(&format!("VRFY {}", address))
}
/// Sends a EXPN command
pub fn expn(&mut self, address: &str) -> SmtpResult {
self.command(&format!("EXPN {}", address))
}
/// Sends a RSET command
pub fn rset(&mut self) -> SmtpResult {
self.command("RSET")
}
/// Sends an AUTH command with the given mechanism
pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> SmtpResult {
if mechanism.supports_initial_response() {
self.command(&format!("AUTH {} {}",
mechanism,
try!(mechanism.response(username, password, None))))
} else {
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
Some(challenge) => challenge,
None => return Err(Error::ResponseParsing("Could not read CRAM challenge")),
};
debug!("CRAM challenge: {}", encoded_challenge);
let cram_response = try!(mechanism.response(username,
password,
Some(&encoded_challenge)));
self.command(&cram_response.clone())
}
}
/// Sends a STARTTLS command
pub fn starttls(&mut self) -> SmtpResult {
self.command("STARTTLS")
}
/// Sends the message content
pub fn message(&mut self, message_content: &str) -> SmtpResult {
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
}
/// Sends a string to the server and gets the response
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
if self.stream.is_none() {
return Err(From::from("Connection closed"));
}
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
try!(self.stream.as_mut().unwrap().flush());
debug!("Wrote: {}", escape_crlf(string));
self.get_reply()
}
/// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult {
let mut parser = ResponseParser::default();
let mut line = String::new();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
debug!("Read: {}", escape_crlf(line.as_ref()));
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
line.clear();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
}
let response = try!(parser.response());
if response.is_positive() {
Ok(response)
} else {
Err(From::from(response))
}
}
/// Used for debug displays
#[cfg(feature = "tracing")]
pub(super) fn escape_crlf(string: &str) -> String {
string.replace("\r\n", "<CRLF>")
}
#[cfg(test)]
mod test {
use super::{escape_crlf, escape_dot, remove_crlf};
use super::*;
#[test]
fn test_escape_dot() {
assert_eq!(escape_dot(".test"), "..test");
assert_eq!(escape_dot("\r.\n.\r\n"), "\r..\n..\r\n");
assert_eq!(escape_dot("test\r\n.test\r\n"), "test\r\n..test\r\n");
assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest");
}
#[test]
fn test_remove_crlf() {
assert_eq!(remove_crlf("\r\n"), "");
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
assert_eq!(remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_nameSIZE 42");
fn test_codec() {
let mut codec = ClientCodec::new();
let mut buf: Vec<u8> = vec![];
codec.encode(b"test\r\n", &mut buf);
codec.encode(b".\r\n", &mut buf);
codec.encode(b"\r\ntest", &mut buf);
codec.encode(b"te\r\n.\r\nst", &mut buf);
codec.encode(b"test", &mut buf);
codec.encode(b"test.", &mut buf);
codec.encode(b"test\n", &mut buf);
codec.encode(b".test\n", &mut buf);
codec.encode(b"test", &mut buf);
assert_eq!(
String::from_utf8(buf).unwrap(),
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
);
}
#[test]
#[cfg(feature = "log")]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
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,100 +1,240 @@
//! A trait to represent a stream
use openssl::ssl::{SslContext, SslStream};
use std::fmt;
use std::fmt::{Debug, Formatter};
use std::io;
use std::io::{ErrorKind, Read, Write};
use std::net::{SocketAddr, TcpStream};
/// 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, ssl_context: Option<&SslContext>) -> io::Result<Self>;
/// Upgrades to TLS connection
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()>;
/// Is the NetworkStream encrypted
fn is_encrypted(&self) -> bool;
}
impl Connector for NetworkStream {
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<NetworkStream> {
let tcp_stream = try!(TcpStream::connect(addr));
match ssl_context {
Some(context) => {
match SslStream::connect(context, tcp_stream) {
Ok(stream) => Ok(NetworkStream::Ssl(stream)),
Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
}
}
None => Ok(NetworkStream::Plain(tcp_stream)),
}
}
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
*self = match *self {
NetworkStream::Plain(ref mut stream) => {
match SslStream::connect(ssl_context, stream.try_clone().unwrap()) {
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
}
}
NetworkStream::Ssl(_) => return Ok(()),
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
use std::{
io::{self, Read, Write},
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
time::Duration,
};
Ok(())
#[cfg(feature = "native-tls")]
use native_tls::TlsStream;
}
#[cfg(feature = "rustls-tls")]
use rustls::{ClientSession, StreamOwned};
fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Plain(_) => false,
NetworkStream::Ssl(_) => true,
}
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::InnerTlsParameters;
use super::{MockStream, TlsParameters};
use crate::transport::smtp::Error;
/// A network stream
pub struct NetworkStream {
inner: InnerNetworkStream,
}
/// Represents the different types of underlying network streams
pub enum NetworkStream {
/// Plain TCP
Plain(TcpStream),
/// SSL over TCP
Ssl(SslStream<TcpStream>),
// usually only one TLS backend at a time is going to be enabled,
// so clippy::large_enum_variant doesn't make sense here
#[allow(clippy::large_enum_variant)]
enum InnerNetworkStream {
/// Plain TCP stream
Tcp(TcpStream),
/// Encrypted TCP stream
#[cfg(feature = "native-tls")]
NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream
#[cfg(feature = "rustls-tls")]
RustlsTls(StreamOwned<ClientSession, TcpStream>),
/// Mock stream
Mock(MockStream),
}
impl Debug for NetworkStream {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NetworkStream(_)")
impl NetworkStream {
fn new(inner: InnerNetworkStream) -> Self {
NetworkStream { inner }
}
pub fn new_mock(mock: MockStream) -> Self {
Self::new(InnerNetworkStream::Mock(mock))
}
/// Returns peer's address
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
match self.inner {
InnerNetworkStream::Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
))),
}
}
/// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
match self.inner {
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::Mock(_) => Ok(()),
}
}
pub fn connect<T: ToSocketAddrs>(
server: T,
timeout: Option<Duration>,
tls_parameters: Option<&TlsParameters>,
) -> Result<NetworkStream, Error> {
fn try_connect_timeout<T: ToSocketAddrs>(
server: T,
timeout: Duration,
) -> Result<TcpStream, Error> {
let addrs = server.to_socket_addrs()?;
for addr in addrs {
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
return Ok(result);
}
}
Err(Error::Client("Could not connect"))
}
let tcp_stream = match timeout {
Some(t) => try_connect_timeout(server, t)?,
None => TcpStream::connect(server)?,
};
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters)?;
}
Ok(stream)
}
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
match &self.inner {
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
InnerNetworkStream::Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
InnerNetworkStream::Tcp(_) => {
// get owned TcpStream
let tcp_stream =
std::mem::replace(&mut self.inner, InnerNetworkStream::Mock(MockStream::new()));
let tcp_stream = match tcp_stream {
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
Ok(())
}
_ => Ok(()),
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
fn upgrade_tls_impl(
tcp_stream: TcpStream,
tls_parameters: &TlsParameters,
) -> Result<InnerNetworkStream, Error> {
Ok(match &tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
let stream = connector
.connect(tls_parameters.domain(), tcp_stream)
.map_err(|err| Error::Io(io::Error::new(io::ErrorKind::Other, err)))?;
InnerNetworkStream::NativeTls(stream)
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(connector) => {
use webpki::DNSNameRef;
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())?;
let stream = StreamOwned::new(
ClientSession::new(&Arc::new(connector.clone()), domain),
tcp_stream,
);
InnerNetworkStream::RustlsTls(stream)
}
})
}
pub fn is_encrypted(&self) -> bool {
match self.inner {
InnerNetworkStream::Tcp(_) | InnerNetworkStream::Mock(_) => false,
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(_) => true,
}
}
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
InnerNetworkStream::Mock(_) => Ok(()),
}
}
/// Set write timeout for IO calls
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::Mock(_) => Ok(()),
}
}
}
impl Read for NetworkStream {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.read(buf),
NetworkStream::Ssl(ref mut stream) => stream.read(buf),
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.read(buf),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
InnerNetworkStream::Mock(ref mut s) => s.read(buf),
}
}
}
impl Write for NetworkStream {
#[inline]
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.write(msg),
NetworkStream::Ssl(ref mut stream) => stream.write(msg),
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.write(buf),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
InnerNetworkStream::Mock(ref mut s) => s.write(buf),
}
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.flush(),
NetworkStream::Ssl(ref mut stream) => stream.flush(),
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.flush(),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
InnerNetworkStream::Mock(ref mut s) => s.flush(),
}
}
}

View File

@@ -0,0 +1,280 @@
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
#[cfg(feature = "rustls-tls")]
use webpki::DNSNameRef;
use crate::transport::smtp::error::Error;
/// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
// This is also rustls' default behavior
#[cfg(feature = "native-tls")]
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_copy_implementations)]
pub enum Tls {
/// Insecure connection only (for testing purposes)
None,
/// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Opportunistic(TlsParameters),
/// Start with insecure connection and require `STARTTLS`
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Required(TlsParameters),
/// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Wrapper(TlsParameters),
}
/// Parameters to use for secure clients
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct TlsParameters {
pub(crate) connector: InnerTlsParameters,
/// The domain name which is expected in the TLS certificate from the server
pub(super) domain: String,
}
/// Builder for `TlsParameters`
#[derive(Clone)]
pub struct TlsParametersBuilder {
domain: String,
root_certs: Vec<Certificate>,
accept_invalid_hostnames: bool,
accept_invalid_certs: bool,
}
impl TlsParametersBuilder {
/// Creates a new builder for `TlsParameters`
pub fn new(domain: String) -> Self {
Self {
domain,
root_certs: Vec::new(),
accept_invalid_hostnames: false,
accept_invalid_certs: false,
}
}
/// Add a custom root certificate
///
/// Can be used to safely connect to a server using a self signed certificate, for example.
pub fn add_root_certificate(&mut self, cert: Certificate) -> &mut Self {
self.root_certs.push(cert);
self
}
/// Controls whether certificates with an invalid hostname are accepted
///
/// Defaults to `false`.
///
/// # Warning
///
/// You should think very carefully before using this method.
/// If hostname verification is disabled *any* valid certificate,
/// including those from other sites, are trusted.
///
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
///
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn dangerous_accept_invalid_hostnames(
&mut self,
accept_invalid_hostnames: bool,
) -> &mut Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
}
/// Controls whether invalid certificates are accepted
///
/// Defaults to `false`.
///
/// # Warning
///
/// You should think very carefully before using this method.
/// If certificate verification is disabled, *any* certificate
/// is trusted for use, including:
///
/// * Self signed certificates
/// * Certificates from different hostnames
/// * Expired certificates
///
/// This method should only be used as a last resort, as it introduces
/// significant vulnerabilities to man-in-the-middle attacks.
pub fn dangerous_accept_invalid_certs(&mut self, accept_invalid_certs: bool) -> &mut Self {
self.accept_invalid_certs = accept_invalid_certs;
self
}
/// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn build(self) -> Result<TlsParameters, Error> {
#[cfg(feature = "native-tls")]
return self.build_native();
#[cfg(not(feature = "native-tls"))]
return self.build_rustls();
}
/// Creates a new `TlsParameters` using native-tls with the provided configuration
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn build_native(self) -> Result<TlsParameters, Error> {
let mut tls_builder = TlsConnector::builder();
for cert in self.root_certs {
tls_builder.add_root_certificate(cert.native_tls);
}
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
let connector = tls_builder.build()?;
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain,
})
}
/// Creates a new `TlsParameters` using rustls with the provided configuration
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
use webpki_roots::TLS_SERVER_ROOTS;
let mut tls = ClientConfig::new();
for cert in self.root_certs {
for rustls_cert in cert.rustls {
tls.root_store
.add(&rustls_cert)
.map_err(|_| Error::InvalidCertificate)?;
}
}
if self.accept_invalid_certs {
tls.dangerous()
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {}));
}
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(tls),
domain: self.domain,
})
}
}
#[derive(Clone)]
pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")]
NativeTls(TlsConnector),
#[cfg(feature = "rustls-tls")]
RustlsTls(ClientConfig),
}
impl TlsParameters {
/// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn new(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build()
}
pub fn builder(domain: String) -> TlsParametersBuilder {
TlsParametersBuilder::new(domain)
}
/// Creates a new `TlsParameters` using native-tls
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn new_native(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_native()
}
/// Creates a new `TlsParameters` using rustls
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
pub fn new_rustls(domain: String) -> Result<Self, Error> {
TlsParametersBuilder::new(domain).build_rustls()
}
pub fn domain(&self) -> &str {
&self.domain
}
}
/// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
#[derive(Clone)]
#[allow(missing_copy_implementations)]
pub struct Certificate {
#[cfg(feature = "native-tls")]
native_tls: native_tls::Certificate,
#[cfg(feature = "rustls-tls")]
rustls: Vec<rustls::Certificate>,
}
impl Certificate {
/// Create a `Certificate` from a DER encoded certificate
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert =
native_tls::Certificate::from_der(&der).map_err(|_| Error::InvalidCertificate)?;
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: vec![rustls::Certificate(der)],
})
}
/// Create a `Certificate` from a PEM encoded certificate
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let native_tls_cert =
native_tls::Certificate::from_pem(pem).map_err(|_| Error::InvalidCertificate)?;
#[cfg(feature = "rustls-tls")]
let rustls_cert = {
use rustls::internal::pemfile;
use std::io::Cursor;
let mut pem = Cursor::new(pem);
pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)?
};
Ok(Self {
#[cfg(feature = "native-tls")]
native_tls: native_tls_cert,
#[cfg(feature = "rustls-tls")]
rustls: rustls_cert,
})
}
}
#[cfg(feature = "rustls-tls")]
struct InvalidCertsVerifier;
#[cfg(feature = "rustls-tls")]
impl ServerCertVerifier for InvalidCertsVerifier {
fn verify_server_cert(
&self,
_roots: &RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: DNSNameRef<'_>,
_ocsp_response: &[u8],
) -> Result<ServerCertVerified, TLSError> {
Ok(ServerCertVerified::assertion())
}
}

View File

@@ -0,0 +1,374 @@
//! SMTP commands
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
error::Error,
extension::{ClientId, MailParameter, RcptParameter},
response::Response,
},
Address,
};
use std::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 let Some(argument) = &self.argument {
write!(f, " {}", argument)?;
}
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(base64::encode);
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 = "tracing")]
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
#[cfg(feature = "tracing")]
tracing::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, 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, None).unwrap()
),
"AUTH LOGIN\r\n"
);
}
}

View File

@@ -1,13 +1,14 @@
//! Error and result type for SMTP clients
use rustc_serialize::base64::FromBase64Error;
use self::Error::*;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use transport::smtp::response::{Response, Severity};
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)]
@@ -23,37 +24,73 @@ pub enum Error {
/// Error parsing a response
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(FromBase64Error),
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")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
Tls(native_tls::Error),
/// Parsing error
Parsing(nom::error::ErrorKind),
/// Invalid hostname
#[cfg(feature = "rustls-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
InvalidDNSName(webpki::InvalidDNSNameError),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
InvalidCertificate,
#[cfg(feature = "r2d2")]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
Pool(r2d2::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
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(any(feature = "native-tls", feature = "rustls-tls"))]
InvalidCertificate => fmt.write_str("invalid certificate"),
#[cfg(feature = "r2d2")]
Pool(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn description(&self) -> &str {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Transient(_) => "a transient error occured during the SMTP transaction",
Permanent(_) => "a permanent error occured during the SMTP transaction",
ResponseParsing(_) => "an error occured while parsing an SMTP response",
ChallengeParsing(_) => "an error occured while parsing a CRAM-MD5 challenge",
Resolution => "could not resolve hostname",
Client(_) => "an unknown error occured",
Io(_) => "an I/O error occured",
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
Io(ref err) => Some(&*err as &StdError),
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,
}
}
@@ -65,9 +102,52 @@ impl From<io::Error> for Error {
}
}
#[cfg(feature = "native-tls")]
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Tls(err)
}
}
impl From<nom::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.severity() {
match response.code.severity {
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
@@ -80,6 +160,3 @@ impl From<&'static str> for Error {
Client(string)
}
}
/// SMTP result type
pub type SmtpResult = Result<Response, Error>;

View File

@@ -1,16 +1,73 @@
//! ESMTP features
use std::collections::HashSet;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::result::Result;
use transport::smtp::authentication::Mechanism;
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,
};
use transport::smtp::error::Error;
use transport::smtp::response::Response;
/// Client identifier, the parameter to `EHLO`
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ClientId {
/// A fully-qualified domain name
Domain(String),
/// An IPv4 address
Ipv4(Ipv4Addr),
/// An IPv6 address
Ipv6(Ipv6Addr),
}
const LOCALHOST_CLIENT: ClientId = ClientId::Ipv4(Ipv4Addr::new(127, 0, 0, 1));
impl Default for ClientId {
fn default() -> Self {
// https://tools.ietf.org/html/rfc5321#section-4.1.4
//
// The SMTP client MUST, if possible, ensure that the domain parameter
// to the EHLO command is a primary host name as specified for this
// command in Section 2.3.5. If this is not possible (e.g., when the
// client's address is dynamically assigned and the client does not have
// an obvious name), an address literal SHOULD be substituted for the
// domain name.
#[cfg(feature = "hostname")]
{
hostname::get()
.ok()
.and_then(|s| s.into_string().map(Self::Domain).ok())
.unwrap_or(LOCALHOST_CLIENT)
}
#[cfg(not(feature = "hostname"))]
LOCALHOST_CLIENT
}
}
impl Display for ClientId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Self::Domain(ref value) => f.write_str(value),
Self::Ipv4(ref value) => write!(f, "[{}]", value),
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value),
}
}
}
impl ClientId {
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
/// Creates a new `ClientId` from a fully qualified domain name
pub fn new(domain: String) -> Self {
Self::Domain(domain)
}
}
/// Supported ESMTP keywords
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Extension {
/// 8BITMIME keyword
///
@@ -29,18 +86,19 @@ pub enum Extension {
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
Extension::StartTls => write!(f, "{}", "STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "{} {}", "AUTH", mechanism),
Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
Extension::StartTls => f.write_str("STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
}
}
}
/// Contains information about an SMTP server
#[derive(Clone,Debug,Eq,PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ServerInfo {
/// Server name
///
@@ -53,19 +111,18 @@ pub struct ServerInfo {
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f,
"{} with {}",
self.name,
match self.features.is_empty() {
true => "no supported features".to_string(),
false => format!("{:?}", self.features),
})
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let features = if self.features.is_empty() {
"no supported features".to_string()
} else {
format!("{:?}", self.features)
};
write!(f, "{} with {}", self.name, features)
}
}
impl ServerInfo {
/// Parses a response to create a `ServerInfo`
/// Parses a EHLO response to create a `ServerInfo`
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
@@ -74,10 +131,13 @@ impl ServerInfo {
let mut features: HashSet<Extension> = HashSet::new();
for line in response.message() {
for line in response.message.as_slice() {
if line.is_empty() {
continue;
}
let splitted: Vec<&str> = line.split_whitespace().collect();
match splitted[0] {
let mut split = line.split_whitespace();
match split.next().unwrap() {
"8BITMIME" => {
features.insert(Extension::EightBitMime);
}
@@ -88,13 +148,16 @@ impl ServerInfo {
features.insert(Extension::StartTls);
}
"AUTH" => {
for &mechanism in &splitted[1..] {
for mechanism in split {
match mechanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
}
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mechanism::CramMd5));
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
"XOAUTH2" => {
features.insert(Extension::Authentication(Mechanism::Xoauth2));
}
_ => (),
}
@@ -105,36 +168,146 @@ impl ServerInfo {
}
Ok(ServerInfo {
name: name,
features: features,
name: name.to_string(),
features,
})
}
/// Checks if the server supports an ESMTP feature
pub fn supports_feature(&self, keyword: &Extension) -> bool {
self.features.contains(keyword)
pub fn supports_feature(&self, keyword: Extension) -> bool {
self.features.contains(&keyword)
}
/// Checks if the server supports an ESMTP feature
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
self.features.contains(&Extension::Authentication(mechanism))
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", derive(serde::Serialize, serde::Deserialize))]
pub enum MailParameter {
/// `BODY` parameter
Body(MailBodyParameter),
/// `SIZE` parameter
Size(usize),
/// `SMTPUTF8` parameter
SmtpUtfEight,
/// Custom parameter
Other {
/// Parameter keyword
keyword: String,
/// Parameter value
value: Option<String>,
},
}
impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
MailParameter::Size(size) => write!(f, "SIZE={}", size),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other {
ref keyword,
value: Some(ref value),
} => write!(f, "{}={}", keyword, XText(value)),
MailParameter::Other {
ref keyword,
value: None,
} => f.write_str(keyword),
}
}
}
/// Values for the `BODY` parameter to `MAIL FROM`
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MailBodyParameter {
/// `7BIT`
SevenBit,
/// `8BITMIME`
EightBitMime,
}
impl Display for MailBodyParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
MailBodyParameter::SevenBit => f.write_str("7BIT"),
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
}
}
}
/// A `RCPT TO` extension parameter
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RcptParameter {
/// Custom parameter
Other {
/// Parameter keyword
keyword: String,
/// Parameter value
value: Option<String>,
},
}
impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
RcptParameter::Other {
ref keyword,
value: Some(ref value),
} => write!(f, "{}={}", keyword, XText(value)),
RcptParameter::Other {
ref keyword,
value: None,
} => f.write_str(keyword),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::transport::smtp::{
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
};
use std::collections::HashSet;
use super::{Extension, ServerInfo};
use transport::smtp::authentication::Mechanism;
use transport::smtp::response::{Category, Code, Response, Severity};
#[test]
fn test_clientid_fmt() {
assert_eq!(
format!("{}", ClientId::Domain("test".to_string())),
"test".to_string()
);
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string());
}
#[test]
fn test_extension_fmt() {
assert_eq!(format!("{}", Extension::EightBitMime),
"8BITMIME".to_string());
assert_eq!(format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string());
assert_eq!(
format!("{}", Extension::EightBitMime),
"8BITMIME".to_string()
);
assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string()
);
}
#[test]
@@ -142,64 +315,91 @@ mod test {
let mut eightbitmime = HashSet::new();
assert!(eightbitmime.insert(Extension::EightBitMime));
assert_eq!(format!("{}",
assert_eq!(
format!(
"{}",
ServerInfo {
name: "name".to_string(),
features: eightbitmime.clone(),
}),
"name with {EightBitMime}".to_string());
features: eightbitmime,
}
),
"name with {EightBitMime}".to_string()
);
let empty = HashSet::new();
assert_eq!(format!("{}",
assert_eq!(
format!(
"{}",
ServerInfo {
name: "name".to_string(),
features: empty,
}),
"name with no supported features".to_string());
}
),
"name with no supported features".to_string()
);
let mut plain = HashSet::new();
assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
assert_eq!(format!("{}",
assert_eq!(
format!(
"{}",
ServerInfo {
name: "name".to_string(),
features: plain.clone(),
}),
"name with {Authentication(Plain)}".to_string());
features: plain,
}
),
"name with {Authentication(Plain)}".to_string()
);
}
#[test]
fn test_serverinfo() {
let response =
Response::new(Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
let response = Response::new(
Code::new(
Severity::PositiveCompletion,
Category::Unspecified4,
Detail::One,
),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
);
let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo {
name: "me".to_string(),
features: features,
features,
};
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
assert!(server_info.supports_feature(&Extension::EightBitMime));
assert!(!server_info.supports_feature(&Extension::StartTls));
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
assert!(server_info.supports_feature(Extension::EightBitMime));
assert!(!server_info.supports_feature(Extension::StartTls));
let response2 =
Response::new(Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
vec!["me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
let response2 = Response::new(
Code::new(
Severity::PositiveCompletion,
Category::Unspecified4,
Detail::One,
),
vec![
"me".to_string(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]);
"SIZE 42".to_string(),
],
);
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mechanism::Plain)));
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5)));
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo {
name: "me".to_string(),
@@ -208,9 +408,8 @@ mod test {
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
assert!(server_info2.supports_feature(&Extension::EightBitMime));
assert!(server_info2.supports_feature(Extension::EightBitMime));
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
assert!(!server_info2.supports_feature(&Extension::StartTls));
assert!(!server_info2.supports_feature(Extension::StartTls));
}
}

View File

@@ -1,419 +1,244 @@
//! This transport sends emails using the SMTP protocol
//! The SMTP transport sends emails using the SMTP protocol.
//!
//! This SMTP client follows [RFC
//! 5321](https://tools.ietf.org/html/rfc5321), and is designed to efficiently send emails from an
//! application to a relay email server, as it relies as much as possible on the relay server
//! for sanity and RFC compliance checks.
//!
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](https://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * STARTTLS ([RFC 2487](https://tools.ietf.org/html/rfc2487))
//!
//! #### SMTP Transport
//!
//! This transport uses the SMTP protocol to send emails over the network (locally or remotely).
//!
//! It is designed to be:
//!
//! * Secured: connections are encrypted by default
//! * Modern: unicode support for email contents and sender/recipient addresses when compatible
//! * Fast: supports connection reuse and pooling
//!
//! This client is designed to send emails to a relay server, and should *not* be used to send
//! emails directly to the destination server.
//!
//! The relay server can be the local email server, a specific host or a third-party service.
//!
//! #### Simple example
//!
//! This is the most basic example of usage:
//!
//! ```rust,no_run
//! # #[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
//! # {
//! use lettre::{Message, Transport, SmtpTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//!
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")
//! .expect("relay valid")
//! .build();
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # }
//! ```
use email::SendableEmail;
//! #### 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();
//! # }
//! ```
//!
use openssl::ssl::{SslContext, SslMethod};
use std::net::{SocketAddr, ToSocketAddrs};
use std::string::String;
use transport::EmailTransport;
use transport::smtp::authentication::Mechanism;
use transport::smtp::client::Client;
#[cfg(feature = "tokio02")]
pub use self::async_transport::Tokio02Connector;
#[cfg(feature = "tokio03")]
pub use self::async_transport::Tokio03Connector;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
pub use self::async_transport::{
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder,
};
#[cfg(feature = "r2d2")]
pub use self::pool::PoolConfig;
pub(crate) use self::transport::SmtpClient;
pub use self::{
error::Error,
transport::{SmtpTransport, SmtpTransportBuilder},
};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use crate::transport::smtp::client::TlsParameters;
use crate::transport::smtp::{
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
client::SmtpConnection,
extension::ClientId,
response::Response,
};
use client::Tls;
use std::time::Duration;
use transport::smtp::error::{SmtpResult, Error};
use transport::smtp::extension::{Extension, ServerInfo};
pub mod extension;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
mod async_transport;
pub mod authentication;
pub mod response;
pub mod client;
pub mod error;
pub mod commands;
mod error;
pub mod extension;
#[cfg(feature = "r2d2")]
mod pool;
pub mod response;
mod transport;
pub mod util;
// Registrated port numbers:
// 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: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub const CRLF: &'static str = "\r\n";
/// Colon
pub const COLON: &'static str = ":";
/// The ending of message content
pub const MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub const NUL: &'static str = "\0";
/// TLS security level
#[derive(Debug)]
pub enum SecurityLevel {
/// Use a TLS wrapped connection
/// Default submission over TLS port
///
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
EncryptedWrapper,
/// Only send an email on encrypted connection (with STARTTLS)
///
/// Recommended mode, prevents MITM when used with verified certificates.
AlwaysEncrypt,
/// Use TLS when available (with STARTTLS)
///
/// Default mode.
Opportunistic,
/// Never use TLS
NeverEncrypt,
}
/// https://tools.ietf.org/html/rfc8314
pub const SUBMISSIONS_PORT: u16 = 465;
/// Contains client configuration
pub struct SmtpTransportBuilder {
/// Maximum connection reuse
///
/// Zero means no limitation
connection_reuse_count_limit: u16,
/// Enable connection reuse
connection_reuse: bool,
/// Name sent during HELO or EHLO
hello_name: String,
/// Credentials
credentials: Option<(String, String)>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// SSL context to use
ssl_context: SslContext,
/// TLS security level
security_level: SecurityLevel,
/// Enable UTF8 mailboxes in envelope or headers
smtp_utf8: bool,
/// Default timeout
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
#[allow(missing_debug_implementations)]
#[derive(Clone)]
struct SmtpInfo {
/// Name sent during EHLO
hello_name: ClientId,
/// Server we are connecting to
server: String,
/// Port to connect to
port: u16,
/// TLS security configuration
tls: Tls,
/// Optional enforced authentication mechanism
authentication_mechanism: Option<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>,
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder {
/// Creates a new local SMTP client
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() {
Some(addr) => {
Ok(SmtpTransportBuilder {
server_addr: addr,
ssl_context: SslContext::new(SslMethod::Tlsv1).unwrap(),
security_level: SecurityLevel::Opportunistic,
smtp_utf8: false,
impl Default for SmtpInfo {
fn default() -> Self {
Self {
server: "localhost".to_string(),
port: SMTP_PORT,
hello_name: ClientId::default(),
credentials: None,
connection_reuse_count_limit: 100,
connection_reuse: false,
hello_name: "localhost".to_string(),
authentication_mechanism: None,
})
}
None => Err(From::from("Could nor resolve hostname")),
}
}
/// Creates a new local SMTP client to port 25
pub fn localhost() -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(("localhost", SMTP_PORT))
}
/// Use STARTTLS with a specific context
pub fn ssl_context(mut self, ssl_context: SslContext) -> SmtpTransportBuilder {
self.ssl_context = ssl_context;
self
}
/// Set the security level for SSL/TLS
pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder {
self.security_level = level;
self
}
/// Require SSL/TLS using STARTTLS
///
/// Incompatible with `ssl_wrapper()``
pub fn encrypt(mut self) -> SmtpTransportBuilder {
self.security_level = SecurityLevel::AlwaysEncrypt;
self
}
/// Require SSL/TLS using SMTPS
///
/// Incompatible with `encrypt()`
pub fn ssl_wrapper(mut self) -> SmtpTransportBuilder {
self.security_level = SecurityLevel::EncryptedWrapper;
self
}
/// 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 HELO or EHLO
pub fn hello_name(mut self, name: &str) -> SmtpTransportBuilder {
self.hello_name = name.to_string();
self
}
/// Enable connection reuse
pub fn connection_reuse(mut self, enable: bool) -> SmtpTransportBuilder {
self.connection_reuse = enable;
self
}
/// Set the maximum number of emails sent using one connection
pub fn connection_reuse_count_limit(mut self, limit: u16) -> SmtpTransportBuilder {
self.connection_reuse_count_limit = limit;
self
}
/// Set the client credentials
pub fn credentials(mut self, username: &str, password: &str) -> SmtpTransportBuilder {
self.credentials = Some((username.to_string(), password.to_string()));
self
}
/// Set the authentication mechanisms
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpTransportBuilder {
self.authentication_mechanism = Some(mechanism);
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
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.reset();
}
return Err(From::from(err))
},
}
})
);
impl SmtpTransport {
/// 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: client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
/// Reset the client state
fn reset(&mut self) {
// Close the SMTP transaction if needed
self.close();
// Reset the client state
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
}
/// Gets the EHLO response and updates server information
pub fn get_ehlo(&mut self) -> SmtpResult {
// Extended Hello
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), 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)
}
}
impl EmailTransport<SmtpResult> for SmtpTransport {
/// Sends an email
fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
// Extract email information
let message_id = email.message_id();
let from_address = email.from_address();
let to_addresses = email.to_addresses();
let message = email.message();
// Check if the connection is still available
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
self.reset();
}
if self.state.connection_reuse_count == 0 {
try!(self.client.connect(&self.client_info.server_addr,
match &self.client_info.security_level {
&SecurityLevel::EncryptedWrapper => {
Some(&self.client_info.ssl_context)
}
_ => None,
}));
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
try!(self.get_ehlo());
match (&self.client_info.security_level,
self.server_info.as_ref().unwrap().supports_feature(&Extension::StartTls)) {
(&SecurityLevel::AlwaysEncrypt, false) => {
return Err(From::from("Could not encrypt connection, aborting"))
}
(&SecurityLevel::Opportunistic, false) => (),
(&SecurityLevel::NeverEncrypt, _) => (),
(&SecurityLevel::EncryptedWrapper, _) => (),
(_, true) => {
try_smtp!(self.client.starttls(), self);
try_smtp!(self.client.upgrade_tls_stream(&self.client_info.ssl_context),
self);
debug!("connection encrypted");
// Send EHLO again
try!(self.get_ehlo());
}
}
if self.client_info.credentials.is_some() {
let (username, password) = self.client_info.credentials.clone().unwrap();
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() {
// If encrypted, allow all mechanisms, with a preference for the
// simplest
vec![Mechanism::Plain, Mechanism::CramMd5]
} else {
// If not encrypted, do not allow clear-text passwords
vec![Mechanism::CramMd5]
}
}
};
for mechanism in accepted_mechanisms {
if self.server_info.as_ref().unwrap().supports_auth_mechanism(mechanism) {
found = true;
try_smtp!(self.client.auth(mechanism, &username, &password), self);
break;
}
}
if !found {
info!("No supported authentication mechanisms available");
authentication: DEFAULT_MECHANISMS.into(),
timeout: Some(DEFAULT_TIMEOUT),
tls: Tls::None,
}
}
}
// Mail
let mail_options = match (self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::EightBitMime),
self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::SmtpUtfEight)) {
(true, true) => Some("BODY=8BITMIME SMTPUTF8"),
(true, false) => Some("BODY=8BITMIME"),
(false, _) => None,
};
try_smtp!(self.client.mail(&from_address, mail_options), self);
// Log the mail command
info!("{}: from=<{}>", message_id, from_address);
// Recipient
for to_address in &to_addresses {
try_smtp!(self.client.rcpt(&to_address), self);
// Log the rcpt command
info!("{}: to=<{}>", message_id, to_address);
}
// Data
try_smtp!(self.client.data(), self);
// Message content
let result = self.client.message(&message);
if result.is_ok() {
// Increment the connection reuse counter
self.state.connection_reuse_count += 1;
// Log the message
info!("{}: conn_use={}, size={}, status=sent ({})",
message_id,
self.state.connection_reuse_count,
message.len(),
result.as_ref()
.ok()
.unwrap()
.message()
.iter()
.next()
.unwrap_or(&"no response".to_string()));
}
// Test if we can reuse the existing connection
if (!self.client_info.connection_reuse) ||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
self.reset();
}
result
}
/// Closes the inner connection
fn close(&mut self) {
self.client.close();
}
}

View File

@@ -0,0 +1,90 @@
use std::time::Duration;
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
use r2d2::{ManageConnection, Pool};
/// Configuration for a connection pool
#[derive(Debug, Clone)]
#[allow(missing_copy_implementations)]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
pub struct PoolConfig {
min_idle: u32,
max_size: u32,
connection_timeout: Duration,
idle_timeout: Duration,
}
impl PoolConfig {
/// Minimum number of idle connections
///
/// Defaults to `0`
pub fn min_idle(mut self, min_idle: u32) -> Self {
self.min_idle = min_idle;
self
}
/// Maximum number of pooled connections
///
/// Defaults to `10`
pub fn max_size(mut self, max_size: u32) -> Self {
self.min_idle = max_size;
self
}
/// Connection timeout
///
/// Defaults to `30 seconds`
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
self.connection_timeout = connection_timeout;
self
}
/// Connection idle timeout
///
/// Defaults to `60 seconds`
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.idle_timeout = idle_timeout;
self
}
pub(crate) fn build<C: ManageConnection>(&self, client: C) -> Pool<C> {
Pool::builder()
.min_idle(Some(self.min_idle))
.max_size(self.max_size)
.connection_timeout(self.connection_timeout)
.idle_timeout(Some(self.idle_timeout))
.build_unchecked(client)
}
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
min_idle: 0,
max_size: 10,
connection_timeout: Duration::from_secs(30),
idle_timeout: Duration::from_secs(60),
}
}
}
impl ManageConnection for SmtpClient {
type Connection = SmtpConnection;
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,196 +1,123 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use self::Category::*;
use self::Severity::*;
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::FromStr;
use transport::smtp::error::{Error, SmtpResult};
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", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity {
/// 2yx
PositiveCompletion,
PositiveCompletion = 2,
/// 3yz
PositiveIntermediate,
PositiveIntermediate = 3,
/// 4yz
TransientNegativeCompletion,
TransientNegativeCompletion = 4,
/// 5yz
PermanentNegativeCompletion,
}
impl FromStr for Severity {
type Err = Error;
fn from_str(s: &str) -> result::Result<Severity, Error> {
match s {
"2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err(Error::ResponseParsing("First digit must be between 2 and 5")),
}
}
PermanentNegativeCompletion = 5,
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f,
"{}",
match *self {
PositiveCompletion => 2,
PositiveIntermediate => 3,
TransientNegativeCompletion => 4,
PermanentNegativeCompletion => 5,
})
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
/// Second digit
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Category {
/// x0z
Syntax,
Syntax = 0,
/// x1z
Information,
Information = 1,
/// x2z
Connections,
Connections = 2,
/// x3z
Unspecified3,
Unspecified3 = 3,
/// x4z
Unspecified4,
Unspecified4 = 4,
/// x5z
MailSystem,
}
impl FromStr for Category {
type Err = Error;
fn from_str(s: &str) -> result::Result<Category, Error> {
match s {
"0" => Ok(Syntax),
"1" => Ok(Information),
"2" => Ok(Connections),
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"5" => Ok(MailSystem),
_ => Err(Error::ResponseParsing("Second digit must be between 0 and 5")),
}
}
MailSystem = 5,
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f,
"{}",
match *self {
Syntax => 0,
Information => 1,
Connections => 2,
Unspecified3 => 3,
Unspecified4 => 4,
MailSystem => 5,
})
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
/// The detail digit of a response code (third digit)
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Detail {
#[allow(missing_docs)]
Zero = 0,
#[allow(missing_docs)]
One = 1,
#[allow(missing_docs)]
Two = 2,
#[allow(missing_docs)]
Three = 3,
#[allow(missing_docs)]
Four = 4,
#[allow(missing_docs)]
Five = 5,
#[allow(missing_docs)]
Six = 6,
#[allow(missing_docs)]
Seven = 7,
#[allow(missing_docs)]
Eight = 8,
#[allow(missing_docs)]
Nine = 9,
}
impl Display for Detail {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
/// Represents a 3 digit SMTP response code
#[derive(PartialEq,Eq,Clone,Debug)]
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Code {
/// First digit of the response code
severity: Severity,
pub severity: Severity,
/// Second digit of the response code
category: Category,
pub category: Category,
/// Third digit
detail: u8,
pub detail: Detail,
}
impl FromStr for Code {
type Err = Error;
#[inline]
fn from_str(s: &str) -> result::Result<Code, Error> {
if s.len() == 3 {
match (s[0..1].parse::<Severity>(),
s[1..2].parse::<Category>(),
s[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => {
Ok(Code {
severity: severity,
category: category,
detail: detail,
})
}
_ => Err(Error::ResponseParsing("Could not parse response code")),
}
} else {
Err(Error::ResponseParsing("Wrong code length (should be 3 digit)"))
}
impl Display for Code {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}{}{}", self.severity, self.category, self.detail)
}
}
impl Code {
/// Creates a new `Code` structure
pub fn new(severity: Severity, category: Category, detail: u8) -> Code {
pub fn new(severity: Severity, category: Category, detail: Detail) -> Code {
Code {
severity: severity,
category: category,
detail: detail,
}
}
/// Returns the reply code
pub fn code(&self) -> String {
format!("{}{}{}", self.severity, self.category, self.detail)
}
}
/// Parses an SMTP response
#[derive(PartialEq,Eq,Clone,Debug,Default)]
pub struct ResponseParser {
/// Response code
code: Option<Code>,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>,
}
impl ResponseParser {
/// Parses a line and return a `bool` indicating if there are more lines to come
pub fn read_line(&mut self, line: &str) -> result::Result<bool, Error> {
if line.len() < 3 {
return Err(Error::ResponseParsing("Wrong code length (should be 3 digit)"));
}
match self.code {
Some(ref code) => {
if code.code() != line[0..3] {
return Err(Error::ResponseParsing("Response code has changed during a \
reponse"));
}
}
None => self.code = Some(try!(line[0..3].parse::<Code>())),
}
if line.len() > 4 {
self.message.push(line[4..].to_string());
Ok(line.as_bytes()[3] == b'-')
} else {
Ok(false)
}
}
/// Builds a response from a `ResponseParser`
pub fn response(self) -> SmtpResult {
match self.code {
Some(code) => Ok(Response::new(code, self.message)),
None => {
Err(Error::ResponseParsing("Incomplete response, could not read response \
code"))
}
severity,
category,
detail,
}
}
}
@@ -199,101 +126,143 @@ impl ResponseParser {
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Response {
/// Response code
code: Code,
pub code: Code,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>,
pub message: Vec<String>,
}
impl FromStr for Response {
type Err = Error;
fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
}
}
impl Response {
/// Creates a new `Response`
pub fn new(code: Code, message: Vec<String>) -> Response {
Response {
code: code,
message: message,
}
Response { code, message }
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.code.severity {
PositiveCompletion => true,
PositiveIntermediate => true,
_ => false,
}
}
/// Returns the message
pub fn message(&self) -> Vec<String> {
self.message.clone()
}
/// Returns the severity (i.e. 1st digit)
pub fn severity(&self) -> Severity {
self.code.severity
}
/// Returns the category (i.e. 2nd digit)
pub fn category(&self) -> Category {
self.code.category
}
/// Returns the detail (i.e. 3rd digit)
pub fn detail(&self) -> u8 {
self.code.detail
}
/// Returns the reply code
fn code(&self) -> String {
self.code.code()
matches!(
self.code.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
}
/// Tests code equality
pub fn has_code(&self, code: u16) -> bool {
self.code() == format!("{}", code)
self.code.to_string() == code.to_string()
}
/// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<String> {
if self.message.is_empty() {
None
} else {
match self.message[0].split_whitespace().next() {
Some(word) => Some(word.to_string()),
None => None,
pub fn first_word(&self) -> Option<&str> {
self.message
.first()
.and_then(|line| line.split_whitespace().next())
}
/// Returns only the line of the message if possible
pub fn first_line(&self) -> Option<&str> {
self.message.first().map(String::as_str)
}
}
// Parsers (originally from tokio-smtp)
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,
},
))
}
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)
}
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)
}
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)
}
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)?;
// Check that all codes are equal.
if !lines.iter().all(|&(code, _, _)| code == last_code) {
return Err(nom::Err::Failure(("", nom::error::ErrorKind::Not)));
}
// Extract text from lines, and append last line.
let mut lines: Vec<String> = lines.into_iter().map(|(_, text, _)| text.into()).collect();
lines.push(last_line.into());
Ok((
i,
Response {
code: last_code,
message: lines,
},
))
}
#[cfg(test)]
mod test {
use super::{Category, Code, Response, ResponseParser, Severity};
#[test]
fn test_severity_from_str() {
assert_eq!("2".parse::<Severity>().unwrap(),
Severity::PositiveCompletion);
assert_eq!("4".parse::<Severity>().unwrap(),
Severity::TransientNegativeCompletion);
assert!("1".parse::<Severity>().is_err());
}
use super::*;
#[test]
fn test_severity_fmt() {
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
}
#[test]
fn test_category_from_str() {
assert_eq!("2".parse::<Category>().unwrap(), Category::Connections);
assert_eq!("4".parse::<Category>().unwrap(), Category::Unspecified4);
assert!("6".parse::<Category>().is_err());
}
#[test]
fn test_category_fmt() {
assert_eq!(format!("{}", Category::Unspecified4), "4");
@@ -301,284 +270,293 @@ mod test {
#[test]
fn test_code_new() {
assert_eq!(Code::new(Severity::TransientNegativeCompletion,
assert_eq!(
Code::new(
Severity::TransientNegativeCompletion,
Category::Connections,
0),
Detail::Zero,
),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 0,
});
detail: Detail::Zero,
}
);
}
#[test]
fn test_code_from_str() {
assert_eq!("421".parse::<Code>().unwrap(),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
});
}
#[test]
fn test_code_code() {
fn test_code_display() {
let code = Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
detail: Detail::One,
};
assert_eq!(code.code(), "421");
assert_eq!(code.to_string(), "421");
}
#[test]
fn test_response_new() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]),
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()],
});
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![]),
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec![],
});
}
#[test]
fn test_response_parser() {
let mut parser = ResponseParser::default();
assert!(parser.read_line("250-me").unwrap());
assert!(parser.read_line("250-8BITMIME").unwrap());
assert!(parser.read_line("250-SIZE 42").unwrap());
assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap());
let response = parser.response().unwrap();
assert_eq!(response,
fn test_response_from_str() {
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
assert_eq!(
raw_response.parse::<Response>().unwrap(),
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: 0,
detail: Detail::Zero,
},
message: vec!["me".to_string(),
message: vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"AUTH PLAIN CRAM-MD5".to_string()],
});
"AUTH PLAIN CRAM-MD5".to_string(),
],
}
);
let wrong_code = "2506-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
assert!(wrong_code.parse::<Response>().is_err());
let wrong_end = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250-AUTH PLAIN CRAM-MD5\r\n";
assert!(wrong_end.parse::<Response>().is_err());
}
#[test]
fn test_response_is_positive() {
assert!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
assert!(Response::new(
Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec!["me".to_string(),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
"SIZE 42".to_string(),
],
)
.is_positive());
assert!(!Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
assert!(!Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec!["me".to_string(),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
"SIZE 42".to_string(),
],
)
.is_positive());
}
#[test]
fn test_response_message() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.message(),
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
let empty_message: Vec<String> = vec![];
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![])
.message(),
empty_message);
}
#[test]
fn test_response_severity() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.severity(),
Severity::PositiveCompletion);
assert_eq!(Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.severity(),
Severity::PermanentNegativeCompletion);
}
#[test]
fn test_response_category() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.category(),
Category::Unspecified4);
}
#[test]
fn test_response_detail() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.detail(),
1);
}
#[test]
fn test_response_code() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.code(),
"241");
}
#[test]
fn test_response_has_code() {
assert!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
assert!(Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec!["me".to_string(),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(241));
assert!(!Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
"SIZE 42".to_string(),
],
)
.has_code(451));
assert!(!Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec!["me".to_string(),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
"SIZE 42".to_string(),
],
)
.has_code(251));
}
#[test]
fn test_response_first_word() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec!["me".to_string(),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
"SIZE 42".to_string(),
],
)
.first_word(),
Some("me".to_string()));
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
Some("me")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec!["me mo".to_string(),
vec![
"me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
"SIZE 42".to_string(),
],
)
.first_word(),
Some("me".to_string()));
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
Some("me")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![])
vec![],
)
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
None
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()])
vec![" ".to_string()],
)
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
None
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()])
vec![" ".to_string()],
)
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
None
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec!["".to_string()])
vec!["".to_string()],
)
.first_word(),
None);
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!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_line(),
Some("me")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_line(),
Some("me mo")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![],
)
.first_line(),
None
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_line(),
Some(" ")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_line(),
Some(" ")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec!["".to_string()],
)
.first_line(),
Some("")
);
}
}

View File

@@ -0,0 +1,227 @@
use std::time::Duration;
#[cfg(feature = "r2d2")]
use r2d2::Pool;
#[cfg(feature = "r2d2")]
use super::PoolConfig;
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::address::Envelope;
use crate::Transport;
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool<SmtpClient>,
#[cfg(not(feature = "r2d2"))]
inner: SmtpClient,
}
impl Transport for SmtpTransport {
type Ok = Response;
type Error = Error;
/// Sends an email
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "r2d2")]
let mut conn = self.inner.get()?;
#[cfg(not(feature = "r2d2"))]
let mut conn = self.inner.connection()?;
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "r2d2"))]
conn.quit()?;
Ok(result)
}
}
impl SmtpTransport {
/// Simple and secure transport, using TLS connections to comunicate with the SMTP server
///
/// The right option for most SMTP servers.
///
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?;
Ok(Self::builder_dangerous(relay)
.port(SUBMISSIONS_PORT)
.tls(Tls::Wrapper(tls_parameters)))
}
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
///
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
/// that don't take SMTPS connections.
///
/// Creates an encrypted transport over submissions port, by first connecting using
/// an unencrypted connection and then upgrading it with STARTTLS. The provided
/// domain is used to validate TLS certificates.
///
/// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?;
Ok(Self::builder_dangerous(relay)
.port(SUBMISSION_PORT)
.tls(Tls::Required(tls_parameters)))
}
/// Creates a new local SMTP client to port 25
///
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
pub fn unencrypted_localhost() -> SmtpTransport {
Self::builder_dangerous("localhost").build()
}
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No authentication
/// * No TLS
/// * A 60 seconds timeout for smtp commands
/// * Port 25
///
/// Consider using [`SmtpTransport::relay`](#method.relay) or
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let mut new = SmtpInfo::default();
new.server = server.into();
SmtpTransportBuilder {
info: new,
#[cfg(feature = "r2d2")]
pool_config: PoolConfig::default(),
}
}
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpTransportBuilder {
info: SmtpInfo,
#[cfg(feature = "r2d2")]
pool_config: PoolConfig,
}
/// 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
}
/// Use a custom configuration for the connection pool
///
/// Defaults can be found at [`PoolConfig`]
#[cfg(feature = "r2d2")]
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
self.pool_config = pool_config;
self
}
/// Build the transport
///
/// If the `r2d2` feature is enabled an `Arc` wrapped pool is be created.
/// Defaults can be found at [`PoolConfig`]
pub fn build(self) -> SmtpTransport {
let client = SmtpClient { info: self.info };
SmtpTransport {
#[cfg(feature = "r2d2")]
inner: self.pool_config.build(client),
#[cfg(not(feature = "r2d2"))]
inner: client,
}
}
}
/// Build client
#[derive(Clone)]
pub struct SmtpClient {
info: SmtpInfo,
}
impl SmtpClient {
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
pub fn connection(&self) -> Result<SmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
};
#[allow(unused_mut)]
let mut conn = SmtpConnection::connect::<(&str, u16)>(
(self.info.server.as_ref(), self.info.port),
self.info.timeout,
&self.info.hello_name,
tls_parameters,
)?;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
match self.info.tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters, &self.info.hello_name)?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters, &self.info.hello_name)?;
}
_ => (),
}
if let Some(credentials) = &self.info.credentials {
conn.auth(&self.info.authentication, &credentials)?;
}
Ok(conn)
}
}

View File

@@ -0,0 +1,48 @@
//! Utils for string manipulation
use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut rest = self.0;
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
let (start, end) = rest.split_at(idx);
f.write_str(start)?;
let mut end_iter = end.char_indices();
let (_, c) = end_iter.next().expect("char");
write!(f, "+{:X}", c as u8)?;
if let Some((idx, _)) = end_iter.next() {
rest = &end[idx..];
} else {
rest = "";
}
}
f.write_str(rest)
}
}
#[cfg(test)]
mod tests {
use super::XText;
#[test]
fn test() {
for (input, expect) in [
("bjorn", "bjorn"),
("bjørn", "bjørn"),
("Ø+= ❤️‰", "Ø+2B+3D+20❤"),
("+", "+2B"),
]
.iter()
{
assert_eq!(format!("{}", XText(input)), expect.to_string());
}
}
}

View File

@@ -1,37 +0,0 @@
//! Error and result type for file transport
use self::Error::*;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(&'static str),
}
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(_) => "an unknown error occured",
}
}
fn cause(&self) -> Option<&StdError> {
None
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
}

View File

@@ -1,28 +1,101 @@
//! This transport is a stub that only logs the message, and always returns
//! success
//! 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, Transport};
//! use lettre::transport::stub::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 email::SendableEmail;
use transport::EmailTransport;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
use async_trait::async_trait;
use std::{error::Error as StdError, fmt};
pub mod error;
#[derive(Debug, Copy, Clone)]
pub struct Error;
/// This transport does nothing except logging the message envelope
pub struct StubEmailTransport;
/// SMTP result type
pub type StubResult = Result<(), error::Error>;
impl EmailTransport<StubResult> for StubEmailTransport {
fn send<T: SendableEmail>(&mut self, email: T) -> StubResult {
info!("{}: from=<{}> to=<{:?}>",
email.message_id(),
email.from_address(),
email.to_addresses());
Ok(())
}
fn close(&mut self) {
()
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("stub error")
}
}
impl StdError for Error {}
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
pub struct StubTransport {
response: Result<(), Error>,
}
impl StubTransport {
/// Creates aResult new transport that always returns the given response
pub fn new(response: Result<(), Error>) -> StubTransport {
StubTransport { response }
}
/// Creates a new transport that always returns a success response
pub fn new_ok() -> StubTransport {
StubTransport { response: Ok(()) }
}
/// Creates a new transport that always returns an error
pub fn new_error() -> StubTransport {
StubTransport {
response: Err(Error),
}
}
}
impl Transport for StubTransport {
type Ok = ();
type Error = Error;
fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.response
}
}
#[cfg(feature = "async-std1")]
#[async_trait]
impl AsyncStd1Transport for StubTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.response
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for StubTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.response
}
}

View File

@@ -1,5 +0,0 @@
extern crate lettre;
mod transport_smtp;
mod transport_stub;
mod transport_file;

View File

@@ -1,37 +1,98 @@
extern crate lettre;
#[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,
};
use std::env::temp_dir;
use std::fs::File;
use std::fs::remove_file;
use std::io::Read;
use lettre::transport::file::FileEmailTransport;
use lettre::transport::EmailTransport;
use lettre::email::{EmailBuilder, SendableEmail};
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
#[test]
fn file_transport() {
let mut sender = FileEmailTransport::new(temp_dir());
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
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.clone());
assert!(result.is_ok());
let message_id = email.message_id();
let file = format!("{}/{}.txt", temp_dir().to_str().unwrap(), message_id);
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,
format!("{}: from=<user@localhost> to=<root@localhost>\n{}",
message_id,
email.message()));
assert_eq!(
buffer,
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
remove_file(file).unwrap();
}
#[cfg(feature = "async-std1")]
#[async_attributes::test]
async fn file_transport_asyncstd1() {
use lettre::AsyncStd1Transport;
let sender = FileTransport::new(temp_dir());
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.unwrap();
let result = sender.send(email).await;
let id = result.unwrap();
let file = temp_dir().join(format!("{}.json", id));
let mut f = File::open(file.clone()).unwrap();
let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer);
assert_eq!(
buffer,
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
remove_file(file).unwrap();
}
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn file_transport_tokio02() {
use lettre::Tokio02Transport;
let sender = FileTransport::new(temp_dir());
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.unwrap();
let result = sender.send(email).await;
let id = result.unwrap();
let file = temp_dir().join(format!("{}.json", id));
let mut f = File::open(file.clone()).unwrap();
let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer);
assert_eq!(
buffer,
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
remove_file(file).unwrap();
}
}

View File

@@ -0,0 +1,63 @@
#[cfg(test)]
#[cfg(feature = "sendmail-transport")]
mod test {
use lettre::{transport::sendmail::SendmailTransport, Message};
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
#[test]
fn sendmail_transport() {
use lettre::Transport;
let sender = SendmailTransport::new();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
let result = sender.send(&email);
println!("{:?}", result);
assert!(result.is_ok());
}
#[cfg(feature = "async-std1")]
#[async_attributes::test]
async fn sendmail_transport_asyncstd1() {
use lettre::AsyncStd1Transport;
let sender = SendmailTransport::new();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.unwrap();
let result = sender.send(email).await;
assert!(result.is_ok());
}
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn sendmail_transport_tokio02() {
use lettre::Tokio02Transport;
let sender = SendmailTransport::new();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.unwrap();
let result = sender.send(email).await;
assert!(result.is_ok());
}
}

View File

@@ -1,19 +1,21 @@
extern crate lettre;
use lettre::transport::smtp::SmtpTransportBuilder;
use lettre::transport::EmailTransport;
use lettre::email::EmailBuilder;
#[cfg(test)]
#[cfg(feature = "smtp-transport")]
mod test {
use lettre::{Message, SmtpTransport, Transport};
#[test]
fn smtp_transport_simple() {
let mut sender = SmtpTransportBuilder::localhost().unwrap().build();
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.unwrap();
SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build()
.send(&email)
.unwrap();
let result = sender.send(email);
assert!(result.is_ok());
}
}

View File

@@ -0,0 +1,60 @@
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
mod test {
use lettre::address::Envelope;
use lettre::{SmtpTransport, Transport};
use std::{sync::mpsc, thread};
fn envelope() -> Envelope {
Envelope::new(
Some("user@localhost".parse().unwrap()),
vec!["root@localhost".parse().unwrap()],
)
.unwrap()
}
#[test]
fn send_one() {
let mailer = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
let result = mailer.send_raw(&envelope(), b"test");
assert!(result.is_ok());
}
#[test]
fn send_from_thread() {
let mailer = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
let (s1, r1) = mpsc::channel();
let (s2, r2) = mpsc::channel();
let mailer1 = mailer.clone();
let t1 = thread::spawn(move || {
s1.send(()).unwrap();
r2.recv().unwrap();
mailer1
.send_raw(&envelope(), b"test1")
.expect("Send failed from thread 1");
});
let mailer2 = mailer.clone();
let t2 = thread::spawn(move || {
s2.send(()).unwrap();
r1.recv().unwrap();
mailer2
.send_raw(&envelope(), b"test2")
.expect("Send failed from thread 2");
});
t1.join().unwrap();
t2.join().unwrap();
mailer
.send_raw(&envelope(), b"test")
.expect("Send failed from main thread");
}
}

View File

@@ -1,19 +1,61 @@
extern crate lettre;
use lettre::{transport::stub::StubTransport, Message};
use lettre::transport::stub::StubEmailTransport;
use lettre::transport::EmailTransport;
use lettre::email::EmailBuilder;
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
#[test]
fn stub_transport() {
let mut sender = StubEmailTransport;
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
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();
let result = sender.send(email);
assert!(result.is_ok());
sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err();
}
#[cfg(feature = "async-std1")]
#[async_attributes::test]
async fn stub_transport_asyncstd1() {
use lettre::AsyncStd1Transport;
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
}
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn stub_transport_tokio02() {
use lettre::Tokio02Transport;
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
}