Compare commits

...

143 Commits

Author SHA1 Message Date
Alexis Mousset
b594945695 Prepare 0.10.0-beta.1 (#555) 2021-02-27 16:54:32 +00:00
Paolo Barbolini
5c83120986 Executor refactor (#545)
* Executor
* Move transports inside the transport module
* AsyncTransport refactor
* Update examples
* Update docs
* impl Default for AsyncSendmailTransport
* Implement AsyncFileTransport::read
* Generalize AsyncFileTransport AsyncTransport implementation
* Remove remaining uses of AsyncSmtpConnector
2021-02-27 16:36:59 +00:00
Alexis Mousset
d4df9a2965 feat(transport): Add SMTPUTF8 handling (#540) 2021-02-27 16:23:48 +00:00
Paolo Barbolini
d2aa959845 Remove deprecated SinglePart methods (#549) 2021-02-19 19:18:38 +01:00
Paolo Barbolini
d1f016e8e2 Fix minimal-version of the mime crate (#548) 2021-02-16 09:50:45 +01:00
konomith
9146212a3e fix(transport-smtp): Fix max_size setter for PoolConfig (#546)
Currently the `max_size` setter method incorrectly assigns the
new value to `self.min_idle` instead of `self.max_size`. This
change fixes the issue.
2021-02-15 21:19:37 +00:00
Alexis Mousset
a04866acfb Improve doc formatting (#539) 2021-02-05 08:39:00 +01:00
Alexis Mousset
be88aabae2 Make ClientCodec private (#541) 2021-02-04 11:11:30 +01:00
Alexis Mousset
6fbb3bf440 feat(transport): Read messages from FileTransport (#516)
* feat(transport): Read messages from FileTransport

* Style improvements
2021-02-03 10:25:47 +01:00
Alexis Mousset
9d8c31bef8 Fix smtp doc examples (#536)
* Fix smtp examples

Make TlsParametersBuilder a consuming builder
as `build()` consumes it. It allows chaining methods.

* Format doc examples
2021-02-03 10:23:08 +01:00
Alexis Mousset
0ea3bfbd13 Remove file and sendmail transport by default (#537)
We can consider the smtp transport as the main
use-case. Let's keep TLS through native-tls
and connection pooling for fast ans secure
defaut feature set.
2021-02-03 10:17:53 +01:00
Alexis Mousset
a0980d017b Make EmailFormat trait private (#535)
It does not need to be exposed.
2021-02-01 10:11:25 +00:00
Paolo Barbolini
40c8a9d000 Better seal AsyncSmtpConnector (#534) 2021-01-31 16:42:37 +00:00
Alexis Mousset
ed50ea74ba Prepare 0.10.0-alpha.5 release 2021-01-28 21:32:24 +01:00
Paolo Barbolini
20d0f8f3ba Add async-std support to smtp transport (#531)
* Add async-std support to smtp transport

* fix Tls import

* fix copy paste

* too many imports

* Temporarely skip async-std native-tls support

* Fix panic message

* TlsParameters: use rustls when async-std is enabled
2021-01-28 11:07:15 +01:00
Anna Clemens
690b143ea3 fix: re-enable unicode-case feature on regex (#532) 2021-01-22 07:18:54 +01:00
Paolo Barbolini
7f384bc983 clippy: fix 1.49 lints (#530) 2021-01-05 21:26:58 +00:00
Paolo Barbolini
d8c4a66206 Update rand to 0.8 (#527) 2021-01-05 22:02:10 +01:00
Paolo Barbolini
54cd221de7 Reduce regex features (#526) 2020-12-26 18:42:52 +00:00
Paolo Barbolini
1a0c344c91 Update to tokio 1.0 (#529) 2020-12-25 21:01:21 +00:00
Paolo Barbolini
15030fde53 Reduce futures-util features (#528) 2020-12-24 17:22:26 +01:00
Paolo Barbolini
89fa5cdb80 sendmail: upgrade to async_std::process from the 1.8.0 release (#520) 2020-12-23 19:33:46 +00:00
Paolo Barbolini
aac5c9929f message: improve docs (#521) 2020-12-23 18:20:00 +00:00
Paolo Barbolini
210133a078 docs: run cargo spellcheck (#524) 2020-12-23 14:04:26 +01:00
Paolo Barbolini
a4c0af9cf1 message: generate a shorter multipart boundary (#523)
Fixes https://tools.ietf.org/tools/msglint/ warning about the
Content-Type header being too long.

I looked at many emails I received over time and I couldn't find
any with a boundary as long as ours, so this isn't only justified
by making some tool happy.
2020-12-22 22:30:09 +01:00
Paolo Barbolini
ad9699827e message: more body improvements (#519) 2020-12-18 14:49:59 +01:00
Paolo Barbolini
f06b8f3823 Return base64 as the Default Content-Transfer-Encoding 2020-12-06 16:23:33 +01:00
Paolo Barbolini
170e929a2b refactor: Message body encoder 2020-12-06 16:23:33 +01:00
Paolo Barbolini
d3c73d8bd7 Bump rustls to 0.19 (#515) 2020-11-25 21:05:10 +01:00
Alexis Mousset
4e13963bf3 add docs 2020-11-25 21:00:03 +01:00
Alexis Mousset
ca6399acbf add docs 2020-11-25 21:00:03 +01:00
Alexis Mousset
3f03b6296b feat(transport): Add the ability to save envelope as JSON in file transport
This will make it possible to read the message and send it later
properly.
2020-11-25 21:00:03 +01:00
RotationMatrix
3683d122ba Update examples/README.md 2020-11-22 18:25:40 +01:00
Federico Guerinoni
0e3526c1bc src/message: Add test for email with png
Signed-off-by: Federico Guerinoni <guerinoni.federico@gmail.com>
2020-11-22 17:45:52 +01:00
Federico Guerinoni
50ac1cdbec src/message: Improve example
Signed-off-by: Federico Guerinoni <guerinoni.federico@gmail.com>
2020-11-22 17:45:52 +01:00
RotationMatrix
227da8ac09 Add HTML examples (#496)
* Add HTML email example

* Add comment about creating `SmtpTransportBuilder`

Add similar comment to `AsyncSmtpTransportBuilder`

* Improve wording and fix typos

* Add file_html example to Cargo.toml

* Update examples/README.md

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Use intra-doc links

* Rename file_html example to basic_html

* Add maud HTML example

* Make CI happy

* Fix CSS

* Fix maud version

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2020-11-22 17:29:09 +01:00
Manuel Pelloni
86763ccefb Changelog (#497)
* Sync changelog with 0.10 changes

* Addess code review
2020-11-14 09:31:02 +01:00
Paolo Barbolini
65d952a64f Remove serde_json from public dependencies (#510) 2020-11-12 15:28:33 +01:00
azul
2e8d43baae feat(transport-file): eml format instead of json (#505)
Change FileTransport to save into .eml format
fixes #499.
2020-11-12 15:14:54 +01:00
Alexis Mousset
648cf4ce5e Prepare 0.10.0-alpha.4 release 2020-11-11 17:18:02 +01:00
Alexis Mousset
4a9d4fbf7e fix(transport-sendmail): Capture sendmail stderr to make possible to debug 2020-11-11 17:14:58 +01:00
Alexis Mousset
a0da9fc2b9 fix(transport-sendmail): Only pass -f option to sendmail when needed 2020-11-11 17:14:58 +01:00
Alexis Mousset
aa31e4fff6 fix(transport-sendmail): Stop argument parsing before destination addresses 2020-11-11 17:14:58 +01:00
Paolo Barbolini
b187885e70 Upgrade to nom 6 2020-11-02 10:00:45 +01:00
Paolo Barbolini
6216fd92c8 Require futures-util ^0.3.7 to avoid RustSec Advisory warning 2020-11-02 10:00:45 +01:00
Manuel Pelloni
0de49c000c Refactor CI configuration and fix building with some combination of features (#494)
* Wip CI refactor, Fix cargo hack test --each-feature

* CI Refactor

* Update .github/workflows/test.yml

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Update .github/workflows/test.yml

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Update .github/workflows/test.yml

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Update .github/workflows/test.yml

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Update .github/workflows/test.yml

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Update .github/workflows/test.yml

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Update .github/workflows/test.yml

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Update src/transport/stub/mod.rs

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Adress code review comment

* Update .github/workflows/test.yml

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Adress review comment

* Add necessary sudo command to install postfix

* Set the action name which setup the cache to Setup cache

* Fix delimiter error

* Fix cargo hack test --each-feature

* Remove blanks before no_run

* Remove useless # before imports in doc tests

* Add builder as required feature for all the examples

* Remove blanks before no_run

* Add builder to the test cfg

* Fix building with tokio03-rustls-tls

* Minor improvements

* Use cargo hack only for check in stable

* Improve chache key

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2020-10-29 21:08:29 +01:00
Paolo Barbolini
b55bcfb6fb Add deps.rs badge (#493) 2020-10-23 11:40:17 +00:00
RotationMatrix
04f064ed5a Add README for examples (#489)
* Add README for examples

* Fix typo in examples/README.md

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Update examples/README.md

Add links to source files and improve wording.

* Fix typos in examples/README.md

* Update examples/README.md

Fix typo.
Add line under title.

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>

* Fix typo in examples/README.md

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2020-10-23 13:31:03 +02:00
Benjamin Beckwith
13b48b656d Replace unwrap in doc examples (#491)
Replace any `unwrap` calls in documentation examples by `?` per
the guidance here:
https://rust-lang.github.io/api-guidelines/documentation.html#examples-use--not-try-not-unwrap-c-question-mark
2020-10-23 08:17:39 +00:00
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
71 changed files with 6496 additions and 2358 deletions

View File

@@ -6,7 +6,7 @@ jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,98 +1,127 @@
name: Continuous integration
name: CI
on: [push, pull_request]
on:
pull_request:
push:
branches:
- master
env:
RUST_BACKTRACE: full
jobs:
test:
name: Test
rustfmt:
name: rustfmt / stable
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
- beta
- 1.40.0
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
- run: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install postfix
- run: smtp-sink 2525 1000&
- uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=native-tls,builder,r2d2,smtp-transport,file-transport,sendmail-transport
- run: rm target/debug/deps/liblettre-*
- uses: actions-rs/cargo@v1
with:
command: test
- run: rm target/debug/deps/liblettre-*
- uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features=builder,smtp-transport,file-transport,sendmail-transport
- run: rm target/debug/deps/liblettre-*
- uses: actions-rs/cargo@v1
with:
command: test
args: --features=async
check:
name: Check
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
- beta
- 1.40.0
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
- uses: actions-rs/cargo@v1
with:
command: check
- name: Checkout
uses: actions/checkout@v2
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Install rust
run: |
rustup update --no-self-update stable
rustup component add rustfmt
- name: cargo fmt
run: cargo fmt --all -- --check
clippy:
name: Clippy
name: clippy / stable
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install rust
run: |
rustup update --no-self-update stable
rustup component add clippy
- name: Run clippy
run: cargo clippy --all-features --all-targets -- -D warnings
check:
name: check / stable
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-check
- name: Install rust
run: rustup update --no-self-update stable
- name: Install cargo hack
run: cargo install cargo-hack --debug
- name: Check with cargo hack
run: cargo hack check --feature-powerset --depth 3
test:
name: test / ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
include:
- name: stable
rust: stable
- name: beta
rust: beta
- name: 1.45.2
rust: 1.45.2
steps:
- uses: actions/checkout@v1
- uses: actions-rs/toolchain@v1
- name: Checkout
uses: actions/checkout@v2
- name: Setup cache
uses: actions/cache@v2
with:
toolchain: ${{ matrix.rust }}
override: true
- uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}
- name: Install rust
run: |
rustup default ${{ matrix.rust }}
rustup update --no-self-update ${{ matrix.rust }}
- name: Install postfix
run: |
DEBIAN_FRONTEND=noninteractive sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix
- name: Run SMTP server
run: smtp-sink 2525 1000&
- name: Test with no default features
run: cargo test --no-default-features
- name: Test with default features
run: cargo test
- name: Test with all features
run: cargo test --all-features
# coverage:
# name: Coverage
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v1
# - uses: actions/checkout@v2
# - uses: actions-rs/toolchain@v1
# with:
# toolchain: nightly

View File

@@ -5,7 +5,8 @@
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::builder`
* MSRV is now 1.45.2
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message`
and make sure to enable the `builder` feature (it's enabled by default).
* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
rename `SendableEmail` to `Email`.
@@ -13,30 +14,46 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
#### Features
* Add `rustls` support ([29e4829](https://github.com/lettre/lettre/commit/29e4829), [39a0686](https://github.com/lettre/lettre/commit/39a0686))
* Allow providing a custom message id ([50d96ad](https://github.com/lettre/lettre/commit/50d96ad))
* Add `EmailAddress::is_valid` and `into_inner` ([e5a1248](https://github.com/lettre/lettre/commit/e5a1248))
* Accept `Into<SendableEmail>` ([86e5181](https://github.com/lettre/lettre/commit/86e5181))
* Allow forcing of a specific auth ([bf2adca](https://github.com/lettre/lettre/commit/bf2adca))
* Add `build_body` ([e927d0b](https://github.com/lettre/lettre/commit/e927d0b))
* Add `tokio` 0.2 and 1.0 support
* Add `rustls` support
* Add `async-std` support. NOTE: native-tls isn't supported when using async-std for the smtp transport.
* Allow enabling multiple SMTP authentication mechanisms
* Allow providing a custom message id
* Allow sending raw emails
#### Changes
#### Breaking Changes
* Merge `lettre_email` into `lettre`
* Merge `Email` and `SendableEmail` into `lettre::message::Email`
* SmtpTransport is now an high level SMTP client. It provides connection pooling and shortcuts for building clients using commonly desired values
* Refactor `TlsParameters` implementation to not expose the internal TLS library
* `FileTransport` writes emails into `.eml` instead of `.json`
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
* The `new` method of `ClientId` is deprecated
* Rename `serde-impls` feature to `serde`
* Move CI to Github Actions ([3eef024](https://github.com/lettre/lettre/commit/3eef024))
* MSRV is now 1.36 ([d227cd4](https://github.com/lettre/lettre/commit/d227cd4))
* Merged `lettre_email` into `lettre` ([0f3f27f](https://github.com/lettre/lettre/commit/0f3f27f))
* Rename `serde-impls` feature to `serde` ([aac3e00](https://github.com/lettre/lettre/commit/aac3e00))
* Use criterion for benchmarks ([eda7fc1](https://github.com/lettre/lettre/commit/eda7fc1))
* Update to nom 5 ([5bc1cba](https://github.com/lettre/lettre/commit/5bc1cba))
* Change website url schemes to https ([6014f5c](https://github.com/lettre/lettre/commit/6014f5c))
* Use serde's `derive` feature instead of the `serde_derive` crate ([4fbe700](https://github.com/lettre/lettre/commit/4fbe700))
* Merge `Email` and `SendableEmail` into `lettre::Email` ([ce37464](https://github.com/lettre/lettre/commit/ce37464))
#### Bug Fixes
* Timeout bug causing infinite hang ([6eff9d3](https://github.com/lettre/lettre/commit/6eff9d3))
* Fix doc tests in website ([947af0a](https://github.com/lettre/lettre/commit/947af0a))
* Fix docs for `domain` field ([0e05e0e](https://github.com/lettre/lettre/commit/0e05e0e))
* Fix argument injection in `SendmailTransport` (see [RUSTSEC-2020-0069](https://github.com/RustSec/advisory-db/blob/master/crates/lettre/RUSTSEC-2020-0069.md))
* Correctly encode header values containing non-ASCII characters
* Timeout bug causing infinite hang
* Fix doc tests in website
* Fix docs for `domain` field
#### Misc
* Improve documentation, examples and tests
* Replace `line-wrap`, `email`, `bufstream` with our own implementations
* Remove `bytes`
* Remove `time`
* Remove `fast_chemail`
* Update `base64` to 0.13
* Update `hostname` to 0.3
* Update to `nom` 6
* Replace `log` with `tracing`
* Move CI to Github Actions
* Use criterion for benchmarks
<a name="v0.9.2"></a>
### v0.9.2 (2019-06-11)

View File

@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.at. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.rs. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

View File

@@ -1,12 +1,13 @@
[package]
name = "lettre"
version = "0.10.0-alpha.0" # remember to update html_root_url
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-beta.1"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.at"
homepage = "https://lettre.rs"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>", "Kayo <kayo@illumium.org>"]
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
categories = ["email", "network-programming"]
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
edition = "2018"
@@ -17,55 +18,138 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
[dependencies]
async-attributes = { version = "1.1", optional = true }
async-std = { version = "1.5", optional = true, features = ["unstable"] }
async-trait = { version = "0.1", optional = true }
base64 = { version = "0.12", optional = true }
bufstream = { version = "0.1", optional = true }
hostname = { version = "0.3", optional = true }
hyperx = { version = "1", optional = true, features = ["headers"] }
idna = "0.2"
line-wrap = "0.1"
log = { version = "0.4", optional = true }
mime = { version = "0.3", optional = true }
native-tls = { version = "0.2", optional = true }
nom = { version = "5", optional = true }
once_cell = "1"
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.4", optional = true }
uuid = { version = "0.8", features = ["v4"] }
rand = { version = "0.8", optional = true }
quoted_printable = { version = "0.4", optional = true }
r2d2 = { version = "0.8", optional = true }
regex = "1"
rustls = { version = "0.17", optional = true }
base64 = { version = "0.13", optional = true }
once_cell = "1"
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
# file transport
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
textnonce = { version = "0.7", optional = true }
uuid = { version = "0.8", features = ["v4"] }
# smtp
nom = { version = "6", default-features = false, features = ["alloc"], optional = true }
r2d2 = { version = "0.8", optional = true } # feature
hostname = { version = "0.3", optional = true } # feature
## tls
native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true }
webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.19", optional = true }
webpki-roots = { version = "0.21", optional = true }
# async
futures-io = { version = "0.3.7", optional = true }
futures-util = { version = "0.3.7", default-features = false, features = ["io"], optional = true }
async-trait = { version = "0.1", optional = true }
## async-std
async-std = { version = "1.8", optional = true, features = ["unstable"] }
#async-native-tls = { version = "0.3.3", optional = true }
async-rustls = { version = "0.2", optional = true }
## tokio
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.15", optional = true }
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "process", "net", "io-util"], optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.22", optional = true }
[dev-dependencies]
criterion = "0.3"
env_logger = "0.7"
tracing-subscriber = "0.2.10"
glob = "0.3"
walkdir = "2"
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1"
maud = "0.22.1"
[[bench]]
harness = false
name = "transport_smtp"
[features]
async = ["async-std", "async-trait", "async-attributes"]
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
default = ["file-transport", "smtp-transport", "rustls-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
file-transport = ["serde", "serde_json"]
rustls-tls = ["webpki", "webpki-roots", "rustls"]
default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"]
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
# transports
file-transport = []
file-transport-envelope = ["serde", "serde_json", "file-transport"]
sendmail-transport = []
smtp-transport = ["bufstream", "base64", "nom"]
unstable = []
smtp-transport = ["base64", "nom"]
rustls-tls = ["webpki", "webpki-roots", "rustls"]
# async
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"]
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"]
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[[example]]
name = "basic_html"
required-features = ["file-transport", "builder"]
[[example]]
name = "maud_html"
required-features = ["file-transport", "builder"]
[[example]]
name = "smtp"
required-features = ["smtp-transport"]
required-features = ["smtp-transport", "builder"]
[[example]]
name = "smtp_gmail"
required-features = ["smtp-transport", "native-tls"]
name = "smtp_tls"
required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "smtp_starttls"
required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "smtp_selfsigned"
required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "tokio02_smtp_tls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]]
name = "tokio02_smtp_starttls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]]
name = "tokio1_smtp_tls"
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
[[example]]
name = "tokio1_smtp_starttls"
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
[[example]]
name = "asyncstd1_smtp_tls"
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
[[example]]
name = "asyncstd1_smtp_starttls"
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]

View File

@@ -1,4 +1,5 @@
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

View File

@@ -21,12 +21,27 @@
<img src="https://badges.gitter.im/lettre/lettre.svg"
alt="chat on gitter" />
</a>
<a href="https://lettre.at">
<a href="https://lettre.rs">
<img src="https://img.shields.io/badge/visit-website-blueviolet"
alt="website" />
</a>
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-beta.1">
<img src="https://deps.rs/crate/lettre/0.10.0-beta.1/status.svg"
alt="dependency status" />
</a>
</div>
---
**NOTE**: this readme refers to the 0.10 version of lettre, which is
still being worked on. The master branch and the alpha releases will see
API breaking changes and some features may be missing or incomplete until
the stable 0.10.0 release is out.
Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x) branch for stable releases.
---
## Features
@@ -37,58 +52,56 @@ 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
Lettre does not provide (for now):
* Async support
* Email parsing
## Example
This library requires Rust 1.20 or newer.
This library requires Rust 1.45 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.9"
lettre_email = "0.9"
lettre = "0.10.0-beta.1"
```
```rust,no_run
use lettre::{EmailTransport, SmtpTransport};
use lettre_email::EmailBuilder;
use std::path::Path;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
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()
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 mut mailer = SmtpTransport::builder_unencrypted_localhost().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
let result = mailer.send(&email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
assert!(result.is_ok());
```
## Testing
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

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

@@ -2,7 +2,9 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::{Message, SmtpTransport, Transport};
fn bench_simple_send(c: &mut Criterion) {
let sender = SmtpTransport::builder("127.0.0.1").port(2525).build();
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
c.bench_function("send email", move |b| {
b.iter(|| {
@@ -11,7 +13,7 @@ fn bench_simple_send(c: &mut Criterion) {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
let result = black_box(sender.send(&email));
assert!(result.is_ok());
@@ -20,7 +22,9 @@ fn bench_simple_send(c: &mut Criterion) {
}
fn bench_reuse_send(c: &mut Criterion) {
let sender = SmtpTransport::builder("127.0.0.1").port(2525).build();
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build();
c.bench_function("send email with connection reuse", move |b| {
b.iter(|| {
let email = Message::builder()
@@ -28,7 +32,7 @@ fn bench_reuse_send(c: &mut Criterion) {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
let result = black_box(sender.send(&email));
assert!(result.is_ok());

BIN
docs/lettre.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

24
examples/README.md Normal file
View File

@@ -0,0 +1,24 @@
# Lettre Examples
This folder contains examples showing how to use lettre in your own projects.
## Message builder examples
- [basic_html.rs] - Create an HTML email.
- [maud_html.rs] - Create an HTML email using a [maud](https://github.com/lambda-fairy/maud) template.
## SMTP Examples
- [smtp.rs] - Send an email using a local SMTP daemon on port 25 as a relay.
- [smtp_tls.rs] - Send an email over SMTP encrypted with TLS and authenticating with username and password.
- [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password.
- [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password.
- The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/).
These files are prefixed with `tokio02_`, `tokio1_` or `asyncstd1_`.
[basic_html.rs]: ./basic_html.rs
[maud_html.rs]: ./maud_html.rs
[smtp.rs]: ./smtp.rs
[smtp_tls.rs]: ./smtp_tls.rs
[smtp_starttls.rs]: ./smtp_starttls.rs
[smtp_selfsigned.rs]: ./smtp_selfsigned.rs

View File

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

View File

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

53
examples/basic_html.rs Normal file
View File

@@ -0,0 +1,53 @@
use lettre::{
message::{header, MultiPart, SinglePart},
FileTransport, Message, Transport,
};
fn main() {
// The html we want to send.
let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello from Lettre!</title>
</head>
<body>
<div style="display: flex; flex-direction: column; align-items: center;">
<h2 style="font-family: Arial, Helvetica, sans-serif;">Hello from Lettre!</h2>
<h4 style="font-family: Arial, Helvetica, sans-serif;">A mailer library for Rust</h4>
</div>
</body>
</html>"#;
// Build the message.
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Hello from Lettre!")
.multipart(
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
)
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.body(String::from(html)),
),
)
.expect("failed to build email");
// Create our mailer. Please see the other examples for creating SMTP mailers.
// The path given here must exist on the filesystem.
let mailer = FileTransport::new("./");
// Store the message when you're ready.
mailer.send(&email).expect("failed to deliver message");
}

62
examples/maud_html.rs Normal file
View File

@@ -0,0 +1,62 @@
use lettre::{
message::{header, MultiPart, SinglePart},
FileTransport, Message, Transport,
};
use maud::html;
fn main() {
// The recipient's name. We might obtain this from a form or their email address.
let recipient = "Hei";
// Create the html we want to send.
let html = html! {
head {
title { "Hello from Lettre!" }
style type="text/css" {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
}
}
div style="display: flex; flex-direction: column; align-items: center;" {
h2 { "Hello from Lettre!" }
// Substitute in the name of our recipient.
p { "Dear " (recipient) "," }
p { "This email was sent with Lettre, a mailer library for Rust!"}
p {
"This example uses "
a href="https://crates.io/crates/maud" { "maud" }
". It is about 20% cooler than the basic HTML example."
}
}
};
// Build the message.
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Hello from Lettre!")
.multipart(
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
)
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.body(html.into_string()),
),
)
.expect("failed to build email");
// Create our mailer. Please see the other examples for creating SMTP mailers.
// The path given here must exist on the filesystem.
let mailer = FileTransport::new("./");
// Store the message when you're ready.
mailer.send(&email).expect("failed to deliver message");
}

View File

@@ -1,25 +1,22 @@
use lettre::{Message, SmtpTransport, Transport};
fn main() {
env_logger::init();
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!")
.body(String::from("Be happy!"))
.unwrap();
// Open a local connection on port 25
let mailer = SmtpTransport::unencrypted_localhost();
// Send the email
let result = mailer.send(&email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
assert!(result.is_ok());
}

View File

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

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(String::from("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),
}
}

View File

@@ -1,18 +1,17 @@
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!")
.body(String::from("Be happy!"))
.unwrap();
let creds = Credentials::new(
"example_username".to_string(),
"example_password".to_string(),
);
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")
@@ -21,13 +20,8 @@ fn main() {
.build();
// Send the email
let result = mailer.send(&email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
match mailer.send(&email) {
Ok(_) => println!("Email sent successfully!"),
Err(e) => panic!("Could not send email: {:?}", e),
}
assert!(result.is_ok());
}

View File

@@ -0,0 +1,37 @@
// 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, Tokio02Executor,
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(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::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,37 @@
// 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, Tokio02Executor,
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(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::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,37 @@
// 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 tokio1_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Executor,
Tokio1Transport,
};
#[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(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::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,37 @@
// 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 tokio1_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Executor,
Tokio1Transport,
};
#[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(String::from("Be happy with async!"))
.unwrap();
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::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,280 +0,0 @@
//! Representation of an email address
use idna::domain_to_ascii;
use once_cell::sync::Lazy;
use regex::Regex;
use std::{
convert::{TryFrom, TryInto},
error::Error,
ffi::OsStr,
fmt::{Display, Formatter, Result as FmtResult},
net::IpAddr,
str::FromStr,
};
/// Email address
///
/// This type contains email in canonical form (_user@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Address {
/// User part
pub user: String,
/// Domain part
pub domain: String,
/// Complete address
complete: String,
}
impl<U, D> TryFrom<(U, D)> for Address
where
U: Into<String>,
D: Into<String>,
{
type Error = AddressError;
fn try_from(from: (U, D)) -> Result<Self, Self::Error> {
let (user, domain) = from;
let user = user.into();
Address::check_user(&user)?;
let domain = domain.into();
Address::check_domain(&domain)?;
let complete = format!("{}@{}", &user, &domain);
Ok(Address {
user,
domain,
complete,
})
}
}
// Regex from the specs
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
// It will mark esoteric email addresses like quoted string as invalid
static USER_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
)
.unwrap()
});
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
impl Address {
/// Create email address from parts
pub fn new<U: Into<String>, D: Into<String>>(user: U, domain: D) -> Result<Self, AddressError> {
(user, domain).try_into()
}
fn check_user(user: &str) -> Result<(), AddressError> {
if USER_RE.is_match(user) {
Ok(())
} else {
Err(AddressError::InvalidUser)
}
}
fn check_domain(domain: &str) -> Result<(), AddressError> {
Address::check_domain_ascii(domain).or_else(|_| {
domain_to_ascii(domain)
.map_err(|_| AddressError::InvalidDomain)
.and_then(|domain| Address::check_domain_ascii(&domain))
})
}
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
if DOMAIN_RE.is_match(domain) {
return Ok(());
}
if let Some(caps) = LITERAL_RE.captures(domain) {
if let Some(cap) = caps.get(1) {
if cap.as_str().parse::<IpAddr>().is_ok() {
return Ok(());
}
}
}
Err(AddressError::InvalidDomain)
}
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
f.write_str(&self.complete)
}
}
impl FromStr for Address {
type Err = AddressError;
fn from_str(val: &str) -> Result<Self, AddressError> {
if val.is_empty() || !val.contains('@') {
return Err(AddressError::MissingParts);
}
let parts: Vec<&str> = val.rsplitn(2, '@').collect();
let user = parts[1];
let domain = parts[0];
Address::check_user(user)
.and_then(|_| Address::check_domain(domain))
.map(|_| Address {
user: user.into(),
domain: domain.into(),
complete: val.to_string(),
})
}
}
impl AsRef<str> for Address {
fn as_ref(&self) -> &str {
&self.complete.as_ref()
}
}
impl AsRef<OsStr> for Address {
fn as_ref(&self) -> &OsStr {
self.complete.as_ref()
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum AddressError {
MissingParts,
Unbalanced,
InvalidUser,
InvalidDomain,
InvalidUtf8b,
}
impl Error for AddressError {}
impl Display for AddressError {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
AddressError::MissingParts => f.write_str("Missing domain or user"),
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
AddressError::InvalidUser => f.write_str("Invalid email user"),
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
AddressError::InvalidUtf8b => f.write_str("Invalid UTF8b data"),
}
}
}
#[cfg(feature = "serde")]
pub mod serde {
use crate::address::Address;
use serde::{
de::{Deserializer, Error as DeError, MapAccess, Visitor},
ser::Serializer,
Deserialize, Serialize,
};
use std::fmt::{Formatter, Result as FmtResult};
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
User,
Domain,
};
const FIELDS: &[&str] = &["user", "domain"];
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("'user' or 'domain'")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: DeError,
{
match value {
"user" => Ok(Field::User),
"domain" => Ok(Field::Domain),
_ => Err(DeError::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct AddressVisitor;
impl<'de> Visitor<'de> for AddressVisitor {
type Value = Address;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
formatter.write_str("email address string or object")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: DeError,
{
s.parse().map_err(DeError::custom)
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let mut user = None;
let mut domain = None;
while let Some(key) = map.next_key()? {
match key {
Field::User => {
if user.is_some() {
return Err(DeError::duplicate_field("user"));
}
let val = map.next_value()?;
Address::check_user(val).map_err(DeError::custom)?;
user = Some(val);
}
Field::Domain => {
if domain.is_some() {
return Err(DeError::duplicate_field("domain"));
}
let val = map.next_value()?;
Address::check_domain(val).map_err(DeError::custom)?;
domain = Some(val);
}
}
}
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
Ok(Address::new(user, domain).unwrap())
}
}
deserializer.deserialize_any(AddressVisitor)
}
}
}

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

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

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

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

112
src/address/serde.rs Normal file
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)
}
}

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

@@ -0,0 +1,257 @@
//! 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;
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// # Ok(())
/// # }
/// ```
///
/// You can also create an `Address` from a string literal by parsing it:
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::from_str("example@email.com")?;
/// # Ok(())
/// # }
/// ```
#[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;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let expected: Address = "example@email.com".parse()?;
/// assert_eq!(expected, address);
/// # Ok(())
/// # }
/// ```
pub fn new<U: AsRef<str>, D: AsRef<str>>(user: U, domain: D) -> Result<Self, AddressError> {
(user, domain).try_into()
}
/// Gets the user portion of the `Address`.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// assert_eq!("example", address.user());
/// # Ok(())
/// # }
/// ```
pub fn user(&self) -> &str {
&self.serialized[..self.at_start]
}
/// Gets the domain portion of the `Address`.
///
/// # Examples
///
/// ```
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// assert_eq!("email.com", address.domain());
/// # Ok(())
/// # }
/// ```
pub fn domain(&self) -> &str {
&self.serialized[self.at_start + 1..]
}
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
if USER_RE.is_match(user) {
Ok(())
} else {
Err(AddressError::InvalidUser)
}
}
pub(super) fn check_domain(domain: &str) -> Result<(), AddressError> {
Address::check_domain_ascii(domain).or_else(|_| {
domain_to_ascii(domain)
.map_err(|_| AddressError::InvalidDomain)
.and_then(|domain| Address::check_domain_ascii(&domain))
})
}
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
if DOMAIN_RE.is_match(domain) {
return Ok(());
}
if let Some(caps) = LITERAL_RE.captures(domain) {
if let Some(cap) = caps.get(1) {
if cap.as_str().parse::<IpAddr>().is_ok() {
return Ok(());
}
}
}
Err(AddressError::InvalidDomain)
}
/// Check if the address contains non-ascii chars
pub(super) fn is_ascii(&self) -> bool {
self.serialized.is_ascii()
}
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str(&self.serialized)
}
}
impl FromStr for Address {
type Err = AddressError;
fn from_str(val: &str) -> Result<Self, AddressError> {
let mut parts = val.rsplitn(2, '@');
let domain = parts.next().ok_or(AddressError::MissingParts)?;
let user = parts.next().ok_or(AddressError::MissingParts)?;
Address::check_user(user)?;
Address::check_domain(domain)?;
Ok(Address {
serialized: val.into(),
at_start: user.len(),
})
}
}
impl 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)]
/// Errors in email addresses parsing
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,3 +1,5 @@
//! Error type for email messages
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
@@ -28,18 +30,18 @@ pub enum Error {
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str(&match self {
Error::MissingFrom => "missing source address, invalid envelope".to_string(),
Error::MissingTo => "missing destination address, invalid envelope".to_string(),
Error::TooManyFrom => "there can only be one source address".to_string(),
Error::EmailMissingAt => "missing @ in email address".to_string(),
Error::EmailMissingLocalPart => "missing local part in email address".to_string(),
Error::EmailMissingDomain => "missing domain in email address".to_string(),
Error::CannotParseFilename => "could not parse attachment filename".to_string(),
Error::NonAsciiChars => "contains non-ASCII chars".to_string(),
Error::Io(e) => e.to_string(),
})
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),
}
}
}

232
src/executor.rs Normal file
View File

@@ -0,0 +1,232 @@
use async_trait::async_trait;
#[cfg(feature = "file-transport")]
use std::io::Result as IoResult;
#[cfg(feature = "file-transport")]
use std::path::Path;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::client::AsyncSmtpConnection;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::client::Tls;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::extension::ClientId;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::Error;
#[async_trait]
pub trait Executor: Send + Sync + private::Sealed {
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error>;
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>>;
#[doc(hidden)]
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
}
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
pub struct Tokio02Executor;
#[async_trait]
#[cfg(feature = "tokio02")]
impl Executor for Tokio02Executor {
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "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)
}
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
tokio02_crate::fs::read(path).await
}
#[doc(hidden)]
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
tokio02_crate::fs::write(path, contents).await
}
}
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub struct Tokio1Executor;
#[async_trait]
#[cfg(feature = "tokio1")]
impl Executor for Tokio1Executor {
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio1(hostname, port, hello_name, tls_parameters).await?;
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
tokio1_crate::fs::read(path).await
}
#[doc(hidden)]
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
tokio1_crate::fs::write(path, contents).await
}
}
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
pub struct AsyncStd1Executor;
#[async_trait]
#[cfg(feature = "async-std1")]
impl Executor for AsyncStd1Executor {
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_asyncstd1(hostname, port, hello_name, tls_parameters)
.await?;
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
}
#[doc(hidden)]
#[cfg(feature = "file-transport-envelope")]
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
async_std::fs::read(path).await
}
#[doc(hidden)]
#[cfg(feature = "file-transport")]
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
async_std::fs::write(path, contents).await
}
}
mod private {
use super::*;
pub trait Sealed {}
#[cfg(feature = "tokio02")]
impl Sealed for Tokio02Executor {}
#[cfg(feature = "tokio1")]
impl Sealed for Tokio1Executor {}
#[cfg(feature = "async-std1")]
impl Sealed for AsyncStd1Executor {}
}

View File

@@ -5,186 +5,124 @@
//! * Unicode support
//! * Secure defaults
//!
//! Lettre requires Rust 1.40 or newer.
//! Lettre requires Rust 1.45 or newer.
//!
//! ## Optional features
//!
//! * **builder**: Message builder
//! * **file-transport**: Transport that write messages into a file
//! * **file-transport-envelope**: Allow writing the envelope into a JSON 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
//! * **tokio1**: Allow to asyncronously send emails using tokio 1.x
//! * **tokio1-rustls-tls**: Async TLS support with the `rustls` crate using tokio 1.x
//! * **tokio1-native-tls**: Async TLS support with the `native-tls` crate using tokio 1.x
//! * **async-std1**: Allow to asynchronously send emails using async-std 1.x
//! * NOTE: native-tls isn't supported with async-std at the moment
//! * **async-std1-rustls-tls**: Async TLS support with the `rustls` crate using async-std 1.x
//! * **r2d2**: Connection pool for SMTP transport
//! * **log**: Logging using the `log` crate
//! * **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/lettre/0.10.0")]
#![doc(html_favicon_url = "https://lettre.at/favicon.png")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-beta.1")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
#![deny(
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
unsafe_code
rust_2018_idioms
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub mod address;
pub mod error;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
mod executor;
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
pub mod message;
pub mod transport;
use crate::error::Error;
#[cfg(feature = "builder")]
pub use crate::message::{
header::{self, Headers},
EmailFormat, Mailbox, Mailboxes, Message,
};
#[macro_use]
extern crate hyperx;
#[cfg(feature = "async-std1")]
pub use self::executor::AsyncStd1Executor;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
pub use self::executor::Executor;
#[cfg(feature = "tokio02")]
pub use self::executor::Tokio02Executor;
#[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor;
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
pub use self::transport::AsyncTransport;
pub use crate::address::Address;
#[cfg(feature = "builder")]
pub use crate::message::Message;
#[cfg(all(
feature = "file-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
pub use crate::transport::file::AsyncFileTransport;
#[cfg(feature = "file-transport")]
pub use crate::transport::file::FileTransport;
#[cfg(all(
feature = "sendmail-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
pub use crate::transport::sendmail::AsyncSendmailTransport;
#[cfg(feature = "sendmail-transport")]
pub use crate::transport::sendmail::SendmailTransport;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio1")
))]
pub use crate::transport::smtp::AsyncSmtpTransport;
pub use crate::transport::Transport;
use crate::{address::Envelope, error::Error};
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
pub use crate::transport::smtp::AsyncStd1Connector;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::client::net::TlsParameters;
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::{SmtpTransport, Tls};
pub use crate::{address::Address, transport::stub::StubTransport};
#[cfg(feature = "builder")]
use std::convert::TryFrom;
use std::{error::Error as StdError, fmt};
/// Simple email envelope representation
///
/// We only accept mailboxes, and do not support source routes (as per RFC).
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Envelope {
/// The envelope recipients' addresses
///
/// This can not be empty.
forward_path: Vec<Address>,
/// The envelope sender address
reverse_path: Option<Address>,
}
impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
if to.is_empty() {
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
reverse_path: from,
})
}
/// Destination addresses of the envelope
pub fn to(&self) -> &[Address] {
self.forward_path.as_slice()
}
/// Source address of the envelope
pub fn from(&self) -> Option<&Address> {
self.reverse_path.as_ref()
}
}
impl TryFrom<&Headers> for Envelope {
type Error = Error;
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
let from = match headers.get::<header::Sender>() {
// If there is a Sender, use it
Some(header::Sender(a)) => Some(a.email.clone()),
// ... else try From
None => match headers.get::<header::From>() {
Some(header::From(a)) => {
let from: Vec<Mailbox> = a.clone().into();
if from.len() > 1 {
return Err(Error::TooManyFrom);
}
Some(from[0].email.clone())
}
None => None,
},
};
fn add_addresses_from_mailboxes(
addresses: &mut Vec<Address>,
mailboxes: Option<&Mailboxes>,
) {
if let Some(mailboxes) = mailboxes {
for mailbox in mailboxes.iter() {
addresses.push(mailbox.email.clone());
}
}
}
let mut to = vec![];
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| &h.0));
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| &h.0));
Self::new(from, to)
}
}
/// Transport method for emails
pub trait Transport {
/// Result types for the transport
type Ok: fmt::Debug;
type Error: StdError;
/// Sends the email
#[cfg(feature = "builder")]
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
self.send_raw(message.envelope(), &raw)
}
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
#[cfg(feature = "async")]
pub mod r#async {
use super::*;
use async_trait::async_trait;
#[async_trait]
pub trait Transport {
/// Result types for the transport
type Ok: fmt::Debug;
type Error: StdError;
/// Sends the email
#[cfg(feature = "builder")]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(
&self,
envelope: &Envelope,
email: &[u8],
) -> Result<Self::Ok, Self::Error>;
}
}
pub use crate::transport::smtp::SmtpTransport;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(all(feature = "smtp-transport", feature = "tokio02"))]
pub use crate::transport::smtp::Tokio02Connector;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
pub use crate::transport::smtp::Tokio1Connector;
#[doc(hidden)]
#[cfg(feature = "async-std1")]
pub use crate::transport::AsyncStd1Transport;
#[doc(hidden)]
#[cfg(feature = "tokio02")]
pub use crate::transport::Tokio02Transport;
#[doc(hidden)]
#[cfg(feature = "tokio1")]
pub use crate::transport::Tokio1Transport;
#[cfg(test)]
#[cfg(feature = "builder")]
mod test {
use super::*;
use crate::message::{header, Mailbox, Mailboxes};
use hyperx::header::Headers;
use std::convert::TryFrom;
#[test]
fn envelope_from_headers() {

581
src/message/body.rs Normal file
View File

@@ -0,0 +1,581 @@
use std::{
io::{self, Write},
ops::Deref,
};
use crate::message::header::ContentTransferEncoding;
/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body that has already been encoded.
#[derive(Debug, Clone)]
pub struct Body {
buf: Vec<u8>,
encoding: ContentTransferEncoding,
}
/// Either a `Vec<u8>` or a `String`.
///
/// If the content is valid utf-8 a `String` should be passed, as it
/// makes for a more efficient `Content-Transfer-Encoding` to be chosen.
#[derive(Debug, Clone)]
pub enum MaybeString {
Binary(Vec<u8>),
String(String),
}
impl Body {
/// Encode the supplied `buf`, making it ready to be sent as a body.
///
/// Takes a `Vec<u8>` or a `String`.
///
/// Automatically chooses the most efficient encoding between
/// `7bit`, `quoted-printable` and `base64`.
///
/// If `buf` is valid utf-8 a `String` should be supplied, as `String`s
/// can be encoded as `7bit` or `quoted-printable`, while `Vec<u8>` always
/// get encoded as `base64`.
pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
let buf: MaybeString = buf.into();
let encoding = buf.encoding();
Self::new_impl(buf.into(), encoding)
}
/// Encode the supplied `buf`, using the provided `encoding`.
///
/// [`Body::new`] is generally the better option.
///
/// Returns an [`Err`] giving back the supplied `buf`, in case the chosen
/// encoding would have resulted into `buf` being encoded
/// into an invalid body.
pub fn new_with_encoding<B: Into<MaybeString>>(
buf: B,
encoding: ContentTransferEncoding,
) -> Result<Self, Vec<u8>> {
let buf: MaybeString = buf.into();
if !buf.is_encoding_ok(encoding) {
return Err(buf.into());
}
Ok(Self::new_impl(buf.into(), encoding))
}
/// Builds a new `Body` using a pre-encoded buffer.
///
/// **Generally not you want.**
///
/// `buf` shouldn't contain non-ascii characters, lines longer than 1000 characters or nul bytes.
#[inline]
pub fn dangerous_pre_encoded(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
Self { buf, encoding }
}
/// Encodes the supplied `buf` using the provided `encoding`
fn new_impl(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
match encoding {
ContentTransferEncoding::SevenBit
| ContentTransferEncoding::EightBit
| ContentTransferEncoding::Binary => Self { buf, encoding },
ContentTransferEncoding::QuotedPrintable => {
let encoded = quoted_printable::encode(buf);
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
}
ContentTransferEncoding::Base64 => {
let base64_len = buf.len() * 4 / 3 + 4;
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
let mut out = Vec::with_capacity(base64_endings_len);
{
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH);
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails");
// modified Write::write_all to work around base64 crate bug
// TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
{
let mut buf: &[u8] = buf.as_ref();
while !buf.is_empty() {
match writer.write(buf) {
Ok(0) => {
// ignore 0 writes
}
Ok(n) => {
buf = &buf[n..];
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
Err(e) => panic!("base64 encoding never fails: {}", e),
}
}
}
}
Self::dangerous_pre_encoded(out, ContentTransferEncoding::Base64)
}
}
}
/// Returns the length of this `Body` in bytes.
#[inline]
pub fn len(&self) -> usize {
self.buf.len()
}
/// Returns `true` if this `Body` has a length of zero, `false` otherwise.
#[inline]
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
/// Returns the `Content-Transfer-Encoding` of this `Body`.
#[inline]
pub fn encoding(&self) -> ContentTransferEncoding {
self.encoding
}
/// Consumes `Body` and returns the inner `Vec<u8>`
#[inline]
pub fn into_vec(self) -> Vec<u8> {
self.buf
}
}
impl MaybeString {
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
///
/// If the `MaybeString` was created from a `String` composed only of US-ASCII
/// characters, with no lines longer than 1000 characters, then 7bit
/// encoding will be used, else quoted-printable will be chosen.
///
/// If the `MaybeString` was instead created from a `Vec<u8>`, base64 encoding is always
/// chosen.
///
/// `8bit` and `binary` encodings are never returned, as they may not be
/// supported by all SMTP servers.
pub fn encoding(&self) -> ContentTransferEncoding {
match &self {
Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit,
// TODO: consider when base64 would be a better option because of output size
Self::String(_) => ContentTransferEncoding::QuotedPrintable,
Self::Binary(_) => ContentTransferEncoding::Base64,
}
}
/// Returns `true` if using `encoding` to encode this `MaybeString`
/// would result into an invalid encoded body.
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool {
match encoding {
ContentTransferEncoding::SevenBit => is_7bit_encoded(&self),
ContentTransferEncoding::EightBit => is_8bit_encoded(&self),
ContentTransferEncoding::Binary
| ContentTransferEncoding::QuotedPrintable
| ContentTransferEncoding::Base64 => true,
}
}
}
/// A trait for something that takes an encoded [`Body`].
///
/// Used by [`MessageBuilder::body`][super::MessageBuilder::body] and
/// [`SinglePartBuilder::body`][super::SinglePartBuilder::body],
/// which can either take something that can be encoded into [`Body`]
/// or a pre-encoded [`Body`].
///
/// If `encoding` is `None` the best encoding between `7bit`, `quoted-printable`
/// and `base64` is chosen based on the input body. **Best option.**
///
/// If `encoding` is `Some` the supplied encoding is used.
/// **NOTE:** if using the specified `encoding` would result into a malformed
/// body, this will panic!
pub trait IntoBody {
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body;
}
impl<T> IntoBody for T
where
T: Into<MaybeString>,
{
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
match encoding {
Some(encoding) => Body::new_with_encoding(self, encoding).expect("invalid encoding"),
None => Body::new(self),
}
}
}
impl IntoBody for Body {
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
let _ = encoding;
self
}
}
impl AsRef<[u8]> for Body {
#[inline]
fn as_ref(&self) -> &[u8] {
self.buf.as_ref()
}
}
impl From<Vec<u8>> for MaybeString {
#[inline]
fn from(b: Vec<u8>) -> Self {
Self::Binary(b)
}
}
impl From<String> for MaybeString {
#[inline]
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<MaybeString> for Vec<u8> {
#[inline]
fn from(s: MaybeString) -> Self {
match s {
MaybeString::Binary(b) => b,
MaybeString::String(s) => s.into(),
}
}
}
impl Deref for MaybeString {
type Target = [u8];
#[inline]
fn deref(&self) -> &Self::Target {
match self {
Self::Binary(b) => b.as_ref(),
Self::String(s) => s.as_ref(),
}
}
}
/// Checks whether it contains only US-ASCII characters,
/// and no lines are longer than 1000 characters including the `\n` character.
///
/// Most efficient content encoding available
fn is_7bit_encoded(buf: &[u8]) -> bool {
buf.is_ascii() && !contains_too_long_lines(buf)
}
/// Checks that no lines are longer than 1000 characters,
/// including the `\n` character.
/// NOTE: 8bit isn't supported by all SMTP servers.
fn is_8bit_encoded(buf: &[u8]) -> bool {
!contains_too_long_lines(buf)
}
/// Checks if there are lines that are longer than 1000 characters,
/// including the `\n` character.
fn contains_too_long_lines(buf: &[u8]) -> bool {
buf.len() > 1000 && buf.split(|&b| b == b'\n').any(|line| line.len() > 999)
}
const LINE_SEPARATOR: &[u8] = b"\r\n";
const LINE_MAX_LENGTH: usize = 78 - LINE_SEPARATOR.len();
/// A `Write`r that inserts a line separator `\r\n` every `max_line_length` bytes.
struct LineWrappingWriter<'a, W> {
writer: &'a mut W,
current_line_length: usize,
max_line_length: usize,
}
impl<'a, W> LineWrappingWriter<'a, W> {
pub fn new(writer: &'a mut W, max_line_length: usize) -> Self {
Self {
writer,
current_line_length: 0,
max_line_length,
}
}
}
impl<'a, W> Write for LineWrappingWriter<'a, W>
where
W: Write,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let remaining_line_len = self.max_line_length - self.current_line_length;
let write_len = std::cmp::min(buf.len(), remaining_line_len);
self.writer.write_all(&buf[..write_len])?;
if remaining_line_len == write_len {
self.writer.write_all(LINE_SEPARATOR)?;
self.current_line_length = 0;
} else {
self.current_line_length += write_len;
}
Ok(write_len)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
#[cfg(test)]
mod test {
use super::{Body, ContentTransferEncoding};
#[test]
fn seven_bit_detect() {
let encoded = Body::new(String::from("Hello, world!"));
assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
assert_eq!(encoded.as_ref(), b"Hello, world!");
}
#[test]
fn seven_bit_encode() {
let encoded = Body::new_with_encoding(
String::from("Hello, world!"),
ContentTransferEncoding::SevenBit,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
assert_eq!(encoded.as_ref(), b"Hello, world!");
}
#[test]
fn seven_bit_too_long_detect() {
let encoded = Body::new("Hello, world!".repeat(100));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(
encoded.as_ref(),
concat!(
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!"
)
.as_bytes()
);
}
#[test]
fn seven_bit_too_long_fail() {
let result = Body::new_with_encoding(
"Hello, world!".repeat(100),
ContentTransferEncoding::SevenBit,
);
assert!(result.is_err());
}
#[test]
fn seven_bit_too_long_encode_quotedprintable() {
let encoded = Body::new_with_encoding(
"Hello, world!".repeat(100),
ContentTransferEncoding::QuotedPrintable,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(
encoded.as_ref(),
concat!(
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
"ello, world!Hello, world!"
)
.as_bytes()
);
}
#[test]
fn seven_bit_invalid() {
let result = Body::new_with_encoding(
String::from("Привет, мир!"),
ContentTransferEncoding::SevenBit,
);
assert!(result.is_err());
}
#[test]
fn eight_bit_encode() {
let encoded = Body::new_with_encoding(
String::from("Привет, мир!"),
ContentTransferEncoding::EightBit,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::EightBit);
assert_eq!(encoded.as_ref(), "Привет, мир!".as_bytes());
}
#[test]
fn eight_bit_too_long_fail() {
let result = Body::new_with_encoding(
"Привет, мир!".repeat(200),
ContentTransferEncoding::EightBit,
);
assert!(result.is_err());
}
#[test]
fn quoted_printable_detect() {
let encoded = Body::new(String::from("Привет, мир!"));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(
encoded.as_ref(),
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
);
}
#[test]
fn quoted_printable_encode_ascii() {
let encoded = Body::new_with_encoding(
String::from("Hello, world!"),
ContentTransferEncoding::QuotedPrintable,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(encoded.as_ref(), b"Hello, world!");
}
#[test]
fn quoted_printable_encode_utf8() {
let encoded = Body::new_with_encoding(
String::from("Привет, мир!"),
ContentTransferEncoding::QuotedPrintable,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(
encoded.as_ref(),
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
);
}
#[test]
fn quoted_printable_encode_line_wrap() {
let encoded = Body::new(String::from("Текст письма в уникоде"));
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
assert_eq!(
encoded.as_ref(),
concat!(
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5"
)
.as_bytes()
);
}
#[test]
fn base64_detect() {
let input = Body::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let encoding = input.encoding();
assert_eq!(encoding, ContentTransferEncoding::Base64);
}
#[test]
fn base64_encode_bytes() {
let encoded = Body::new_with_encoding(
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
ContentTransferEncoding::Base64,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(encoded.as_ref(), b"AAECAwQFBgcICQ==");
}
#[test]
fn base64_encode_bytes_wrapping() {
let encoded = Body::new_with_encoding(
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
ContentTransferEncoding::Base64,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(
encoded.as_ref(),
concat!(
"AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUG\r\n",
"BwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQID\r\n",
"BAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkA\r\n",
"AQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAk="
)
.as_bytes()
);
}
#[test]
fn base64_encode_ascii() {
let encoded = Body::new_with_encoding(
String::from("Hello World!"),
ContentTransferEncoding::Base64,
)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(encoded.as_ref(), b"SGVsbG8gV29ybGQh");
}
#[test]
fn base64_encode_ascii_wrapping() {
let encoded =
Body::new_with_encoding("Hello World!".repeat(20), ContentTransferEncoding::Base64)
.unwrap();
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
assert_eq!(
encoded.as_ref(),
concat!(
"SGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29y\r\n",
"bGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8g\r\n",
"V29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVs\r\n",
"bG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQh\r\n",
"SGVsbG8gV29ybGQh"
)
.as_bytes()
);
}
}

View File

@@ -1,251 +0,0 @@
use crate::message::header::ContentTransferEncoding;
use line_wrap::{crlf, line_wrap, LineEnding};
use std::io::Write;
/// Encoder trait
pub trait EncoderCodec: Send {
/// Encode all data
fn encode(&mut self, input: &[u8]) -> Vec<u8>;
}
/// 7bit codec
///
/// WARNING: Panics when passed non-ascii chars
struct SevenBitCodec {
line_wrapper: EightBitCodec,
}
impl SevenBitCodec {
pub fn new() -> Self {
SevenBitCodec {
line_wrapper: EightBitCodec::new(),
}
}
}
impl EncoderCodec for SevenBitCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
if input.iter().all(u8::is_ascii) {
self.line_wrapper.encode(input)
} else {
panic!("")
}
}
}
/// Quoted-Printable codec
///
struct QuotedPrintableCodec();
impl QuotedPrintableCodec {
pub fn new() -> Self {
QuotedPrintableCodec()
}
}
impl EncoderCodec for QuotedPrintableCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
quoted_printable::encode(input)
}
}
/// Base64 codec
///
struct Base64Codec {
line_wrapper: EightBitCodec,
}
impl Base64Codec {
pub fn new() -> Self {
Base64Codec {
// TODO probably 78, 76 is for qp
line_wrapper: EightBitCodec::new().with_limit(78 - 2),
}
}
}
impl EncoderCodec for Base64Codec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
self.line_wrapper.encode(base64::encode(input).as_bytes())
}
}
/// 8bit codec
///
struct EightBitCodec {
max_length: usize,
}
const DEFAULT_MAX_LINE_LENGTH: usize = 1000 - 2;
impl EightBitCodec {
pub fn new() -> Self {
EightBitCodec {
max_length: DEFAULT_MAX_LINE_LENGTH,
}
}
pub fn with_limit(mut self, max_length: usize) -> Self {
self.max_length = max_length;
self
}
}
impl EncoderCodec for EightBitCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
let ending = &crlf();
let mut out = vec![0_u8; input.len() + input.len() / self.max_length * ending.len()];
let mut writer: &mut [u8] = out.as_mut();
writer.write_all(input).unwrap();
line_wrap(&mut out, input.len(), self.max_length, ending);
out
}
}
/// Binary codec
///
struct BinaryCodec;
impl BinaryCodec {
pub fn new() -> Self {
BinaryCodec
}
}
impl EncoderCodec for BinaryCodec {
fn encode(&mut self, input: &[u8]) -> Vec<u8> {
input.into()
}
}
pub fn codec(encoding: Option<&ContentTransferEncoding>) -> Box<dyn EncoderCodec> {
use self::ContentTransferEncoding::*;
if let Some(encoding) = encoding {
match encoding {
SevenBit => Box::new(SevenBitCodec::new()),
QuotedPrintable => Box::new(QuotedPrintableCodec::new()),
Base64 => Box::new(Base64Codec::new()),
EightBit => Box::new(EightBitCodec::new()),
Binary => Box::new(BinaryCodec::new()),
}
} else {
Box::new(BinaryCodec::new())
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn seven_bit_encode() {
let mut c = SevenBitCodec::new();
assert_eq!(
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
"Hello, world!"
);
}
#[test]
#[should_panic]
fn seven_bit_encode_panic() {
let mut c = SevenBitCodec::new();
c.encode("Hello, мир!".as_bytes());
}
#[test]
fn quoted_printable_encode() {
let mut c = QuotedPrintableCodec::new();
assert_eq!(
&String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!"
);
assert_eq!(&String::from_utf8(c.encode("Текст письма в уникоде".as_bytes())).unwrap(),
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5");
}
#[test]
fn base64_encode() {
let mut c = Base64Codec::new();
assert_eq!(
&String::from_utf8(c.encode("Привет, мир!".as_bytes())).unwrap(),
"0J/RgNC40LLQtdGCLCDQvNC40YAh"
);
assert_eq!(
&String::from_utf8(c.encode("Текст письма в уникоде подлиннее.".as_bytes())).unwrap(),
concat!(
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQ",
"vtC00LUg0L/QvtC00LvQuNC90L3Q\r\ntdC1Lg=="
)
);
assert_eq!(
&String::from_utf8(c.encode(
"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую.".as_bytes()
)).unwrap(),
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRji4=")
);
assert_eq!(
&String::from_utf8(c.encode(
"Ну прямо супер-длинный текст письма в уникоде, который уж точно ну никак не поместиться в 78 байт, как ни крути, я гарантирую это.".as_bytes()
)).unwrap(),
concat!("0J3RgyDQv9GA0Y/QvNC+INGB0YPQv9C10YAt0LTQu9C40L3QvdGL0Lkg0YLQtdC60YHRgiDQv9C4\r\n",
"0YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LUsINC60L7RgtC+0YDRi9C5INGD0LYg0YLQvtGH0L3Q\r\n",
"viDQvdGDINC90LjQutCw0Log0L3QtSDQv9C+0LzQtdGB0YLQuNGC0YzRgdGPINCyIDc4INCx0LDQ\r\n",
"udGCLCDQutCw0Log0L3QuCDQutGA0YPRgtC4LCDRjyDQs9Cw0YDQsNC90YLQuNGA0YPRjiDRjdGC\r\n",
"0L4u")
);
}
#[test]
fn base64_encodeed() {
let mut c = Base64Codec::new();
assert_eq!(
&String::from_utf8(c.encode("Chunk.".as_bytes())).unwrap(),
"Q2h1bmsu"
);
}
#[test]
fn eight_bit_encode() {
let mut c = EightBitCodec::new();
assert_eq!(
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
"Hello, world!"
);
assert_eq!(
&String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
"Hello, мир!"
);
}
#[test]
fn binary_encode() {
let mut c = BinaryCodec::new();
assert_eq!(
&String::from_utf8(c.encode("Hello, world!".as_bytes())).unwrap(),
"Hello, world!"
);
assert_eq!(
&String::from_utf8(c.encode("Hello, мир!".as_bytes())).unwrap(),
"Hello, мир!"
);
}
}

View File

@@ -7,6 +7,16 @@ use std::{
str::{from_utf8, FromStr},
};
header! {
/// `Content-Id` header, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
(ContentId, "Content-ID") => [String]
}
/// `Content-Transfer-Encoding` of the body
///
/// The `Message` builder takes care of choosing the most
/// efficient encoding based on the chosen body, so in most
/// use-caches this header shouldn't be set manually.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ContentTransferEncoding {
SevenBit,
@@ -19,19 +29,18 @@ pub enum ContentTransferEncoding {
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::SevenBit
ContentTransferEncoding::Base64
}
}
impl Display for ContentTransferEncoding {
fn fmt(&self, f: &mut FmtFormatter) -> FmtResult {
use self::ContentTransferEncoding::*;
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
f.write_str(match *self {
SevenBit => "7bit",
QuotedPrintable => "quoted-printable",
Base64 => "base64",
EightBit => "8bit",
Binary => "binary",
Self::SevenBit => "7bit",
Self::QuotedPrintable => "quoted-printable",
Self::Base64 => "base64",
Self::EightBit => "8bit",
Self::Binary => "binary",
})
}
}
@@ -39,13 +48,12 @@ impl Display for ContentTransferEncoding {
impl FromStr for ContentTransferEncoding {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::ContentTransferEncoding::*;
match s {
"7bit" => Ok(SevenBit),
"quoted-printable" => Ok(QuotedPrintable),
"base64" => Ok(Base64),
"8bit" => Ok(EightBit),
"binary" => Ok(Binary),
"7bit" => Ok(Self::SevenBit),
"quoted-printable" => Ok(Self::QuotedPrintable),
"base64" => Ok(Self::Base64),
"8bit" => Ok(Self::EightBit),
"binary" => Ok(Self::Binary),
_ => Err(s.into()),
}
}
@@ -71,7 +79,7 @@ impl Header for ContentTransferEncoding {
})
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}", self))
}
}

View File

@@ -35,7 +35,7 @@ macro_rules! mailbox_header {
}).map($type_name)
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&self.0.recode_name(utf8_b::encode))
}
}
@@ -70,7 +70,7 @@ macro_rules! mailboxes_header {
.map($type_name)
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
format_mailboxes(self.0.iter(), f)
}
}
@@ -82,7 +82,7 @@ mailbox_header! {
`Sender` header
This header contains [`Mailbox`](::Mailbox) associated with sender.
This header contains [`Mailbox`][self::Mailbox] associated with sender.
```no_test
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
@@ -96,7 +96,7 @@ mailboxes_header! {
`From` header
This header contains [`Mailboxes`](::Mailboxes).
This header contains [`Mailboxes`][self::Mailboxes].
*/
(From, "From")
@@ -107,7 +107,7 @@ mailboxes_header! {
`Reply-To` header
This header contains [`Mailboxes`](::Mailboxes).
This header contains [`Mailboxes`][self::Mailboxes].
*/
(ReplyTo, "Reply-To")
@@ -118,7 +118,7 @@ mailboxes_header! {
`To` header
This header contains [`Mailboxes`](::Mailboxes).
This header contains [`Mailboxes`][self::Mailboxes].
*/
(To, "To")
@@ -129,7 +129,7 @@ mailboxes_header! {
`Cc` header
This header contains [`Mailboxes`](::Mailboxes).
This header contains [`Mailboxes`][self::Mailboxes].
*/
(Cc, "Cc")
@@ -140,7 +140,7 @@ mailboxes_header! {
`Bcc` header
This header contains [`Mailboxes`](::Mailboxes).
This header contains [`Mailboxes`][self::Mailboxes].
*/
(Bcc, "Bcc")
@@ -155,7 +155,7 @@ fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
Err(HeaderError::Header)
}
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter) -> FmtResult {
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<_>>(),

View File

@@ -1,8 +1,4 @@
/*!
## Headers widely used in email messages
*/
//! Headers widely used in email messages
mod content;
mod mailbox;

View File

@@ -5,6 +5,7 @@ use hyperx::{
use std::{fmt::Result as FmtResult, str::from_utf8};
#[derive(Debug, Clone, Copy, PartialEq)]
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
pub struct MimeVersion {
pub major: u8,
pub minor: u8,
@@ -35,20 +36,17 @@ impl Header for MimeVersion {
Self: Sized,
{
raw.one().ok_or(HeaderError::Header).and_then(|r| {
let s: Vec<&str> = from_utf8(r)
.map_err(|_| HeaderError::Header)?
.split('.')
.collect();
if s.len() != 2 {
return Err(HeaderError::Header);
}
let major = s[0].parse().map_err(|_| HeaderError::Header)?;
let minor = s[1].parse().map_err(|_| HeaderError::Header)?;
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 {
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}.{}", self.major, self.minor))
}
}

View File

@@ -6,8 +6,9 @@ use hyperx::{
use std::{fmt::Result as FmtResult, str::from_utf8};
macro_rules! text_header {
( $type_name: ident, $header_name: expr ) => {
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
#[derive(Debug, Clone, PartialEq)]
$(#[$attr])*
pub struct $type_name(pub String);
impl Header for $type_name {
@@ -26,20 +27,48 @@ macro_rules! text_header {
.map($type_name)
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> FmtResult {
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");
text_header!(
/// `Subject` of the message, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
Header(Subject, "Subject")
);
text_header!(
/// `Comments` of the message, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
Header(Comments, "Comments")
);
text_header!(
/// `Keywords` header. Should contain a comma-separated list of one or more
/// words or quoted-strings, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
Header(Keywords, "Keywords")
);
text_header!(
/// `In-Reply-To` header. Contains one or more
/// unique message identifiers,
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
Header(InReplyTo, "In-Reply-To")
);
text_header!(
/// `References` header. Contains one or more
/// unique message identifiers,
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
Header(References, "References")
);
text_header!(
/// `Message-Id` header. Contains a unique message identifier,
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
Header(MessageId, "Message-Id")
);
text_header!(
/// `User-Agent` header. Contains information about the client,
/// defined in [draft-melnikov-email-user-agent-00](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00#section-3)
Header(UserAgent, "User-Agent")
);
fn parse_text(raw: &[u8]) -> HyperResult<String> {
if let Ok(src) = from_utf8(raw) {
@@ -50,7 +79,7 @@ fn parse_text(raw: &[u8]) -> HyperResult<String> {
Err(HeaderError::Header)
}
fn fmt_text(s: &str, f: &mut HeaderFormatter) -> FmtResult {
fn fmt_text(s: &str, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&utf8_b::encode(s))
}

View File

@@ -23,7 +23,7 @@ impl<'de> Deserialize<'de> for Mailbox {
enum Field {
Name,
Email,
};
}
const FIELDS: &[&str] = &["name", "email"];
@@ -37,7 +37,7 @@ impl<'de> Deserialize<'de> for Mailbox {
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("'name' or 'email'")
}
@@ -62,7 +62,7 @@ impl<'de> Deserialize<'de> for Mailbox {
impl<'de> Visitor<'de> for MailboxVisitor {
type Value = Mailbox;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("mailbox string or object")
}
@@ -123,7 +123,7 @@ impl<'de> Deserialize<'de> for Mailboxes {
impl<'de> Visitor<'de> for MailboxesVisitor {
type Value = Mailboxes;
fn expecting(&self, formatter: &mut Formatter) -> FmtResult {
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
formatter.write_str("mailboxes string or sequence")
}

View File

@@ -9,22 +9,60 @@ use std::{
str::FromStr,
};
/// Email address with optional addressee name
/// Represents an email address with an optional name for the sender/recipient.
///
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
///
/// **NOTE**: Enable feature "serde" to be able serialize/deserialize it using [serde](https://serde.rs/).
///
/// # Examples
///
/// You can create a `Mailbox` from a string and an [`Address`]:
///
/// ```
/// # use lettre::{Address, message::Mailbox};
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let mailbox = Mailbox::new(None, address);
/// # Ok(())
/// # }
/// ```
///
/// You can also create one from a string literal:
///
/// ```
/// # use lettre::message::Mailbox;
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let mailbox: Mailbox = "John Smith <example@email.com>".parse()?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailbox {
/// User name part
/// The name associated with the address.
pub name: Option<String>,
/// Email address part
/// The email address itself.
pub email: Address,
}
impl Mailbox {
/// Create new mailbox using email address and addressee name
/// Creates a new `Mailbox` using an email address and the name of the recipient if there is one.
///
/// # Examples
///
/// ```
/// use lettre::{message::Mailbox, Address};
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let mailbox = Mailbox::new(None, address);
/// # Ok(())
/// # }
/// ```
pub fn new(name: Option<String>, email: Address) -> Self {
Mailbox { name, email }
}
@@ -39,7 +77,7 @@ impl Mailbox {
}
impl Display for Mailbox {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(ref name) = self.name {
let name = name.trim();
if !name.is_empty() {
@@ -99,7 +137,7 @@ impl FromStr for Mailbox {
}
}
/// List or email mailboxes
/// Represents a sequence of [`Mailbox`] instances.
///
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
///
@@ -108,29 +146,119 @@ impl FromStr for Mailbox {
pub struct Mailboxes(Vec<Mailbox>);
impl Mailboxes {
/// Create mailboxes list
/// Creates a new list of [`Mailbox`] instances.
///
/// # Examples
///
/// ```
/// use lettre::message::Mailboxes;
/// let mailboxes = Mailboxes::new();
/// ```
pub fn new() -> Self {
Mailboxes(Vec::new())
}
/// Add mailbox to a list
/// Adds a new [`Mailbox`] to the list, in a builder style pattern.
///
/// # Examples
///
/// ```
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let mut mailboxes = Mailboxes::new().with(Mailbox::new(None, address));
/// # Ok(())
/// # }
/// ```
pub fn with(mut self, mbox: Mailbox) -> Self {
self.0.push(mbox);
self
}
/// Add mailbox to a list
/// Adds a new [`Mailbox`] to the list, in a Vec::push style pattern.
///
/// # Examples
///
/// ```
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let mut mailboxes = Mailboxes::new();
/// mailboxes.push(Mailbox::new(None, address));
/// # Ok(())
/// # }
/// ```
pub fn push(&mut self, mbox: Mailbox) {
self.0.push(mbox);
}
/// Extract first mailbox
/// Extracts the first [`Mailbox`] if it exists.
///
/// # Examples
///
/// ```
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let empty = Mailboxes::new();
/// assert!(empty.into_single().is_none());
///
/// let mut mailboxes = Mailboxes::new();
/// let address = Address::new("example", "email.com")?;
///
/// mailboxes.push(Mailbox::new(None, address));
/// assert!(mailboxes.into_single().is_some());
/// # Ok(())
/// # }
/// ```
pub fn into_single(self) -> Option<Mailbox> {
self.into()
}
/// Iterate over mailboxes
pub fn iter(&self) -> Iter<Mailbox> {
/// Creates an iterator over the [`Mailbox`] instances that are currently stored.
///
/// # Examples
///
/// ```
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let mut mailboxes = Mailboxes::new();
///
/// let address = Address::new("example", "email.com")?;
/// mailboxes.push(Mailbox::new(None, address));
///
/// let address = Address::new("example", "email.com")?;
/// mailboxes.push(Mailbox::new(None, address));
///
/// let mut iter = mailboxes.iter();
///
/// assert!(iter.next().is_some());
/// assert!(iter.next().is_some());
///
/// assert!(iter.next().is_none());
/// # Ok(())
/// # }
/// ```
pub fn iter(&self) -> Iter<'_, Mailbox> {
self.0.iter()
}
}
@@ -142,26 +270,26 @@ impl Default for Mailboxes {
}
impl From<Mailbox> for Mailboxes {
fn from(single: Mailbox) -> Self {
Mailboxes(vec![single])
fn from(mailbox: Mailbox) -> Self {
Mailboxes(vec![mailbox])
}
}
impl Into<Option<Mailbox>> for Mailboxes {
fn into(self) -> Option<Mailbox> {
self.into_iter().next()
impl From<Mailboxes> for Option<Mailbox> {
fn from(mailboxes: Mailboxes) -> Option<Mailbox> {
mailboxes.into_iter().next()
}
}
impl From<Vec<Mailbox>> for Mailboxes {
fn from(list: Vec<Mailbox>) -> Self {
Mailboxes(list)
fn from(vec: Vec<Mailbox>) -> Self {
Mailboxes(vec)
}
}
impl Into<Vec<Mailbox>> for Mailboxes {
fn into(self) -> Vec<Mailbox> {
self.0
impl From<Mailboxes> for Vec<Mailbox> {
fn from(mailboxes: Mailboxes) -> Vec<Mailbox> {
mailboxes.0
}
}
@@ -183,7 +311,7 @@ impl Extend<Mailbox> for Mailboxes {
}
impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut iter = self.iter();
if let Some(mbox) = iter.next() {

View File

@@ -1,21 +1,17 @@
use crate::message::{
encoder::codec,
header::{ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat,
EmailFormat, IntoBody,
};
use mime::Mime;
use textnonce::TextNonce;
use rand::Rng;
/// MIME part variants
///
#[derive(Debug, Clone)]
pub enum Part {
/// Single part with content
///
Single(SinglePart),
/// Multiple parts of content
///
Multi(MultiPart),
}
@@ -38,11 +34,9 @@ impl Part {
}
/// Parts of multipart body
///
pub type Parts = Vec<Part>;
/// Creates builder for single part
///
#[derive(Debug, Clone)]
pub struct SinglePartBuilder {
headers: Headers,
@@ -69,10 +63,15 @@ impl SinglePartBuilder {
}
/// Build singlepart using body
pub fn body<T: Into<Vec<u8>>>(self, body: T) -> SinglePart {
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding());
SinglePart {
headers: self.headers,
body: body.into(),
body: body.into_vec(),
}
}
}
@@ -88,14 +87,16 @@ impl Default for SinglePartBuilder {
/// # Example
///
/// ```
/// use lettre::message::{SinglePart, header};
/// use lettre::message::{header, SinglePart};
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let part = SinglePart::builder()
/// .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
/// .header(header::ContentTransferEncoding::Binary)
/// .body("Текст письма в уникоде");
/// .header(header::ContentType("text/plain; charset=utf8".parse()?))
/// .body(String::from("Текст письма в уникоде"));
/// # Ok(())
/// # }
/// ```
///
#[derive(Debug, Clone)]
pub struct SinglePart {
headers: Headers,
@@ -103,57 +104,25 @@ pub struct SinglePart {
}
impl SinglePart {
/// Creates a default builder for singlepart
/// Creates a builder for singlepart
#[inline]
pub fn builder() -> SinglePartBuilder {
SinglePartBuilder::new()
}
/// Creates a singlepart builder with 7bit encoding
///
/// Shortcut for `SinglePart::builder().header(ContentTransferEncoding::SevenBit)`.
pub fn seven_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::SevenBit)
}
/// 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
#[inline]
pub fn headers(&self) -> &Headers {
&self.headers
}
/// Read the body from singlepart
pub fn body_ref(&self) -> &[u8] {
/// Get the encoded body
#[inline]
pub fn raw_body(&self) -> &[u8] {
&self.body
}
/// Get message content formatted for SMTP
/// Get message content formatted for sending
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
@@ -165,18 +134,13 @@ 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(&self.body);
out.extend_from_slice(b"\r\n");
}
}
/// The kind of multipart
///
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts
///
@@ -192,23 +156,44 @@ pub enum MultiPartKind {
///
/// 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(40)
.map(char::from)
.collect()
}
impl MultiPartKind {
fn to_mime<S: AsRef<str>>(self, boundary: Option<S>) -> Mime {
let boundary = boundary
.map(|s| s.as_ref().into())
.unwrap_or_else(|| TextNonce::sized(68).unwrap().into_string());
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=\"{}\"",
"multipart/{}; boundary=\"{}\"{}",
match self {
Mixed => "mixed",
Alternative => "alternative",
Related => "related",
Encrypted { .. } => "encrypted",
Signed { .. } => "signed",
},
boundary
boundary,
match self {
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
Signed { protocol, micalg } =>
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
_ => String::new(),
}
)
.parse()
.unwrap()
@@ -220,6 +205,15 @@ impl MultiPartKind {
"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,
}
}
@@ -232,7 +226,6 @@ impl From<MultiPartKind> for Mime {
}
/// Multipart builder
///
#[derive(Debug, Clone)]
pub struct MultiPartBuilder {
headers: Headers,
@@ -298,7 +291,6 @@ impl Default for MultiPartBuilder {
}
/// Multipart variant with parts
///
#[derive(Debug, Clone)]
pub struct MultiPart {
headers: Headers,
@@ -332,6 +324,20 @@ impl MultiPart {
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);
@@ -492,7 +498,7 @@ mod test {
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"example.c".as_bytes().into(),
"example.c".into(),
)],
})
.header(header::ContentTransferEncoding::Binary)
@@ -516,6 +522,126 @@ mod test {
"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",
"Content-Transfer-Encoding: 7bit\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",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
"...\r\n",
"-----END PGP MESSAGE-----\r\n",
"\r\n",
"--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",
"Content-Transfer-Encoding: 7bit\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",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
"=3FYZ\r\n",
"-----END PGP SIGNATURE-----\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
}
#[test]
fn multi_part_alternative() {
@@ -566,7 +692,7 @@ mod test {
.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())]
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; }")));
@@ -601,4 +727,20 @@ mod test {
"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!(40, boundary.len());
}
}
}

View File

@@ -1,69 +1,98 @@
//! Provides a strongly typed way to build emails
//!
//! ### Creating messages
//!
//! This section explains how to create emails.
//!
//! ## Usage
//!
//! ### Format email messages
//! This section demonstrates how to build messages.
//!
//! #### With string body
//! <!--
//! style for <details><summary>Blablabla</summary> Lots of stuff</details>
//! borrowed from https://docs.rs/time/0.2.23/src/time/lib.rs.html#49-54
//! -->
//! <style>
//! summary, details:not([open]) { cursor: pointer; }
//! summary { display: list-item; }
//! summary::marker { content: '▶ '; }
//! details[open] summary::marker { content: '▼ '; }
//! </style>
//!
//! The easiest way how we can create email message with simple string.
//!
//! ### Plain body
//!
//! The easiest way of creating a message, which uses a plain text body.
//!
//! ```rust
//! # extern crate lettre;
//! use lettre::message::Message;
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//! .body(String::from("Be happy!"))?;
//! # Ok(())
//! # }
//! ```
//!
//! Will produce:
//! Which produces:
//! <details>
//! <summary>Click to expand</summary>
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
//! Content-Transfer-Encoding: 7bit
//!
//! Be happy!
//! ```
//! </details>
//! <br />
//!
//! The unicode header data will be encoded using _UTF8-Base64_ encoding.
//! The unicode header data is encoded using _UTF8-Base64_ encoding, when necessary.
//!
//! ### With MIME body
//! The `Content-Transfer-Encoding` is chosen based on the best encoding
//! available for the given body, between `7bit`, `quoted-printable` and `base64`.
//!
//! ##### Single part
//! ### Plain and HTML body
//!
//! The more complex way is using MIME contents.
//! Uses a MIME body to include both plain text and HTML versions of the body.
//!
//! ```rust
//! # extern crate lettre;
//! use lettre::message::{header, Message, SinglePart, Part};
//! # use std::error::Error;
//! use lettre::message::{header, Message, MultiPart, Part, SinglePart};
//!
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/plain; charset=utf8".parse().unwrap(),
//! )).header(header::ContentTransferEncoding::QuotedPrintable)
//! .body("Привет, мир!"),
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body(String::from("Hello, world! :)")),
//! )
//! .unwrap();
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>",
//! )),
//! ),
//! )?;
//! # Ok(())
//! # }
//! ```
//!
//! The body will be encoded using selected `Content-Transfer-Encoding`.
//! Which produces:
//! <details>
//! <summary>Click to expand</summary>
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
@@ -71,134 +100,170 @@
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! MIME-Version: 1.0
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
//! Content-Type: multipart/alternative; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
//!
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
//! Content-Type: text/plain; charset=utf8
//! Content-Transfer-Encoding: quoted-printable
//! Content-Transfer-Encoding: 7bit
//!
//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
//! Hello, world! :)
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
//! Content-Type: text/html; charset=utf8
//! Content-Transfer-Encoding: 7bit
//!
//! <p><b>Hello</b>, <i>world</i>! <img src="cid:123"></p>
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
//! ```
//! </details>
//!
//! ##### Multiple parts
//! ### Complex MIME body
//!
//! And more advanced way of building message by using multipart MIME contents.
//! This example shows how to include both plain and HTML versions of the body,
//! attachments and inlined images.
//!
//! ```rust
//! # extern crate lettre;
//! use lettre::message::{header, Message, MultiPart, SinglePart, Part};
//! # use std::error::Error;
//! use lettre::message::{header, Body, Message, MultiPart, Part, SinglePart};
//! use std::fs;
//!
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let image = fs::read("docs/lettre.png")?;
//! // this image_body can be cloned and reused between emails.
//! // since `Body` holds a pre-encoded body, reusing it means avoiding having
//! // to re-encode the same body for every email (this clearly only applies
//! // when sending multiple emails with the same attachment).
//! let image_body = Body::new(image);
//!
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .multipart(
//! MultiPart::mixed()
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::quoted_printable()
//! .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
//! .body("Привет, мир!")
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body(String::from("Hello, world! :)")),
//! )
//! .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::builder()
//! .header(header::ContentType(
//! "text/html; charset=utf8".parse()?,
//! ))
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
//! )),
//! )
//! .singlepart(
//! SinglePart::base64()
//! .header(header::ContentType("image/png".parse().unwrap()))
//! SinglePart::builder()
//! .header(header::ContentType("image/png".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
//! })
//! .body("<smile-raw-image-data>")
//! )
//! )
//! .header(header::ContentId("<123>".into()))
//! .body(image_body),
//! ),
//! ),
//! )
//! .singlepart(
//! SinglePart::seven_bit()
//! .header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Attachment,
//! parameters: vec![
//! header::DispositionParam::Filename(
//! parameters: vec![header::DispositionParam::Filename(
//! header::Charset::Ext("utf-8".into()),
//! None, "example.c".as_bytes().into()
//! )
//! ]
//! None,
//! "example.rs".as_bytes().into(),
//! )],
//! })
//! .body("int main() { return 0; }")
//! )
//! ).unwrap();
//! .body(String::from("fn main() { println!(\"Hello, World!\") }")),
//! ),
//! )?;
//! # Ok(())
//! # }
//! ```
//!
//! Which produces:
//! <details>
//! <summary>Click to expand</summary>
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! MIME-Version: 1.0
//! Content-Type: multipart/mixed; boundary="RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m"
//! Date: Sat, 12 Dec 2020 16:30:45 GMT
//! Content-Type: multipart/mixed; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
//!
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
//! Content-Type: multipart/alternative; boundary="qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy"
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
//! Content-Type: multipart/alternative; boundary="EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk"
//!
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
//! Content-Transfer-Encoding: quoted-printable
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
//! Content-Type: text/plain; charset=utf8
//! Content-Transfer-Encoding: 7bit
//!
//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
//! Content-Type: multipart/related; boundary="BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8"
//! Hello, world! :)
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
//! Content-Type: multipart/related; boundary="eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr"
//!
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
//! Content-Transfer-Encoding: 8bit
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
//! Content-Type: text/html; charset=utf8
//! Content-Transfer-Encoding: 7bit
//!
//! <p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
//! Content-Transfer-Encoding: base64
//! <p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
//! Content-Type: image/png
//! Content-Disposition: inline
//! Content-ID: <123>
//! Content-Transfer-Encoding: base64
//!
//! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg==
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8--
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy--
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
//! Content-Transfer-Encoding: 7bit
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr--
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk--
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
//! Content-Type: text/plain; charset=utf8
//! Content-Disposition: attachment; filename="example.c"
//!
//! int main() { return 0; }
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m--
//! Content-Disposition: attachment; filename="example.rs"
//! Content-Transfer-Encoding: 7bit
//!
//! fn main() { println!("Hello, World!") }
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
//! ```
//! </details>
pub use encoder::*;
pub use body::{Body, IntoBody, MaybeString};
pub use mailbox::*;
pub use mimebody::*;
pub use mime;
mod encoder;
mod body;
pub mod header;
mod mailbox;
mod mimebody;
mod utf8_b;
use crate::{
message::header::{EmailDate, Header, Headers, MailboxesHeader},
Envelope, Error as EmailError,
};
use std::{convert::TryFrom, time::SystemTime};
use uuid::Uuid;
use crate::{
address::Envelope,
message::header::{ContentTransferEncoding, EmailDate, Header, Headers, MailboxesHeader},
Error as EmailError,
};
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
pub trait EmailFormat {
/// Something that can be formatted as an email message
trait EmailFormat {
// Use a writer?
fn format(&self, out: &mut Vec<u8>);
}
@@ -267,7 +332,7 @@ impl MessageBuilder {
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.header(header::Sender(mbox))`.
pub fn sender(self, mbox: Mailbox) -> Self {
@@ -276,7 +341,7 @@ impl MessageBuilder {
/// Set or add mailbox to `From` header
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.mailbox(header::From(mbox))`.
pub fn from(self, mbox: Mailbox) -> Self {
@@ -285,7 +350,7 @@ impl MessageBuilder {
/// Set or add mailbox to `ReplyTo` header
///
/// https://tools.ietf.org/html/rfc5322#section-3.6.2
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
///
/// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
pub fn reply_to(self, mbox: Mailbox) -> Self {
@@ -367,7 +432,7 @@ impl MessageBuilder {
// TODO: High-level methods for attachments and embedded files
/// Create message from body
fn build(self, body: Body) -> Result<Message, EmailError> {
fn build(self, body: MessageBody) -> Result<Message, EmailError> {
// Check for missing required headers
// https://tools.ietf.org/html/rfc5322#section-3.6
@@ -402,32 +467,27 @@ impl MessageBuilder {
})
}
// In theory having a body is optional
/// Plain ASCII body
/// Create [`Message`] using a [`Vec<u8>`], [`String`], or [`Body`] body
///
/// *WARNING*: Generally not what you want
pub fn body<T: Into<String>>(self, body: T) -> Result<Message, EmailError> {
// 998 chars by line
// CR and LF MUST only occur together as CRLF; they MUST NOT appear
// independently in the body.
let body = body.into();
/// Automatically gets encoded with `7bit`, `quoted-printable` or `base64`
/// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
/// for `body`.
pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
let body = body.into_body(maybe_encoding);
if !&body.is_ascii() {
return Err(EmailError::NonAsciiChars);
self.headers.set(body.encoding());
self.build(MessageBody::Raw(body.into_vec()))
}
self.build(Body::Raw(body))
}
/// Create message using mime body ([`MultiPart`](::MultiPart))
/// Create message using mime body ([`MultiPart`][self::MultiPart])
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Multi(part)))
self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
}
/// Create message using mime body ([`SinglePart`](::SinglePart)
/// Create message using mime body ([`SinglePart`][self::SinglePart])
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
self.mime_1_0().build(Body::Mime(Part::Single(part)))
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
}
}
@@ -435,14 +495,14 @@ impl MessageBuilder {
#[derive(Clone, Debug)]
pub struct Message {
headers: Headers,
body: Body,
body: MessageBody,
envelope: Envelope,
}
#[derive(Clone, Debug)]
enum Body {
enum MessageBody {
Mime(Part),
Raw(String),
Raw(Vec<u8>),
}
impl Message {
@@ -472,11 +532,12 @@ impl Message {
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) => {
MessageBody::Mime(p) => p.format(out),
MessageBody::Raw(r) => {
out.extend_from_slice(b"\r\n");
out.extend(r.as_bytes())
out.extend_from_slice(&r)
}
}
}
@@ -490,11 +551,13 @@ impl Default for MessageBuilder {
#[cfg(test)]
mod test {
use crate::message::{header, mailbox::Mailbox, Message};
use crate::message::{header, mailbox::Mailbox, Message, MultiPart, SinglePart};
#[test]
fn email_missing_originator() {
assert!(Message::builder().body("Happy new year!").is_err());
assert!(Message::builder()
.body(String::from("Happy new year!"))
.is_err());
}
#[test]
@@ -502,7 +565,7 @@ mod test {
assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.to("NoBody <nobody@domain.tld>".parse().unwrap())
.body("Happy new year!")
.body(String::from("Happy new year!"))
.is_ok());
}
@@ -511,7 +574,7 @@ mod test {
assert!(Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.from("AnyBody <anybody@domain.tld>".parse().unwrap())
.body("Happy new year!")
.body(String::from("Happy new year!"))
.is_err());
}
@@ -532,7 +595,7 @@ mod test {
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject("яңа ел белән!".into()))
.body("Happy new year!")
.body(String::from("Happy new year!"))
.unwrap();
assert_eq!(
@@ -542,9 +605,57 @@ mod test {
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: Pony O.P. <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Happy new year!"
)
);
}
#[test]
fn email_with_png() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
let img = std::fs::read("./docs/lettre.png").unwrap();
let m = Message::builder()
.date(date)
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.multipart(
MultiPart::related()
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.body(String::from(
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
)),
)
.singlepart(
SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap()))
.header(header::ContentDisposition {
disposition: header::DispositionType::Inline,
parameters: vec![],
})
.header(header::ContentId("<123>".into()))
.body(img),
),
)
.unwrap();
let output = String::from_utf8(m.formatted()).unwrap();
let file_expected = std::fs::read("./testdata/email_with_png.eml").unwrap();
let expected = String::from_utf8(file_expected).unwrap();
for (i, line) in output.lines().zip(expected.lines()).enumerate() {
if i == 6 || i == 8 || i == 13 || i == 232 {
continue;
}
assert_eq!(line.0, line.1)
}
}
}

View File

@@ -1,5 +1,3 @@
use std::str::from_utf8;
// https://tools.ietf.org/html/rfc1522
fn allowed_char(c: char) -> bool {
@@ -18,23 +16,16 @@ pub fn encode(s: &str) -> String {
}
pub fn decode(s: &str) -> Option<String> {
let s = s.trim();
if s.starts_with("=?utf-8?b?") && s.ends_with("?=") {
let s = s.split_at(10).1;
let s = s.split_at(s.len() - 2).0;
base64::decode(s)
.map_err(|_| ())
.and_then(|v| {
if let Ok(s) = from_utf8(&v) {
Ok(Some(s.into()))
} else {
Err(())
}
})
.unwrap_or(None)
} else {
Some(s.into())
}
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)]

View File

@@ -14,16 +14,18 @@ pub enum Error {
Client(&'static str),
/// IO error
Io(io::Error),
/// JSON serialization error
JsonSerialization(serde_json::Error),
/// JSON error
#[cfg(feature = "file-transport-envelope")]
Json(serde_json::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
JsonSerialization(ref err) => err.fmt(fmt),
#[cfg(feature = "file-transport-envelope")]
Json(ref err) => err.fmt(fmt),
}
}
}
@@ -32,7 +34,8 @@ impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
#[cfg(feature = "file-transport-envelope")]
Json(ref err) => Some(&*err),
_ => None,
}
}
@@ -44,9 +47,10 @@ impl From<io::Error> for Error {
}
}
#[cfg(feature = "file-transport-envelope")]
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::JsonSerialization(err)
Error::Json(err)
}
}

View File

@@ -1,50 +1,152 @@
//! The file transport writes the emails to the given directory. The name of the file will be
//! `message_id.txt`.
//! `message_id.eml`.
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//!
//! #### File Transport
//!
//! The file transport writes the emails to the given directory. The name of the file will be
//! `message_id.json`.
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//! ## Sync example
//!
//! ```rust
//! # #[cfg(feature = "file-transport")]
//! # {
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir;
//! use lettre::{Transport, Envelope, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//! .body(String::from("Be happy!"))?;
//!
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//!
//! # #[cfg(not(all(feature = "file-transport", feature = "builder")))]
//! # fn main() {}
//! ```
//!
//! ## Sync example with envelope
//!
//! It is possible to also write the envelope content in a separate JSON file
//! by using the `with_envelope` builder. The JSON file will be written in the
//! target directory with same name and a `json` extension.
//!
//! ```rust
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir;
//!
//! // Write to the local temp directory
//! let sender = FileTransport::with_envelope(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//!
//! # #[cfg(not(all(feature = "file-transport-envelope", feature = "builder")))]
//! # fn main() {}
//! ```
//!
//! ## Async tokio 1.x
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
//! use lettre::{AsyncTransport, Tokio1Executor, Message, AsyncFileTransport};
//!
//! // Write to the local temp directory
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.json`:
//! ## Async async-std 1.x
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use std::env::temp_dir;
//! use lettre::{AsyncTransport, AsyncStd1Executor, Message, AsyncFileTransport};
//!
//! // Write to the local temp directory
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! ---
//!
//! Example email content result
//!
//! ```eml
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! Date: Tue, 18 Aug 2020 22:50:17 GMT
//!
//! Be happy!
//! ```
//!
//! Example envelope result
//!
//! ```json
//! TODO
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
//! ```
use crate::{transport::file::error::Error, Envelope, Transport};
pub use self::error::Error;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use std::marker::PhantomData;
use std::{
fs::File,
io::prelude::*,
path::{Path, PathBuf},
str,
};
use uuid::Uuid;
pub mod error;
mod error;
type Id = String;
@@ -53,23 +155,106 @@ type Id = String;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FileTransport {
path: PathBuf,
#[cfg(feature = "file-transport-envelope")]
save_envelope: bool,
}
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
pub struct AsyncFileTransport<E: Executor> {
inner: FileTransport,
marker_: PhantomData<E>,
}
impl FileTransport {
/// Creates a new transport to the given directory
///
/// Writes the email content in eml format.
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport {
path: PathBuf::from(path.as_ref()),
}
#[cfg(feature = "file-transport-envelope")]
save_envelope: false,
}
}
#[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>,
/// Creates a new transport to the given directory
///
/// Writes the email content in eml format and the envelope
/// in json format.
#[cfg(feature = "file-transport-envelope")]
pub fn with_envelope<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport {
path: PathBuf::from(path.as_ref()),
#[cfg(feature = "file-transport-envelope")]
save_envelope: true,
}
}
/// Read a message that was written using the file transport.
///
/// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")]
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
use std::fs;
let eml_file = self.path.join(format!("{}.eml", email_id));
let eml = fs::read(eml_file)?;
let json_file = self.path.join(format!("{}.json", email_id));
let json = fs::read(&json_file)?;
let envelope = serde_json::from_slice(&json)?;
Ok((envelope, eml))
}
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
self.path.join(format!("{}.{}", email_id, extension))
}
}
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
impl<E> AsyncFileTransport<E>
where
E: Executor,
{
/// Creates a new transport to the given directory
///
/// Writes the email content in eml format.
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self {
inner: FileTransport::new(path),
marker_: PhantomData,
}
}
/// Creates a new transport to the given directory
///
/// Writes the email content in eml format and the envelope
/// in json format.
#[cfg(feature = "file-transport-envelope")]
pub fn with_envelope<P: AsRef<Path>>(path: P) -> Self {
Self {
inner: FileTransport::with_envelope(path),
marker_: PhantomData,
}
}
/// Read a message that was written using the file transport.
///
/// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")]
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
let eml_file = self.inner.path.join(format!("{}.eml", email_id));
let eml = E::fs_read(&eml_file).await?;
let json_file = self.inner.path.join(format!("{}.json", email_id));
let json = E::fs_read(&json_file).await?;
let envelope = serde_json::from_slice(&json)?;
Ok((envelope, eml))
}
}
impl Transport for FileTransport {
@@ -77,68 +262,53 @@ impl Transport for FileTransport {
type Error = Error;
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use std::fs;
let email_id = Uuid::new_v4();
let file = self.path.join(format!("{}.json", email_id));
let serialized = match str::from_utf8(email) {
// Serialize as UTF-8 string if possible
Ok(m) => serde_json::to_string(&SerializableEmail {
envelope: envelope.clone(),
message: Some(m),
raw_message: None,
}),
Err(_) => serde_json::to_string(&SerializableEmail {
envelope: envelope.clone(),
message: None,
raw_message: Some(email),
}),
}?;
let file = self.path(&email_id, "eml");
fs::write(file, email)?;
#[cfg(feature = "file-transport-envelope")]
{
if self.save_envelope {
let file = self.path(&email_id, "json");
fs::write(file, serde_json::to_string(&envelope)?)?;
}
}
// use envelope anyway
let _ = envelope;
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
Ok(email_id.to_string())
}
}
#[cfg(feature = "async")]
pub mod r#async {
use super::{FileTransport, Id, SerializableEmail};
use crate::{r#async::Transport, transport::file::error::Error, Envelope};
use async_std::fs::File;
use async_std::prelude::*;
use async_trait::async_trait;
use std::str;
use uuid::Uuid;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[async_trait]
impl Transport for FileTransport {
impl<E> AsyncTransport for AsyncFileTransport<E>
where
E: Executor,
{
type Ok = Id;
type Error = Error;
async fn send_raw(
&self,
envelope: &Envelope,
email: &[u8],
) -> Result<Self::Ok, Self::Error> {
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let email_id = Uuid::new_v4();
let file = self.path.join(format!("{}.json", email_id));
let serialized = match str::from_utf8(email) {
// Serialize as UTF-8 string if possible
Ok(m) => serde_json::to_string(&SerializableEmail {
envelope: envelope.clone(),
message: Some(m),
raw_message: None,
}),
Err(_) => serde_json::to_string(&SerializableEmail {
envelope: envelope.clone(),
message: None,
raw_message: Some(email),
}),
}?;
let file = self.inner.path(&email_id, "eml");
E::fs_write(&file, email).await?;
#[cfg(feature = "file-transport-envelope")]
{
if self.inner.save_envelope {
let file = self.inner.path(&email_id, "json");
let buf = serde_json::to_vec(&envelope)?;
E::fs_write(&file, &buf).await?;
}
}
// use envelope anyway
let _ = envelope;
let mut file = File::create(file.as_path()).await?;
file.write_all(serialized.as_bytes()).await?;
Ok(email_id.to_string())
}
}
}

View File

@@ -16,10 +16,77 @@
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the
//! logs.
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
#[doc(hidden)]
#[deprecated(note = "use lettre::AsyncStd1Transport")]
#[cfg(feature = "async-std1")]
pub use self::AsyncTransport as AsyncStd1Transport;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio1Transport")]
#[cfg(feature = "tokio1")]
pub use self::AsyncTransport as Tokio1Transport;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio02Transport")]
#[cfg(feature = "tokio02")]
pub use self::AsyncTransport as Tokio02Transport;
use crate::Envelope;
#[cfg(feature = "builder")]
use crate::Message;
#[cfg(feature = "file-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
pub mod file;
#[cfg(feature = "sendmail-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
pub mod sendmail;
#[cfg(feature = "smtp-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
pub mod smtp;
pub mod stub;
/// Blocking Transport method for emails
pub trait Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
self.send_raw(message.envelope(), &raw)
}
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
/// Async Transport method for emails
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[async_trait]
pub trait AsyncTransport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}

View File

@@ -20,7 +20,7 @@ pub enum Error {
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),

View File

@@ -1,47 +1,139 @@
//! The sendmail transport sends the email using the local sendmail command.
//!
//! #### Sendmail Transport
//! ## Sync example
//!
//! The sendmail transport sends the email using the local sendmail command.
//! ```rust
//! # use std::error::Error;
//!
//! ```rust,no_run
//! # #[cfg(feature = "sendmail-transport")]
//! # {
//! use lettre::{Message, Envelope, Transport, SendmailTransport};
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! # 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())
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//! .body(String::from("Be happy!"))?;
//!
//! let sender = SendmailTransport::new();
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//!
//! # #[cfg(not(all(feature = "sendmail-transport", feature = "builder")))]
//! # fn main() {}
//! ```
//!
//! ## Async tokio 0.2 example
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "tokio02", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, Tokio02Executor, AsyncSendmailTransport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! ## Async tokio 1.x example
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, Tokio1Executor, AsyncSendmailTransport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! ## Async async-std 1.x example
//!
//!```rust,no_run
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor, AsyncSendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
use crate::{transport::sendmail::error::Error, Envelope, Transport};
pub use self::error::Error;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor;
#[cfg(feature = "tokio02")]
use crate::Tokio02Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Executor;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use std::marker::PhantomData;
use std::{
convert::AsRef,
ffi::OsString,
io::prelude::*,
process::{Command, Stdio},
};
pub mod error;
mod error;
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
/// Sends an email using the `sendmail` command
#[derive(Debug, Default)]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SendmailTransport {
command: OsString,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
pub struct AsyncSendmailTransport<E: Executor> {
inner: SendmailTransport,
marker_: PhantomData<E>,
}
impl SendmailTransport {
/// Creates a new transport with the default `/usr/sbin/sendmail` command
pub fn new() -> SendmailTransport {
@@ -59,16 +151,112 @@ impl SendmailTransport {
fn command(&self, envelope: &Envelope) -> Command {
let mut c = Command::new(&self.command);
c.arg("-i")
.arg("-f")
.arg(envelope.from().map(|f| f.as_ref()).unwrap_or("\"\""))
c.arg("-i");
if let Some(from) = envelope.from() {
c.arg("-f").arg(from);
}
c.arg("--")
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped());
.stdout(Stdio::piped())
.stderr(Stdio::piped());
c
}
}
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
impl<E> AsyncSendmailTransport<E>
where
E: Executor,
{
/// Creates a new transport with the default `/usr/sbin/sendmail` command
pub fn new() -> Self {
Self {
inner: SendmailTransport::new(),
marker_: PhantomData,
}
}
/// Creates a new transport to the given sendmail command
pub fn new_with_command<S: Into<OsString>>(command: S) -> Self {
Self {
inner: SendmailTransport::new_with_command(command),
marker_: PhantomData,
}
}
#[cfg(feature = "tokio02")]
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
use tokio02_crate::process::Command;
let mut c = Command::new(&self.inner.command);
c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
c.arg("-f").arg(from);
}
c.arg("--")
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
c
}
#[cfg(feature = "tokio1")]
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
use tokio1_crate::process::Command;
let mut c = Command::new(&self.inner.command);
c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
c.arg("-f").arg(from);
}
c.arg("--")
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
c
}
#[cfg(feature = "async-std1")]
fn async_std_command(&self, envelope: &Envelope) -> async_std::process::Command {
use async_std::process::Command;
let mut c = Command::new(&self.inner.command);
// TODO: figure out why enabling this kills it earlier
// c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
c.arg("-f").arg(from);
}
c.arg("--")
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
c
}
}
impl Default for SendmailTransport {
fn default() -> Self {
Self::new()
}
}
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
impl<E> Default for AsyncSendmailTransport<E>
where
E: Executor,
{
fn default() -> Self {
Self::new()
}
}
impl Transport for SendmailTransport {
type Ok = ();
type Error = Error;
@@ -88,35 +276,22 @@ impl Transport for SendmailTransport {
}
}
#[cfg(feature = "async")]
pub mod r#async {
use super::SendmailTransport;
use crate::{r#async::Transport, transport::sendmail::error::Error, Envelope};
use async_trait::async_trait;
use std::io::Write;
#[cfg(feature = "async-std1")]
#[async_trait]
impl Transport for SendmailTransport {
impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
type Ok = ();
type Error = Error;
// TODO: Convert to real async, once async-std has a process implementation.
async fn send_raw(
&self,
envelope: &Envelope,
email: &[u8],
) -> Result<Self::Ok, Self::Error> {
let mut command = self.command(envelope);
let email = email.to_vec();
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use async_std::io::prelude::WriteExt;
let mut command = self.async_std_command(envelope);
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?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.output().await?;
if output.status.success() {
Ok(())
@@ -125,4 +300,53 @@ pub mod r#async {
}
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl AsyncTransport for AsyncSendmailTransport<Tokio02Executor> {
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 = "tokio1")]
#[async_trait]
impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio1_crate::io::AsyncWriteExt;
let mut command = self.tokio1_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
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,292 @@
use std::marker::PhantomData;
use async_trait::async_trait;
use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
};
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Executor;
use crate::{Envelope, Executor};
#[allow(missing_debug_implementations)]
pub struct AsyncSmtpTransport<E> {
// TODO: pool
inner: AsyncSmtpClient<E>,
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl AsyncTransport for AsyncSmtpTransport<Tokio02Executor> {
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 = "tokio1")]
#[async_trait]
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
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 = "async-std1")]
#[async_trait]
impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
type Ok = Response;
type Error = Error;
/// Sends an email
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut conn = self.inner.connection().await?;
let result = conn.send(envelope, email).await?;
conn.quit().await?;
Ok(result)
}
}
impl<E> AsyncSmtpTransport<E>
where
E: Executor,
{
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
///
/// The right option for most SMTP servers.
///
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{Tls, 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 = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{Tls, 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<E> {
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 new = SmtpInfo {
server: server.into(),
..Default::default()
};
AsyncSmtpTransportBuilder { info: new }
}
}
impl<E> Clone for AsyncSmtpTransport<E>
where
E: Executor,
{
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
/// Contains client configuration.
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
#[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 = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
pub fn tls(mut self, tls: super::Tls) -> Self {
self.info.tls = tls;
self
}
/// Build the transport (with default pool if enabled)
pub fn build<E>(self) -> AsyncSmtpTransport<E>
where
E: Executor,
{
let client = AsyncSmtpClient {
info: self.info,
marker_: PhantomData,
};
AsyncSmtpTransport { inner: client }
}
}
/// Build client
pub struct AsyncSmtpClient<C> {
info: SmtpInfo,
marker_: PhantomData<C>,
}
impl<E> AsyncSmtpClient<E>
where
E: Executor,
{
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
let mut conn = E::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)
}
}
impl<E> AsyncSmtpClient<E>
where
E: Executor,
{
fn clone(&self) -> Self {
Self {
info: self.info.clone(),
marker_: PhantomData,
}
}
}
#[doc(hidden)]
#[deprecated(note = "use lettre::Executor instead")]
pub use crate::Executor as AsyncSmtpConnector;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio02Executor instead")]
#[cfg(feature = "tokio02")]
pub type Tokio02Connector = crate::Tokio02Executor;
#[doc(hidden)]
#[deprecated(note = "use lettre::Tokio1Executor instead")]
#[cfg(feature = "tokio1")]
pub type Tokio1Connector = crate::Tokio1Executor;
#[doc(hidden)]
#[deprecated(note = "use lettre::AsyncStd1Executor instead")]
#[cfg(feature = "async-std1")]
pub type AsyncStd1Connector = crate::AsyncStd1Executor;

View File

@@ -7,25 +7,6 @@ use std::fmt::{self, Display, Formatter};
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Convertible to user credentials
pub trait IntoCredentials {
/// Converts to a `Credentials` struct
fn into_credentials(self) -> Credentials;
}
impl IntoCredentials for Credentials {
fn into_credentials(self) -> Credentials {
self
}
}
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
fn into_credentials(self) -> Credentials {
let (username, password) = self;
Credentials::new(username.into(), password.into())
}
}
/// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -44,33 +25,40 @@ impl Credentials {
}
}
impl<S, T> From<(S, T)> for Credentials
where
S: Into<String>,
T: Into<String>,
{
fn from((username, password): (S, T)) -> Self {
Credentials::new(username.into(), password.into())
}
}
/// Represents authentication mechanisms
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Mechanism {
/// PLAIN authentication mechanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
/// PLAIN authentication mechanism, defined in
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
Plain,
/// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
///
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
Login,
/// Non-standard XOAUTH2 mechanism
/// https://developers.google.com/gmail/imap/xoauth2-protocol
/// Non-standard XOAUTH2 mechanism, defined in
/// [xoauth2-protocol](https://developers.google.com/gmail/imap/xoauth2-protocol)
Xoauth2,
}
impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
Mechanism::Xoauth2 => "XOAUTH2",
}
)
})
}
}
@@ -172,4 +160,12 @@ mod test {
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
#[test]
fn test_from_user_pass_for_credentials() {
assert_eq!(
Credentials::new("alice".to_string(), "wonderland".to_string()),
Credentials::from(("alice", "wonderland"))
);
}
}

View File

@@ -0,0 +1,319 @@
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 = "tokio1")]
pub async fn connect_tokio1(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio1(hostname, port, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_asyncstd1(hostname, port, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
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![];
// Internationalization handling
//
// * 8BITMIME: https://tools.ietf.org/html/rfc6152
// * SMTPUTF8: https://tools.ietf.org/html/rfc653
// Check for non-ascii addresses and use the SMTPUTF8 option if any.
if envelope.has_non_ascii_addresses() {
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
// don't try to send non-ascii addresses (per RFC)
return Err(Error::Client(
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
));
}
mail_options.push(MailParameter::SmtpUtfEight);
}
// Check for non-ascii content in message
if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(Error::Client(
"Message contains non-ascii chars but server does not support 8BITMIME",
));
}
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options))
.await,
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.code));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.code));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
}
}

View File

@@ -0,0 +1,559 @@
#[cfg(any(
feature = "tokio02-rustls-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
))]
use std::sync::Arc;
use std::{
net::SocketAddr,
pin::Pin,
task::{Context, Poll},
};
use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
Result as IoResult,
};
#[cfg(feature = "tokio02")]
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
#[cfg(feature = "tokio1")]
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
#[cfg(feature = "async-std1")]
use async_std::net::TcpStream as AsyncStd1TcpStream;
#[cfg(feature = "tokio02")]
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
#[cfg(feature = "tokio1")]
use tokio1_crate::net::TcpStream as Tokio1TcpStream;
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "tokio02-native-tls")]
use tokio02_native_tls_crate::TlsStream as Tokio02TlsStream;
#[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "async-std1-rustls-tls")]
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio02-rustls-tls")]
use tokio02_rustls::client::TlsStream as Tokio02RustlsTlsStream;
#[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
use super::InnerTlsParameters;
use super::TlsParameters;
use crate::transport::smtp::Error;
/// 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 1.x TCP stream
#[cfg(feature = "tokio1")]
Tokio1Tcp(Tokio1TcpStream),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-native-tls")]
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "tokio1-rustls-tls")]
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>),
/// Plain Tokio 1.x TCP stream
#[cfg(feature = "async-std1")]
AsyncStd1Tcp(AsyncStd1TcpStream),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-native-tls")]
AsyncStd1NativeTls(AsyncStd1TlsStream<AsyncStd1TcpStream>),
/// Encrypted Tokio 1.x TCP stream
#[cfg(feature = "async-std1-rustls-tls")]
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
/// Can't be built
None,
}
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 = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Err(IoError::new(
ErrorKind::Other,
"InnerAsyncNetworkStream::None must never be built",
))
}
}
}
#[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 = "tokio1")]
pub async fn connect_tokio1(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio1TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
}
#[cfg(feature = "async-std1")]
pub async fn connect_asyncstd1(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(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 = "tokio1",
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
}
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters).await?;
Ok(())
}
#[cfg(all(
feature = "async-std1",
not(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))
))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the async-std1-native-tls or the async-std1-rustls-tls feature");
}
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters).await?;
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 = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
async fn upgrade_tokio1_tls(
tcp_stream: Tokio1TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio1-native-tls"))]
panic!("built without the tokio1-native-tls feature");
#[cfg(feature = "tokio1-native-tls")]
return {
use tokio1_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio1-rustls-tls"))]
panic!("built without the tokio1-rustls-tls feature");
#[cfg(feature = "tokio1-rustls-tls")]
return {
use tokio1_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::Tokio1RustlsTls(stream))
};
}
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
async fn upgrade_asyncstd1_tls(
tcp_stream: AsyncStd1TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
/*
#[cfg(not(feature = "async-std1-native-tls"))]
panic!("built without the async-std1-native-tls feature");
#[cfg(feature = "async-std1-native-tls")]
return {
use async_native_tls::TlsConnector;
// TODO: fix
let connector: TlsConnector = todo!();
// let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::AsyncStd1NativeTls(stream))
};
*/
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "async-std1-rustls-tls"))]
panic!("built without the async-std1-rustls-tls feature");
#[cfg(feature = "async-std1-rustls-tls")]
return {
use async_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(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 = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
InnerAsyncNetworkStream::None => false,
}
}
}
impl FuturesAsyncRead 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 = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => {
let mut b = Tokio1ReadBuf::new(buf);
match Pin::new(s).poll_read(cx, &mut b) {
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
Poll::Pending => Poll::Pending,
}
}
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
Pin::new(s).poll_read(cx, buf)
}
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
Pin::new(s).poll_read(cx, buf)
}
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(0))
}
}
}
}
impl FuturesAsyncWrite for AsyncNetworkStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<IoResult<usize>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
Pin::new(s).poll_write(cx, buf)
}
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
Pin::new(s).poll_write(cx, buf)
}
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None 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 = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
}
}
fn poll_close(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_shutdown(cx),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-native-tls")]
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "tokio1-rustls-tls")]
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
#[cfg(feature = "async-std1")]
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-native-tls")]
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_close(cx),
#[cfg(feature = "async-std1-rustls-tls")]
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_close(cx),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Poll::Ready(Ok(()))
}
}
}
}

View File

@@ -0,0 +1,292 @@
use std::{
fmt::Display,
io::{self, BufRead, BufReader, Write},
net::ToSocketAddrs,
time::Duration,
};
use super::{ClientCodec, NetworkStream, TlsParameters};
use crate::{
address::Envelope,
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![];
// Internationalization handling
//
// * 8BITMIME: https://tools.ietf.org/html/rfc6152
// * SMTPUTF8: https://tools.ietf.org/html/rfc653
// Check for non-ascii addresses and use the SMTPUTF8 option if any.
if envelope.has_non_ascii_addresses() {
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
// don't try to send non-ascii addresses (per RFC)
return Err(Error::Client(
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
));
}
mail_options.push(MailParameter::SmtpUtfEight);
}
// Check for non-ascii content in message
if !email.is_ascii() {
if !self.server_info().supports_feature(Extension::EightBitMime) {
return Err(Error::Client(
"Message contains non-ascii chars but server does not support 8BITMIME",
));
}
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options)),
self
);
// 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.code));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.code));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
}
}

View File

@@ -1,36 +1,58 @@
//! SMTP client
//!
//! `SmtpConnection` allows manually sending SMTP commands.
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! # #[cfg(feature = "smtp-transport")]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection};
//!
//! let hello = ClientId::Domain("my_hostname".to_string());
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None)?;
//! client.command(
//! Mail::new(Some("user@example.com".parse()?), vec![])
//! )?;
//! client.command(
//! Rcpt::new("user@example.org".parse()?, vec![])
//! )?;
//! client.command(Data)?;
//! client.message("Test email".as_bytes())?;
//! client.command(Quit)?;
//! # Ok(())
//! # }
//! ```
use crate::{
transport::smtp::{
authentication::{Credentials, Mechanism},
client::net::{NetworkStream, TlsParameters},
commands::*,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
Envelope,
};
use bufstream::BufStream;
#[cfg(feature = "log")]
use log::debug;
#[cfg(feature = "serde")]
use std::fmt::Debug;
use std::{
fmt::Display,
io::{self, BufRead, Write},
net::ToSocketAddrs,
string::String,
time::Duration,
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
pub(crate) use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
pub(crate) use self::async_net::AsyncNetworkStream;
use self::net::NetworkStream;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub(super) use self::tls::InnerTlsParameters;
pub use self::{
connection::SmtpConnection,
mock::MockStream,
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
};
pub mod mock;
pub mod net;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
mod async_connection;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
mod async_net;
mod connection;
mod mock;
mod net;
mod tls;
/// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClientCodec {
struct ClientCodec {
escape_count: u8,
}
@@ -41,17 +63,16 @@ impl ClientCodec {
}
/// Adds transparency
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) -> Result<(), Error> {
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
match frame.len() {
0 => {
match self.escape_count {
0 => buf.write_all(b"\r\n.\r\n")?,
1 => buf.write_all(b"\n.\r\n")?,
2 => buf.write_all(b".\r\n")?,
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;
Ok(())
}
_ => {
let mut start = 0;
@@ -64,13 +85,12 @@ impl ClientCodec {
}
if self.escape_count == 3 {
self.escape_count = 0;
buf.write_all(&frame[start..idx])?;
buf.write_all(b".")?;
buf.extend_from_slice(&frame[start..idx]);
buf.extend_from_slice(b".");
start = idx;
}
}
buf.write_all(&frame[start..])?;
Ok(())
buf.extend_from_slice(&frame[start..]);
}
}
}
@@ -78,267 +98,11 @@ impl ClientCodec {
/// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays
#[cfg(feature = "log")]
fn escape_crlf(string: &str) -> String {
#[cfg(feature = "tracing")]
pub(super) fn escape_crlf(string: &str) -> String {
string.replace("\r\n", "<CRLF>")
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort();
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client
pub struct SmtpConnection {
/// TCP stream between client and server
/// Value is None before connection
stream: BufStream<NetworkStream>,
/// Panic state
panic: bool,
/// Information about the server
server_info: ServerInfo,
}
impl SmtpConnection {
pub fn server_info(&self) -> &ServerInfo {
&self.server_info
}
// FIXME add simple connect and rename this one
/// Connects to the configured server
///
/// Sends EHLO and parses server information
pub fn connect<A: ToSocketAddrs>(
server: A,
timeout: Option<Duration>,
hello_name: &ClientId,
tls_parameters: Option<&TlsParameters>,
) -> Result<SmtpConnection, Error> {
let stream = BufStream::new(NetworkStream::connect(server, timeout, tls_parameters)?);
let mut conn = SmtpConnection {
stream,
panic: false,
server_info: ServerInfo::default(),
};
conn.set_timeout(timeout)?;
// TODO log
let _response = conn.read_response()?;
conn.ehlo(hello_name)?;
// Print server information
#[cfg(feature = "log")]
debug!("server {}", conn.server_info);
Ok(conn)
}
pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
// Mail
let mut mail_options = vec![];
if self.server_info().supports_feature(Extension::EightBitMime) {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options,)),
self
);
// Recipient
for to_address in envelope.to() {
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
}
// Data
try_smtp!(self.command(Data), self);
// Message content
let result = try_smtp!(self.message(email), self);
Ok(result)
}
pub fn has_broken(&self) -> bool {
self.panic
}
pub fn can_starttls(&self) -> bool {
!self.stream.get_ref().is_encrypted()
&& self.server_info.supports_feature(Extension::StartTls)
}
#[allow(unused_variables)]
pub fn starttls(
&mut self,
tls_parameters: &TlsParameters,
hello_name: &ClientId,
) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
{
try_smtp!(self.command(Starttls), self);
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
#[cfg(feature = "log")]
debug!("connection encrypted");
// Send EHLO again
try_smtp!(self.ehlo(hello_name), self);
Ok(())
}
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
// This should never happen as `Tls` can only be created
// when a TLS library is enabled
unreachable!("TLS support required but not supported");
} else {
Err(Error::Client("STARTTLS is not supported on this server"))
}
}
/// Send EHLO and update server info
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(
self.command(Ehlo::new(ClientId::new(hello_name.to_string()))),
self
);
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
Ok(())
}
pub fn quit(&mut self) -> Result<Response, Error> {
Ok(try_smtp!(self.command(Quit), self))
}
pub fn abort(&mut self) {
// Only try to quit if we are not already broken
if !self.panic {
self.panic = true;
let _ = self.command(Quit);
}
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: NetworkStream) {
self.stream = BufStream::new(stream);
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
self.stream.get_ref().is_encrypted()
}
/// Set timeout
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
self.stream.get_mut().set_read_timeout(duration)?;
self.stream.get_mut().set_write_timeout(duration)
}
/// Checks if the server is connected using the NOOP SMTP command
pub fn test_connected(&mut self) -> bool {
self.command(Noop).is_ok()
}
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
pub fn auth(
&mut self,
mechanisms: &[Mechanism],
credentials: &Credentials,
) -> Result<Response, Error> {
let mechanism = match self.server_info.get_auth_mechanism(mechanisms) {
Some(m) => m,
None => {
return Err(Error::Client(
"No compatible authentication mechanism was found",
))
}
};
// Limit challenges to avoid blocking
let mut challenges = 10;
let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
while challenges > 0 && response.has_code(334) {
challenges -= 1;
response = try_smtp!(
self.command(Auth::new_from_response(
mechanism,
credentials.clone(),
&response,
)?),
self
);
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
} else {
Ok(response)
}
}
/// Sends the message content
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new();
codec.encode(message, &mut out_buf)?;
self.write(out_buf.as_slice())?;
self.write(b"\r\n.\r\n")?;
self.read_response()
}
/// Sends an SMTP command
pub fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
self.write(command.to_string().as_bytes())?;
self.read_response()
}
/// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
self.stream.write_all(string)?;
self.stream.flush()?;
#[cfg(feature = "log")]
debug!(
"Wrote: {}",
escape_crlf(String::from_utf8_lossy(string).as_ref())
);
Ok(())
}
/// Gets the SMTP response
pub fn read_response(&mut self) -> Result<Response, Error> {
let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer)? > 0 {
#[cfg(feature = "log")]
debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) {
Ok((_remaining, response)) => {
if response.is_positive() {
return Ok(response);
}
return Err(response.into());
}
Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.1));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.1));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
}
}
#[cfg(test)]
mod test {
use super::*;
@@ -348,15 +112,15 @@ mod test {
let mut codec = ClientCodec::new();
let mut buf: Vec<u8> = vec![];
assert!(codec.encode(b"test\r\n", &mut buf).is_ok());
assert!(codec.encode(b".\r\n", &mut buf).is_ok());
assert!(codec.encode(b"\r\ntest", &mut buf).is_ok());
assert!(codec.encode(b"te\r\n.\r\nst", &mut buf).is_ok());
assert!(codec.encode(b"test", &mut buf).is_ok());
assert!(codec.encode(b"test.", &mut buf).is_ok());
assert!(codec.encode(b"test\n", &mut buf).is_ok());
assert!(codec.encode(b".test\n", &mut buf).is_ok());
assert!(codec.encode(b"test", &mut buf).is_ok());
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"

View File

@@ -1,12 +1,3 @@
//! A trait to represent a stream
use crate::transport::smtp::{client::mock::MockStream, error::Error};
#[cfg(feature = "native-tls")]
use native_tls::{TlsConnector, TlsStream};
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConfig, ClientSession};
#[cfg(feature = "native-tls")]
use std::io::ErrorKind;
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
use std::{
@@ -15,61 +6,57 @@ use std::{
time::Duration,
};
/// Parameters to use for secure clients
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct TlsParameters {
/// A connector from `native-tls`
#[cfg(feature = "native-tls")]
connector: TlsConnector,
/// A client from `rustls`
#[cfg(feature = "rustls-tls")]
// TODO use the same in all transports of the client
connector: Box<ClientConfig>,
/// The domain name which is expected in the TLS certificate from the server
domain: String,
}
use native_tls::TlsStream;
impl TlsParameters {
/// Creates a `TlsParameters`
#[cfg(feature = "native-tls")]
pub fn new(domain: String, connector: TlsConnector) -> Self {
Self { connector, domain }
}
/// Creates a `TlsParameters`
#[cfg(feature = "rustls-tls")]
pub fn new(domain: String, connector: ClientConfig) -> Self {
Self {
connector: Box::new(connector),
domain,
}
}
use rustls::{ClientSession, StreamOwned};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::InnerTlsParameters;
use super::{MockStream, TlsParameters};
use crate::transport::smtp::Error;
/// A network stream
pub struct NetworkStream {
inner: InnerNetworkStream,
}
/// Represents the different types of underlying network streams
pub enum NetworkStream {
// 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")]
Tls(Box<TlsStream<TcpStream>>),
NativeTls(TlsStream<TcpStream>),
/// Encrypted TCP stream
#[cfg(feature = "rustls-tls")]
Tls(Box<rustls::StreamOwned<ClientSession, TcpStream>>),
RustlsTls(StreamOwned<ClientSession, TcpStream>),
/// Mock stream
Mock(MockStream),
}
impl NetworkStream {
fn new(inner: InnerNetworkStream) -> Self {
NetworkStream { inner }
}
pub fn new_mock(mock: MockStream) -> Self {
Self::new(InnerNetworkStream::Mock(mock))
}
/// Returns peer's address
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
match *self {
NetworkStream::Tcp(ref s) => s.peer_addr(),
match self.inner {
InnerNetworkStream::Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")]
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
))),
@@ -78,13 +65,13 @@ impl NetworkStream {
/// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref s) => s.shutdown(how),
match self.inner {
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")]
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
NetworkStream::Mock(_) => Ok(()),
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::Mock(_) => Ok(()),
}
}
@@ -99,9 +86,8 @@ impl NetworkStream {
) -> Result<TcpStream, Error> {
let addrs = server.to_socket_addrs()?;
for addr in addrs {
let result = TcpStream::connect_timeout(&addr, timeout);
if result.is_ok() {
return result.map_err(|e| e.into());
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
return Ok(result);
}
}
Err(Error::Client("Could not connect"))
@@ -112,119 +98,143 @@ impl NetworkStream {
None => TcpStream::connect(server)?,
};
match tls_parameters {
#[cfg(feature = "native-tls")]
Some(context) => context
.connector
.connect(context.domain.as_ref(), tcp_stream)
.map(|tls| NetworkStream::Tls(Box::new(tls)))
.map_err(|e| Error::Io(io::Error::new(ErrorKind::Other, e))),
#[cfg(feature = "rustls-tls")]
Some(context) => {
let domain = webpki::DNSNameRef::try_from_ascii_str(&context.domain)?;
Ok(NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
ClientSession::new(&Arc::new(*context.connector.clone()), domain),
tcp_stream,
))))
}
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
Some(_) => panic!("TLS configuration without support"),
None => Ok(NetworkStream::Tcp(tcp_stream)),
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters)?;
}
Ok(stream)
}
#[allow(unused_variables, unreachable_code)]
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
*self = match *self {
#[cfg(feature = "native-tls")]
NetworkStream::Tcp(ref mut stream) => match tls_parameters
.connector
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
{
Ok(tls_stream) => NetworkStream::Tls(Box::new(tls_stream)),
Err(err) => return Err(Error::Io(io::Error::new(ErrorKind::Other, err))),
},
#[cfg(feature = "rustls-tls")]
NetworkStream::Tcp(ref mut stream) => {
let domain = webpki::DNSNameRef::try_from_ascii_str(&tls_parameters.domain)?;
NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
ClientSession::new(&Arc::new(*tls_parameters.connector.clone()), domain),
stream.try_clone().unwrap(),
)))
}
match &self.inner {
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
NetworkStream::Tcp(_) => panic!("STARTTLS without TLS support"),
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"))]
NetworkStream::Tls(_) => return Ok(()),
NetworkStream::Mock(_) => return Ok(()),
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 {
NetworkStream::Tcp(_) | NetworkStream::Mock(_) => false,
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
NetworkStream::Tls(_) => true,
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 {
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
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 {
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
match self.inner {
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::Mock(_) => Ok(()),
}
}
}
impl Read for NetworkStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
NetworkStream::Tcp(ref mut s) => s.read(buf),
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.read(buf),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref mut s) => s.read(buf),
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
#[cfg(feature = "rustls-tls")]
NetworkStream::Tls(ref mut s) => s.read(buf),
NetworkStream::Mock(ref mut s) => s.read(buf),
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
InnerNetworkStream::Mock(ref mut s) => s.read(buf),
}
}
}
impl Write for NetworkStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
NetworkStream::Tcp(ref mut s) => s.write(buf),
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.write(buf),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref mut s) => s.write(buf),
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
#[cfg(feature = "rustls-tls")]
NetworkStream::Tls(ref mut s) => s.write(buf),
NetworkStream::Mock(ref mut s) => s.write(buf),
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
InnerNetworkStream::Mock(ref mut s) => s.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut s) => s.flush(),
match self.inner {
InnerNetworkStream::Tcp(ref mut s) => s.flush(),
#[cfg(feature = "native-tls")]
NetworkStream::Tls(ref mut s) => s.flush(),
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
#[cfg(feature = "rustls-tls")]
NetworkStream::Tls(ref mut s) => s.flush(),
NetworkStream::Mock(ref mut s) => s.flush(),
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
InnerNetworkStream::Mock(ref mut s) => s.flush(),
}
}
}

View File

@@ -0,0 +1,283 @@
#[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) -> Self {
self.root_certs.push(cert);
self
}
/// Controls whether certificates with an invalid hostname are accepted
///
/// Defaults to `false`.
///
/// # Warning
///
/// You should think very carefully before using this method.
/// If hostname verification is disabled *any* valid certificate,
/// including those from other sites, are trusted.
///
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
///
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
}
/// Controls whether invalid certificates are accepted
///
/// Defaults to `false`.
///
/// # Warning
///
/// You should think very carefully before using this method.
/// If certificate verification is disabled, *any* certificate
/// is trusted for use, including:
///
/// * Self signed certificates
/// * Certificates from different hostnames
/// * Expired certificates
///
/// This method should only be used as a last resort, as it introduces
/// significant vulnerabilities to man-in-the-middle attacks.
pub fn dangerous_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
self.accept_invalid_certs = accept_invalid_certs;
self
}
/// Creates a new `TlsParameters` using native-tls or rustls
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
// TODO: remove below line once native-tls is supported with async-std
#[allow(unreachable_code)]
pub fn build(self) -> Result<TlsParameters, Error> {
// TODO: remove once native-tls is supported with async-std
#[cfg(all(feature = "rustls-tls", feature = "async-std1"))]
return self.build_rustls();
#[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

@@ -9,12 +9,7 @@ use crate::{
},
Address,
};
#[cfg(feature = "log")]
use log::debug;
use std::{
convert::AsRef,
fmt::{self, Display, Formatter},
};
use std::fmt::{self, Display, Formatter};
/// EHLO command
#[derive(PartialEq, Clone, Debug)]
@@ -24,7 +19,7 @@ pub struct Ehlo {
}
impl Display for Ehlo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "EHLO {}\r\n", self.client_id)
}
}
@@ -42,7 +37,7 @@ impl Ehlo {
pub struct Starttls;
impl Display for Starttls {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("STARTTLS\r\n")
}
}
@@ -56,7 +51,7 @@ pub struct Mail {
}
impl Display for Mail {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"MAIL FROM:<{}>",
@@ -85,7 +80,7 @@ pub struct Rcpt {
}
impl Display for Rcpt {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
@@ -110,7 +105,7 @@ impl Rcpt {
pub struct Data;
impl Display for Data {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("DATA\r\n")
}
}
@@ -121,7 +116,7 @@ impl Display for Data {
pub struct Quit;
impl Display for Quit {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("QUIT\r\n")
}
}
@@ -132,7 +127,7 @@ impl Display for Quit {
pub struct Noop;
impl Display for Noop {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("NOOP\r\n")
}
}
@@ -145,10 +140,10 @@ pub struct Help {
}
impl Display for Help {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("HELP")?;
if self.argument.is_some() {
write!(f, " {}", self.argument.as_ref().unwrap())?;
if let Some(argument) = &self.argument {
write!(f, " {}", argument)?;
}
f.write_str("\r\n")
}
@@ -169,7 +164,7 @@ pub struct Vrfy {
}
impl Display for Vrfy {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "VRFY {}\r\n", self.argument)
}
}
@@ -189,7 +184,7 @@ pub struct Expn {
}
impl Display for Expn {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "EXPN {}\r\n", self.argument)
}
}
@@ -207,7 +202,7 @@ impl Expn {
pub struct Rset;
impl Display for Rset {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("RSET\r\n")
}
}
@@ -223,11 +218,8 @@ pub struct Auth {
}
impl Display for Auth {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = self
.response
.as_ref()
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
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())?;
@@ -275,12 +267,12 @@ impl Auth {
let encoded_challenge = response
.first_word()
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
#[cfg(feature = "log")]
debug!("auth encoded challenge: {}", encoded_challenge);
#[cfg(feature = "tracing")]
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
#[cfg(feature = "log")]
debug!("auth decoded challenge: {}", decoded_challenge);
#[cfg(feature = "tracing")]
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
@@ -343,7 +335,7 @@ mod test {
"RCPT TO:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", Rcpt::new(email.clone(), vec![rcpt_parameter])),
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n"
);
assert_eq!(format!("{}", Quit), "QUIT\r\n");
@@ -374,7 +366,7 @@ mod test {
assert_eq!(
format!(
"{}",
Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
Auth::new(Mechanism::Login, credentials, None).unwrap()
),
"AUTH LOGIN\r\n"
);

View File

@@ -35,29 +35,35 @@ pub enum 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> {
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",
}),
Transient(ref err) => fmt.write_str(
err.first_line()
.unwrap_or("transient error during SMTP transaction"),
),
Permanent(ref err) => fmt.write_str(
err.first_line()
.unwrap_or("permanent error during SMTP transaction"),
),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
@@ -69,6 +75,8 @@ impl Display for Error {
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),
}
@@ -101,12 +109,12 @@ impl From<native_tls::Error> for Error {
}
}
impl From<nom::Err<(&str, nom::error::ErrorKind)>> for Error {
fn from(err: nom::Err<(&str, nom::error::ErrorKind)>) -> Error {
impl From<nom::Err<nom::error::Error<&str>>> for Error {
fn from(err: nom::Err<nom::error::Error<&str>>) -> Error {
Parsing(match err {
nom::Err::Incomplete(_) => nom::error::ErrorKind::Complete,
nom::Err::Failure((_, k)) => k,
nom::Err::Error((_, k)) => k,
nom::Err::Failure(e) => e.code,
nom::Err::Error(e) => e.code,
})
}
}

View File

@@ -10,12 +10,10 @@ use std::{
result::Result,
};
/// Default client id
const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
/// Client identifier, the parameter to `EHLO`
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ClientId {
/// A fully-qualified domain name
Domain(String),
@@ -25,36 +23,45 @@ pub enum ClientId {
Ipv6(Ipv6Addr),
}
const LOCALHOST_CLIENT: ClientId = ClientId::Ipv4(Ipv4Addr::new(127, 0, 0, 1));
impl Default for ClientId {
fn default() -> Self {
// https://tools.ietf.org/html/rfc5321#section-4.1.4
//
// The SMTP client MUST, if possible, ensure that the domain parameter
// to the EHLO command is a primary host name as specified for this
// command in Section 2.3.5. If this is not possible (e.g., when the
// client's address is dynamically assigned and the client does not have
// an obvious name), an address literal SHOULD be substituted for the
// domain name.
#[cfg(feature = "hostname")]
{
hostname::get()
.ok()
.and_then(|s| s.into_string().map(Self::Domain).ok())
.unwrap_or(LOCALHOST_CLIENT)
}
#[cfg(not(feature = "hostname"))]
LOCALHOST_CLIENT
}
}
impl Display for ClientId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
ClientId::Domain(ref value) => f.write_str(value),
ClientId::Ipv4(ref value) => write!(f, "{}", value),
ClientId::Ipv6(ref value) => write!(f, "{}", value),
Self::Domain(ref value) => f.write_str(value),
Self::Ipv4(ref value) => write!(f, "[{}]", value),
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value),
}
}
}
impl ClientId {
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
/// Creates a new `ClientId` from a fully qualified domain name
pub fn new(domain: String) -> ClientId {
ClientId::Domain(domain)
}
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
/// found
#[cfg(feature = "hostname")]
pub fn hostname() -> ClientId {
ClientId::Domain(
hostname::get()
.map_err(|_| ())
.and_then(|s| s.into_string().map_err(|_| ()))
.unwrap_or_else(|_| DEFAULT_DOMAIN_CLIENT_ID.to_string()),
)
}
#[cfg(not(feature = "hostname"))]
pub fn hostname() -> ClientId {
ClientId::Domain(DEFAULT_DOMAIN_CLIENT_ID.to_string())
pub fn new(domain: String) -> Self {
Self::Domain(domain)
}
}
@@ -64,26 +71,26 @@ impl ClientId {
pub enum Extension {
/// 8BITMIME keyword
///
/// RFC 6152: https://tools.ietf.org/html/rfc6152
/// Defined in [RFC 6152](https://tools.ietf.org/html/rfc6152)
EightBitMime,
/// SMTPUTF8 keyword
///
/// RFC 6531: https://tools.ietf.org/html/rfc6531
/// Defined in [RFC 6531](https://tools.ietf.org/html/rfc6531)
SmtpUtfEight,
/// STARTTLS keyword
///
/// RFC 2487: https://tools.ietf.org/html/rfc2487
/// Defined in [RFC 2487](https://tools.ietf.org/html/rfc2487)
StartTls,
/// AUTH mechanism
Authentication(Mechanism),
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "SMTPUTF8"),
Extension::StartTls => write!(f, "STARTTLS"),
Extension::EightBitMime => f.write_str("8BITMIME"),
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
Extension::StartTls => f.write_str("STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
}
}
@@ -104,17 +111,13 @@ pub struct ServerInfo {
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{} with {}",
self.name,
if self.features.is_empty() {
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)
}
}
@@ -212,7 +215,7 @@ pub enum MailParameter {
}
impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
MailParameter::Size(size) => write!(f, "SIZE={}", size),
@@ -240,7 +243,7 @@ pub enum MailBodyParameter {
}
impl Display for MailBodyParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
MailBodyParameter::SevenBit => f.write_str("7BIT"),
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
@@ -262,7 +265,7 @@ pub enum RcptParameter {
}
impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
RcptParameter::Other {
ref keyword,
@@ -279,7 +282,7 @@ impl Display for RcptParameter {
#[cfg(test)]
mod test {
use super::{ClientId, Extension, ServerInfo};
use super::*;
use crate::transport::smtp::{
authentication::Mechanism,
response::{Category, Code, Detail, Response, Severity},
@@ -289,9 +292,10 @@ mod test {
#[test]
fn test_clientid_fmt() {
assert_eq!(
format!("{}", ClientId::new("test".to_string())),
format!("{}", ClientId::Domain("test".to_string())),
"test".to_string()
);
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string());
}
#[test]
@@ -316,7 +320,7 @@ mod test {
"{}",
ServerInfo {
name: "name".to_string(),
features: eightbitmime.clone(),
features: eightbitmime,
}
),
"name with {EightBitMime}".to_string()
@@ -343,7 +347,7 @@ mod test {
"{}",
ServerInfo {
name: "name".to_string(),
features: plain.clone(),
features: plain,
}
),
"name with {Authentication(Plain)}".to_string()

View File

@@ -8,8 +8,8 @@
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * 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
//!
@@ -17,12 +17,12 @@
//!
//! It is designed to be:
//!
//! * Secured: email are encrypted by default
//! * Modern: unicode support for email content and sender/recipient addresses when compatible
//! * 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.
//! emails directly to the destination server.
//!
//! The relay server can be the local email server, a specific host or a third-party service.
//!
@@ -31,181 +31,138 @@
//! This is the most basic example of usage:
//!
//! ```rust,no_run
//! # #[cfg(feature = "smtp-transport")]
//! # {
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//! .body(String::from("Be happy!"))?;
//!
//! // Create local transport on port 25
//! let sender = SmtpTransport::unencrypted_localhost();
//! // Send the email on local relay
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! .build();
//! // Send the email via remote relay
//! let result = sender.send(&email);
//!
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! #### Complete example
//! #### Authentication
//!
//! ```todo
//! # #[cfg(feature = "smtp-transport")]
//! # {
//! use lettre::transport::smtp::authentication::{Credentials, Mechanism};
//! use lettre::{Email, Envelope, Transport, SmtpClient};
//! use lettre::transport::smtp::extension::ClientId;
//! Example with authentication and connection pool:
//!
//! 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(),
//! );
//! ```rust,no_run
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::{PoolConfig, authentication::{Credentials, Mechanism}}};
//!
//! 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(),
//! );
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! // 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()))
//! // Create TLS transport on port 587 with STARTTLS
//! let sender = SmtpTransport::starttls_relay("smtp.example.com")?
//! // Add credentials for authentication
//! .credentials(Credentials::new("username".to_string(), "password".to_string()))
//! // 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();
//! .authentication(vec![Mechanism::Plain])
//! // Connection pool settings
//! .pool_config( PoolConfig::new().max_size(20))
//! .build();
//!
//! 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();
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
//! You can specify custom TLS settings:
//!
//! ```todo
//! # #[cfg(feature = "native-tls")]
//! # {
//! use lettre::{
//! ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
//! Email, SmtpClient, Transport,
//! };
//! use lettre::transport::smtp::authentication::{Credentials, Mechanism};
//! use lettre::transport::smtp::ConnectionReuseParameters;
//! use native_tls::{Protocol, TlsConnector};
//!
//! let email = Email::new(
//! Envelope::new(
//! Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
//! vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
//! ).unwrap(),
//! "message_id".to_string(),
//! "Hello world".to_string().into_bytes(),
//! );
//!
//! let mut tls_builder = TlsConnector::builder();
//! tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
//! let tls_parameters =
//! ClientTlsParameters::new(
//! "smtp.example.com".to_string(),
//! tls_builder.build().unwrap()
//! );
//!
//! let mut mailer = SmtpClient::new(
//! ("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
//! ).unwrap()
//! .authentication_mechanism(Mechanism::Login)
//! .credentials(Credentials::new(
//! "example_username".to_string(), "example_password".to_string()
//! ))
//! .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
//! .transport();
//!
//! let result = mailer.send(&email);
//!
//! assert!(result.is_ok());
//!
//! mailer.close();
//! # }
//! ```
//!
//! #### Lower level
//!
//! You can also send commands, here is a simple email transaction without
//! error handling:
//!
//! ```rust,no_run
//! # #[cfg(feature = "smtp-transport")]
//! # {
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection};
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
//! use lettre::{Message, Transport, SmtpTransport, transport::smtp::client::{TlsParameters, Tls}};
//!
//! let hello = ClientId::new("my_hostname".to_string());
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None).unwrap();
//! client.command(
//! Mail::new(Some("user@example.com".parse().unwrap()), vec![])
//! ).unwrap();
//! client.command(
//! Rcpt::new("user@example.org".parse().unwrap(), vec![])
//! ).unwrap();
//! client.command(Data).unwrap();
//! client.message("Test email".as_bytes()).unwrap();
//! client.command(Quit).unwrap();
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_string())
//! .dangerous_accept_invalid_certs(true).build()?;
//!
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! // Custom TLS configuration
//! .tls(Tls::Required(tls))
//! .build();
//!
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
pub use self::async_transport::{
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder,
};
#[cfg(feature = "r2d2")]
pub use self::pool::PoolConfig;
#[cfg(feature = "r2d2")]
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::net::TlsParameters;
use crate::{
transport::smtp::{
use crate::transport::smtp::client::TlsParameters;
use crate::transport::smtp::{
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
client::SmtpConnection,
error::Error,
extension::ClientId,
response::Response,
},
Envelope, Transport,
};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "r2d2")]
use r2d2::{Builder, Pool};
#[cfg(feature = "rustls-tls")]
use rustls::ClientConfig;
use client::Tls;
use std::time::Duration;
#[cfg(feature = "rustls-tls")]
use webpki_roots::TLS_SERVER_ROOTS;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(feature = "async-std1")]
pub use self::async_transport::AsyncStd1Connector;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(feature = "tokio02")]
pub use self::async_transport::Tokio02Connector;
#[doc(hidden)]
#[allow(deprecated)]
#[cfg(feature = "tokio1")]
pub use self::async_transport::Tokio1Connector;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
mod async_transport;
pub mod authentication;
pub mod client;
pub mod commands;
pub mod error;
mod error;
pub mod extension;
#[cfg(feature = "r2d2")]
pub mod pool;
mod pool;
pub mod response;
mod transport;
pub mod util;
// Registered port numbers:
@@ -218,112 +175,12 @@ pub const SMTP_PORT: u16 = 25;
pub const SUBMISSION_PORT: u16 = 587;
/// Default submission over TLS port
///
/// https://tools.ietf.org/html/rfc8314
/// Defined in [RFC8314](https://tools.ietf.org/html/rfc8314)
pub const SUBMISSIONS_PORT: u16 = 465;
/// Default timeout
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
/// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
// This is also rustls' default behavior
#[cfg(feature = "native-tls")]
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_debug_implementations, missing_copy_implementations)]
pub enum Tls {
/// Insecure connection only (for testing purposes)
None,
/// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Opportunistic(TlsParameters),
/// Start with insecure connection and require `STARTTLS`
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Required(TlsParameters),
/// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Wrapper(TlsParameters),
}
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool<SmtpClient>,
#[cfg(not(feature = "r2d2"))]
inner: SmtpClient,
}
impl Transport for SmtpTransport {
type Ok = Response;
type Error = Error;
/// Sends an email
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "r2d2")]
let mut conn = self.inner.get()?;
#[cfg(not(feature = "r2d2"))]
let mut conn = self.inner.connection()?;
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "r2d2"))]
conn.quit()?;
Ok(result)
}
}
impl SmtpTransport {
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No authentication
/// * A 60 seconds timeout for smtp commands
/// * Port 587
///
/// Consider using [`SmtpTransport::new`] instead, if possible.
pub fn builder<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let mut new = SmtpInfo::default();
new.server = server.into();
SmtpTransportBuilder { info: new }
}
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
#[cfg(feature = "native-tls")]
let mut tls_builder = TlsConnector::builder();
#[cfg(feature = "native-tls")]
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
#[cfg(feature = "native-tls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls_builder.build()?);
#[cfg(feature = "rustls-tls")]
let mut tls = ClientConfig::new();
#[cfg(feature = "rustls-tls")]
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
#[cfg(feature = "rustls-tls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls);
Ok(Self::builder(relay)
.port(SUBMISSIONS_PORT)
.tls(Tls::Wrapper(tls_parameters)))
}
/// Creates a new local SMTP client to port 25
///
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
pub fn unencrypted_localhost() -> SmtpTransport {
Self::builder("localhost").port(SMTP_PORT).build()
}
}
#[allow(missing_debug_implementations)]
#[derive(Clone)]
struct SmtpInfo {
@@ -348,8 +205,8 @@ impl Default for SmtpInfo {
fn default() -> Self {
Self {
server: "localhost".to_string(),
port: SUBMISSION_PORT,
hello_name: ClientId::hostname(),
port: SMTP_PORT,
hello_name: ClientId::default(),
credentials: None,
authentication: DEFAULT_MECHANISMS.into(),
timeout: Some(DEFAULT_TIMEOUT),
@@ -357,122 +214,3 @@ impl Default for SmtpInfo {
}
}
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpTransportBuilder {
info: SmtpInfo,
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder {
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self {
self.info.hello_name = name;
self
}
/// Set the authentication mechanism to use
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.info.credentials = Some(credentials);
self
}
/// Set the authentication mechanism to use
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
self.info.authentication = mechanisms;
self
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
self.info.timeout = timeout;
self
}
/// Set the port to use
pub fn port(mut self, port: u16) -> Self {
self.info.port = port;
self
}
/// Set the TLS settings to use
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls;
self
}
/// Build the client
fn build_client(self) -> SmtpClient {
SmtpClient { info: self.info }
}
/// Build the transport with custom pool settings
#[cfg(feature = "r2d2")]
pub fn build_with_pool(self, pool: Builder<SmtpClient>) -> SmtpTransport {
let pool = pool.build_unchecked(self.build_client());
SmtpTransport { inner: pool }
}
/// Build the transport (with default pool if enabled)
pub fn build(self) -> SmtpTransport {
let client = self.build_client();
SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool::builder().max_size(5).build_unchecked(client),
#[cfg(not(feature = "r2d2"))]
inner: client,
}
}
}
/// Build client
#[derive(Clone)]
pub struct SmtpClient {
info: SmtpInfo,
}
impl SmtpClient {
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
pub fn connection(&self) -> Result<SmtpConnection, Error> {
let mut conn = SmtpConnection::connect::<(&str, u16)>(
(self.info.server.as_ref(), self.info.port),
self.info.timeout,
&self.info.hello_name,
#[allow(clippy::match_single_binding)]
match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
},
)?;
#[allow(clippy::match_single_binding)]
match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters, &self.info.hello_name)?;
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters, &self.info.hello_name)?;
}
_ => (),
}
match &self.info.credentials {
Some(credentials) => {
conn.auth(self.info.authentication.as_slice(), &credentials)?;
}
None => (),
}
Ok(conn)
}
}

View File

@@ -1,5 +1,78 @@
use std::time::Duration;
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
use r2d2::ManageConnection;
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 {
/// Create a new pool configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Minimum number of idle connections
///
/// Defaults to `0`
pub fn min_idle(mut self, min_idle: u32) -> Self {
self.min_idle = min_idle;
self
}
/// Maximum number of pooled connections
///
/// Defaults to `10`
pub fn max_size(mut self, max_size: u32) -> Self {
self.max_size = max_size;
self
}
/// Connection timeout
///
/// Defaults to `30 seconds`
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
self.connection_timeout = connection_timeout;
self
}
/// Connection idle timeout
///
/// Defaults to `60 seconds`
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.idle_timeout = idle_timeout;
self
}
pub(crate) fn build<C: ManageConnection>(&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;

View File

@@ -32,7 +32,7 @@ pub enum Severity {
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
@@ -56,7 +56,7 @@ pub enum Category {
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter) -> Result {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
@@ -88,7 +88,7 @@ pub enum Detail {
}
impl Display for Detail {
fn fmt(&self, f: &mut Formatter) -> Result {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", *self as u8)
}
}
@@ -106,7 +106,7 @@ pub struct Code {
}
impl Display for Code {
fn fmt(&self, f: &mut Formatter) -> Result {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}{}{}", self.severity, self.category, self.detail)
}
}
@@ -151,10 +151,10 @@ impl Response {
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.code.severity {
Severity::PositiveCompletion | Severity::PositiveIntermediate => true,
_ => false,
}
matches!(
self.code.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
}
/// Tests code equality
@@ -237,25 +237,22 @@ pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
let (i, _) = complete(tag("\r\n"))(i)?;
// Check that all codes are equal.
if !lines.iter().all(|&(ref code, _, _)| *code == last_code) {
return Err(nom::Err::Failure(("", nom::error::ErrorKind::Not)));
if !lines.iter().all(|&(code, _, _)| code == last_code) {
return Err(nom::Err::Failure(nom::error::Error::new(
"",
nom::error::ErrorKind::Not,
)));
}
// Extract text from lines, and append last line.
let mut lines: Vec<&str> = lines
.into_iter()
.map(|(_, text, _)| text)
.collect::<Vec<_>>();
lines.push(last_line);
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
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>(),
message: lines,
},
))
}

View File

@@ -0,0 +1,230 @@
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, Transport};
#[allow(missing_debug_implementations)]
#[derive(Clone)]
/// Transport using the SMTP protocol
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 communicate with the SMTP server
///
/// The right option for most SMTP servers.
///
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
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 new = SmtpInfo {
server: server.into(),
..Default::default()
};
SmtpTransportBuilder {
info: new,
#[cfg(feature = "r2d2")]
pool_config: PoolConfig::default(),
}
}
}
/// Contains client configuration.
/// Instances of this struct can be created using functions of [`SmtpTransport`].
#[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

@@ -8,7 +8,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut rest = self.0;
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
let (start, end) = rest.split_at(idx);

View File

@@ -7,22 +7,32 @@
//! testing purposes.
//!
//! ```rust
//! use lettre::{Message, Envelope, Transport, StubTransport};
//! # #[cfg(feature = "builder")]
//! # {
//! use lettre::{transport::stub::StubTransport, Message, Transport};
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse().unwrap())
//! .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
//! .to("Hei <hei@domain.tld>".parse().unwrap())
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")
//! .unwrap();
//! .body(String::from("Be happy!"))?;
//!
//! let mut sender = StubTransport::new_ok();
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! # }
//! ```
use crate::{Envelope, Transport};
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait;
use std::{error::Error as StdError, fmt};
#[derive(Debug, Copy, Clone)]
@@ -30,15 +40,11 @@ pub struct Error;
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "stub error")
f.write_str("stub error")
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
None
}
}
impl StdError for Error {}
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
@@ -47,7 +53,7 @@ pub struct StubTransport {
}
impl StubTransport {
/// Creates aResult new transport that always returns the given response
/// Creates a new transport that always returns the given Result
pub fn new(response: Result<(), Error>) -> StubTransport {
StubTransport { response }
}
@@ -74,23 +80,13 @@ impl Transport for StubTransport {
}
}
#[cfg(feature = "async")]
pub mod r#async {
use super::StubTransport;
use crate::{r#async::Transport, transport::stub::Error, Envelope};
use async_trait::async_trait;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[async_trait]
impl Transport for StubTransport {
impl AsyncTransport for StubTransport {
type Ok = ();
type Error = Error;
async fn send_raw(
&self,
_envelope: &Envelope,
_email: &[u8],
) -> Result<Self::Ok, Self::Error> {
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.response
}
}
}

233
testdata/email_with_png.eml vendored Normal file
View File

@@ -0,0 +1,233 @@
Date: Tue, 15 Nov 1994 08:12:31 GMT
From: NoBody <nobody@domain.tld>
Reply-To: Yuin <yuin@domain.tld>
To: Hei <hei@domain.tld>
Subject: Happy new year
MIME-Version: 1.0
Content-Type: multipart/related; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
Content-Type: text/html; charset=utf8
Content-Transfer-Encoding: 7bit
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
Content-Type: image/png
Content-Disposition: inline
Content-ID: <123>
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAACXBIWXMAASdGAAEnRgHWSSfaAAAA
GXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJzs3WmMlfXd//HvzLAO
izAORCmKgKmKKQa1taUWl1C1cWm1rVvdSls01bbGpcsDTWOrMcabdNNiXdqkrSamMbY2hkTbOiyD
0HFBiRSLE6WFKgoFFIRBztwP7M2//gEVnJnf95zzeiUmLjDziYlnztvrd12noaOjozsAAACAohpL
DwAAAAAEOgAAAKQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ9Cs9ADL6wx/+EHfffXfpGQB7ZNiw
YfGb3/ym9AwAYC8JdNiFN954I1atWlV6BsAeGT58eOkJAMAH4Ig7AAAAJCDQAQAAIAGBDgAAAAkI
dAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhA
oAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEAC
Ah0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAAS
EOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQ
gEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACA
BAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAA
JCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAA
IAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAA
AAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAA
AEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMA
AEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0A
AAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgA
AACQgEAHAACABAQ6AAAAJCDQAQAAIIF+pQdARieeeGIcfPDBpWdQJ55//vn40Y9+VHoGAACFCXTY
hf322y/222+/0jOoE01NTaUnAACQgCPuAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
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
--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--

View File

@@ -1,13 +1,15 @@
#[cfg(test)]
#[cfg(feature = "file-transport")]
#[cfg(all(feature = "file-transport", feature = "builder"))]
mod test {
use lettre::{transport::file::FileTransport, Message};
use std::{
env::temp_dir,
fs::{remove_file, File},
io::Read,
fs::{read_to_string, remove_file},
};
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
#[test]
fn file_transport() {
use lettre::Transport;
@@ -18,48 +20,153 @@ mod test {
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(&email);
let id = result.unwrap();
let file = temp_dir().join(format!("{}.json", id));
let mut f = File::open(file.clone()).unwrap();
let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer);
let eml_file = temp_dir().join(format!("{}.eml", id));
let eml = read_to_string(&eml_file).unwrap();
assert_eq!(
buffer,
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
remove_file(file).unwrap();
eml,
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
#[cfg(feature = "async")]
#[async_attributes::test]
async fn file_transport_async() {
use lettre::r#async::Transport;
let sender = FileTransport::new(temp_dir());
#[test]
#[cfg(feature = "file-transport-envelope")]
fn file_transport_with_envelope() {
use lettre::Transport;
let sender = FileTransport::with_envelope(temp_dir());
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(&email);
let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id));
let eml = read_to_string(&eml_file).unwrap();
let json_file = temp_dir().join(format!("{}.json", id));
let json = read_to_string(&json_file).unwrap();
assert_eq!(
eml,
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
assert_eq!(
json,
"{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"}"
);
let (e, m) = sender.read(&id).unwrap();
assert_eq!(&e, email.envelope());
assert_eq!(m, email.formatted());
remove_file(eml_file).unwrap();
remove_file(json_file).unwrap();
}
#[cfg(feature = "async-std1")]
#[async_std::test]
async fn file_transport_asyncstd1() {
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport};
let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("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);
let eml_file = temp_dir().join(format!("{}.eml", id));
let eml = read_to_string(&eml_file).unwrap();
assert_eq!(
buffer,
"{\"envelope\":{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"},\"raw_message\":null,\"message\":\"From: NoBody <nobody@domain.tld>\\r\\nReply-To: Yuin <yuin@domain.tld>\\r\\nTo: Hei <hei@domain.tld>\\r\\nSubject: Happy new year\\r\\nDate: Tue, 15 Nov 1994 08:12:31 GMT\\r\\n\\r\\nBe happy!\"}");
remove_file(file).unwrap();
eml,
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn file_transport_tokio02() {
use lettre::{AsyncFileTransport, AsyncTransport, Tokio02Executor};
let sender = AsyncFileTransport::<Tokio02Executor>::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(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id));
let eml = read_to_string(&eml_file).unwrap();
assert_eq!(
eml,
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
}

View File

@@ -1,8 +1,11 @@
#[cfg(test)]
#[cfg(feature = "sendmail-transport")]
#[cfg(all(feature = "sendmail-transport", feature = "builder"))]
mod test {
use lettre::{transport::sendmail::SendmailTransport, Message};
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
#[test]
fn sendmail_transport() {
use lettre::Transport;
@@ -12,7 +15,7 @@ mod test {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(&email);
@@ -20,21 +23,43 @@ mod test {
assert!(result.is_ok());
}
#[cfg(feature = "async")]
#[async_attributes::test]
async fn sendmail_transport_async() {
use lettre::r#async::Transport;
let sender = SendmailTransport::new();
#[cfg(feature = "async-std1")]
#[async_std::test]
async fn sendmail_transport_asyncstd1() {
use lettre::{AsyncSendmailTransport, AsyncStd1Executor, AsyncTransport};
let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
println!("{:?}", result);
assert!(result.is_ok());
}
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn sendmail_transport_tokio02() {
use lettre::{AsyncSendmailTransport, Tokio02Executor, Tokio02Transport};
let sender = AsyncSendmailTransport::<Tokio02Executor>::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(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
println!("{:?}", result);
assert!(result.is_ok());
}
}

View File

@@ -1,5 +1,5 @@
#[cfg(test)]
#[cfg(feature = "smtp-transport")]
#[cfg(all(feature = "smtp-transport", feature = "builder"))]
mod test {
use lettre::{Message, SmtpTransport, Transport};
@@ -10,9 +10,9 @@ mod test {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
SmtpTransport::builder("127.0.0.1")
SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build()
.send(&email)

View File

@@ -1,7 +1,7 @@
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
mod test {
use lettre::{Envelope, SmtpTransport, Transport};
use r2d2::Pool;
use lettre::{address::Envelope, SmtpTransport, Transport};
use std::{sync::mpsc, thread};
fn envelope() -> Envelope {
@@ -14,10 +14,9 @@ mod test {
#[test]
fn send_one() {
let pool = Pool::builder().max_size(1);
let mailer = SmtpTransport::builder("127.0.0.1")
let mailer = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build_with_pool(pool);
.build();
let result = mailer.send_raw(&envelope(), b"test");
assert!(result.is_ok());
@@ -25,11 +24,9 @@ mod test {
#[test]
fn send_from_thread() {
let pool = Pool::builder().max_size(1);
let mailer = SmtpTransport::builder("127.0.0.1")
let mailer = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build_with_pool(pool);
.build();
let (s1, r1) = mpsc::channel();
let (s2, r2) = mpsc::channel();

View File

@@ -1,5 +1,11 @@
#[cfg(test)]
#[cfg(feature = "builder")]
mod test {
use lettre::{transport::stub::StubTransport, Message};
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
#[test]
fn stub_transport() {
use lettre::Transport;
@@ -10,17 +16,18 @@ fn stub_transport() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err();
}
#[cfg(feature = "async")]
#[async_attributes::test]
async fn stub_transport_async() {
use lettre::r#async::Transport;
#[cfg(feature = "async-std1")]
#[async_std::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()
@@ -29,9 +36,30 @@ async fn stub_transport_async() {
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.body(String::from("Be happy!"))
.unwrap();
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(String::from("Be happy!"))
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
}
}