Compare commits

..

77 Commits

Author SHA1 Message Date
Alexis Mousset
f17dccc46d builder: Fix Message-ID header (#614) 2021-05-04 18:45:29 +02:00
Paolo Barbolini
7e7f05eb45 Prepare 0.10.0-beta.4 (#613) 2021-05-04 18:31:55 +02:00
Paolo Barbolini
99df9e8d7c Headers insert_raw -> append_raw, set_raw -> insert_raw (#612) 2021-05-04 18:19:21 +02:00
Paolo Barbolini
1b5109b6ac Add docs to Headers (#610) 2021-05-02 11:10:04 +02:00
Alexis Mousset
a4be3c4cd8 Add InvalidHeaderName error (#608)
* Add InvalidHeaderName error
2021-05-01 22:00:33 +02:00
Paolo Barbolini
4586f2ad8a Remove useless clones (#609) 2021-05-01 18:22:53 +02:00
Paolo Barbolini
31de9e508b Replace hyperx Header and Headers with our own implementation (#607)
* Replace hyperx Header and Headers with our own implementation

* Remove utf8_b

* Add RFC 1522 encoder

* Fix most tests

* Throw away old tests

* Header encoding tests

* Fix slicing in the middle of a char

* Content-Disposition after rebase

* Fix the rest of the tests

* Fix useless clone clippy warnings

* Remove Headers::get_raw_mut

* HeaderName::new_from_ascii fallible API

* Tidy up HeaderName::new_from_ascii_str

* HeaderName::new_from_ascii(_str) tests
2021-05-01 13:27:00 +02:00
Paolo Barbolini
69334fe5eb Replace the hyperx ContentDisposition header with our own implementation (#601) 2021-04-24 18:21:29 +02:00
Paolo Barbolini
2ad2444183 Replace the hyperx ContentLocation header with our own implementation (#603) 2021-04-24 18:00:36 +02:00
Paolo Barbolini
8afa442e93 Add missing doc(cfg(..)) attributes (#604) 2021-04-20 19:09:28 +02:00
Paolo Barbolini
486e0f9d50 Replace hyperx ContentType header with our own implementation (#598)
* Replace hyperx ContentType header with our own implementation

* Let's not forget ContentTypeErr

* Adress code review comment
2021-04-08 08:40:07 +00:00
Paolo Barbolini
acc4ff4898 Replace hyperx Date header with our own implementation (#597) 2021-04-08 07:55:20 +02:00
Paolo Barbolini
1728d57c34 Stop using the uuid crate for generating the Message-Id (#602) 2021-04-07 18:38:56 +00:00
Alex Wennerberg
53bfb65423 Replace rand with fastrand (#600)
We don't need cryptographically secure random numbers, this simplifies
the dependency tree and speeds up builds.
2021-04-06 21:32:43 +02:00
Paolo Barbolini
61b08814c9 Avoid useless allocations while formatting headers (#599) 2021-04-06 17:02:37 +00:00
Paolo Barbolini
0e74042b4e Convert String Body line-endings to CRLF (#588) 2021-04-01 12:29:51 +02:00
Paolo Barbolini
29affe9398 Seal header contents (#591) 2021-04-01 12:18:38 +02:00
Jupp56
b10f6ff8de Fix: example does not compile (#592)
A missing simple string conversion prevented the example code from compiling.
2021-03-31 12:14:49 +02:00
Paolo Barbolini
2002a9d75a tls: use rustls if both native-tls and rustls-tls are enabled (#586)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-03-30 09:06:26 +00:00
Paolo Barbolini
1193e1134d Bump MSRV to 1.46.0 (#587) 2021-03-30 08:40:05 +00:00
Alexis Mousset
7c6ade7afe Add a get started doc for transports (#577)
* Add a get started doc for transports

This will help users not familiar with email infrastructure.
2021-03-19 08:34:13 +00:00
Paolo Barbolini
3bc729ca64 Remove MockStream and all internal uses of it (#580)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-03-19 08:20:05 +00:00
Paolo Barbolini
f041c00df7 Fix a few clippy warnings (#579) 2021-03-19 08:03:41 +00:00
Alexis Mousset
fe8dc4967d Prepare 0.10.0-beta.3 (#578) 2021-03-18 20:28:02 +00:00
Paolo Barbolini
137566a4e4 Improve docs in lib.rs (#574)
* Improve docs in lib.rs

* Typos
2021-03-18 08:02:21 +00:00
Paolo Barbolini
216c612931 Fix missing re-export of AsyncSmtpTransport when using async-std1 feature (#573) 2021-03-17 06:52:57 +00:00
Paolo Barbolini
a429a24913 Add missing Debug implementations (#570) 2021-03-14 10:17:07 +01:00
Paolo Barbolini
648bf2b2f6 chore: remove some uses of * (#569) 2021-03-14 09:22:59 +01:00
Alexis Mousset
509a623a27 feat(transport): Seal file and sendmail error types (#567) 2021-03-14 07:42:52 +00:00
Paolo Barbolini
a681c6b49d Remove From implementations on Error for file and sendmail transport (#566) 2021-03-13 17:41:44 +00:00
Alexis Mousset
22efe341fe feat(builder): Seal SMTP error type (#564)
* feat(builder): Seal SMTP error type

* More precise error types
2021-03-13 17:15:21 +00:00
Paolo Barbolini
97fba6a47e docs: improve docs for lettre::transport (#565) 2021-03-13 16:03:05 +00:00
Paolo Barbolini
f7066ac858 Fix various parts of the docs (#563) 2021-03-12 20:02:31 +01:00
Alexis Mousset
9379f2e328 Prepare 0.10.0-beta.2 2021-03-10 21:56:00 +00:00
Paolo Barbolini
05133a7102 Test using all supported async executors 2021-03-08 13:23:27 +01:00
Paolo Barbolini
d7d05bf48a Update deprecated imports 2021-03-08 10:59:28 +01:00
Paolo Barbolini
34ac265d60 Remove deprecated executor methods 2021-03-08 10:59:28 +01:00
Hari Konomi
bbf56de83d feat(transport-smtp): Call conn.quit() when pooled conns are released (#559)
* Implement CustomizeConnection::on_release() for SmtpConnection
2021-03-05 19:02:01 +01:00
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
69 changed files with 5647 additions and 2479 deletions

View File

@@ -6,6 +6,9 @@ on:
branches:
- master
env:
RUST_BACKTRACE: full
jobs:
rustfmt:
name: rustfmt / stable
@@ -76,8 +79,8 @@ jobs:
rust: stable
- name: beta
rust: beta
- name: 1.45.2
rust: 1.45.2
- name: 1.46.0
rust: 1.46.0
steps:
- name: Checkout

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,33 +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
* Move CI to Github Actions ([3eef024](https://github.com/lettre/lettre/commit/3eef024))
* MSRV is now 1.36 ([d227cd4](https://github.com/lettre/lettre/commit/d227cd4))
* Merged `lettre_email` into `lettre` ([0f3f27f](https://github.com/lettre/lettre/commit/0f3f27f))
* Rename `serde-impls` feature to `serde` ([aac3e00](https://github.com/lettre/lettre/commit/aac3e00))
* Use criterion for benchmarks ([eda7fc1](https://github.com/lettre/lettre/commit/eda7fc1))
* Update to nom 5 ([5bc1cba](https://github.com/lettre/lettre/commit/5bc1cba))
* Change website url schemes to https ([6014f5c](https://github.com/lettre/lettre/commit/6014f5c))
* Use serde's `derive` feature instead of the `serde_derive` crate ([4fbe700](https://github.com/lettre/lettre/commit/4fbe700))
* Merge `Email` and `SendableEmail` into `lettre::Email` ([ce37464](https://github.com/lettre/lettre/commit/ce37464))
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as
EHLO parameter (for better RFC compliance and mail server compatibility)
* 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`
#### 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

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-alpha.4"
version = "0.10.0-beta.4"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -22,46 +22,47 @@ idna = "0.2"
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
# builder
hyperx = { version = "1", optional = true, features = ["headers"] }
mime = { version = "0.3", optional = true }
uuid = { version = "0.8", features = ["v4"] }
rand = { version = "0.7", optional = true }
httpdate = { version = "1", optional = true }
mime = { version = "0.3.4", optional = true }
fastrand = { version = "1.4", optional = true }
quoted_printable = { version = "0.4", optional = true }
base64 = { version = "0.13", optional = true }
once_cell = "1"
regex = "1"
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
# file transport
uuid = { version = "0.8", features = ["v4"], optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
# smtp
nom = { version = "6", default-features = false, features = ["alloc"], optional = true }
nom = { version = "6", default-features = false, features = ["alloc", "std"], optional = true }
r2d2 = { version = "0.8", optional = true } # feature
hostname = { version = "0.3", optional = true } # feature
## tls
native-tls = { version = "0.2", optional = true } # feature
rustls = { version = "0.18", features = ["dangerous_configuration"], optional = true }
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true }
webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.20", optional = true }
webpki-roots = { version = "0.21", optional = true }
# async
futures-io = { version = "0.3.7", optional = true }
futures-util = { version = "0.3.7", features = ["io"], 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-attributes = { version = "1.1", optional = true }
async-std = { version = "1.5", optional = true, features = ["unstable"] }
async-trait = { version = "0.1", optional = true }
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.14", optional = true }
tokio03_crate = { package = "tokio", version = "0.3", features = ["fs", "process", "net", "io-util"], optional = true }
tokio03_native_tls_crate = { package = "tokio-native-tls", version = "0.2", optional = true }
tokio03_rustls = { package = "tokio-rustls", version = "0.20", optional = true }
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"
@@ -69,37 +70,50 @@ tracing-subscriber = "0.2.10"
glob = "0.3"
walkdir = "2"
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
tokio03_crate = { package = "tokio", version = "0.3", features = ["macros", "rt-multi-thread"] }
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]
default = ["file-transport", "smtp-transport", "native-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"]
builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable"]
# transports
file-transport = ["serde", "serde_json"]
file-transport = ["uuid"]
file-transport-envelope = ["serde", "serde_json", "file-transport"]
sendmail-transport = []
smtp-transport = ["base64", "nom"]
rustls-tls = ["webpki", "webpki-roots", "rustls"]
# async
async-std1 = ["async-std", "async-trait", "async-attributes"]
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"]
tokio03 = ["tokio03_crate", "async-trait", "futures-io", "futures-util"]
tokio03-native-tls = ["tokio03", "native-tls", "tokio03_native_tls_crate"]
tokio03-rustls-tls = ["tokio03", "rustls-tls", "tokio03_rustls"]
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", "builder"]
@@ -125,10 +139,17 @@ name = "tokio02_smtp_starttls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]]
name = "tokio03_smtp_tls"
required-features = ["smtp-transport", "tokio03", "tokio03-native-tls", "builder"]
name = "tokio1_smtp_tls"
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
[[example]]
name = "tokio03_smtp_starttls"
required-features = ["smtp-transport", "tokio03", "tokio03-native-tls", "builder"]
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

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-alpha.4">
<img src="https://deps.rs/crate/lettre/0.10.0-alpha.4/status.svg"
<a href="https://deps.rs/crate/lettre/0.10.0-beta.4">
<img src="https://deps.rs/crate/lettre/0.10.0-beta.4/status.svg"
alt="dependency status" />
</a>
</div>
@@ -52,7 +52,7 @@ Lettre provides the following features:
* Unicode support (for email content and addresses)
* Secure delivery with SMTP using encryption and authentication
* Easy email builders
* Async support (incomplete)
* Async support
Lettre does not provide (for now):
@@ -60,13 +60,13 @@ Lettre does not provide (for now):
## Example
This library requires Rust 1.45 or newer.
This library requires Rust 1.46 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.10.0-alpha.4"
lettre = "0.10.0-beta.4"
```
```rust,no_run
@@ -78,7 +78,7 @@ let email = Message::builder()
.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("smtp_username".to_string(), "smtp_password".to_string());

View File

@@ -13,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());
@@ -32,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

View File

@@ -2,14 +2,22 @@
This folder contains examples showing how to use lettre in your own projects.
## Examples
## 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_` or `tokio03_`.
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

View File

@@ -0,0 +1,32 @@
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
AsyncTransport, 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,
AsyncTransport, 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),
}
}

49
examples/basic_html.rs Normal file
View File

@@ -0,0 +1,49 @@
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)
.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)
.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");
}

58
examples/maud_html.rs Normal file
View File

@@ -0,0 +1,58 @@
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)
.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)
.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

@@ -8,7 +8,7 @@ fn main() {
.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

View File

@@ -1,8 +1,10 @@
use std::fs;
use lettre::{
transport::smtp::authentication::Credentials,
transport::smtp::client::{Certificate, Tls, TlsParameters},
transport::smtp::{
authentication::Credentials,
client::{Certificate, Tls, TlsParameters},
},
Message, SmtpTransport, Transport,
};
@@ -14,15 +16,16 @@ fn main() {
.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();
// Use a custom certificate stored on disk to securely verify the server's certificate
let pem_cert = fs::read("certificate.pem").unwrap();
let cert = Certificate::from_pem(&pem_cert).unwrap();
let mut tls = TlsParameters::builder("smtp.server.com".to_string());
tls.add_root_certificate(cert);
let tls = tls.build().unwrap();
let 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());

View File

@@ -8,7 +8,7 @@ fn main() {
.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("smtp_username".to_string(), "smtp_password".to_string());

View File

@@ -8,7 +8,7 @@ fn main() {
.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("smtp_username".to_string(), "smtp_password".to_string());

View File

@@ -4,8 +4,8 @@
use tokio02_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
Tokio02Transport,
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio02Executor,
};
#[tokio::main]
@@ -17,16 +17,17 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.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::<Tokio02Connector>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
let mailer: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {

View File

@@ -4,8 +4,8 @@
use tokio02_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
Tokio02Transport,
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio02Executor,
};
#[tokio::main]
@@ -17,16 +17,17 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.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::<Tokio02Connector>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
let mailer: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {

View File

@@ -1,11 +1,11 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio03_crate as tokio;
use tokio1_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio03Connector,
Tokio03Transport,
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor,
};
#[tokio::main]
@@ -17,16 +17,17 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.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::<Tokio03Connector>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
let mailer: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {

View File

@@ -1,11 +1,11 @@
// This line is only to make it compile from lettre's examples folder,
// since it uses Rust 2018 crate renaming to import tokio.
// Won't be needed in user's code.
use tokio03_crate as tokio;
use tokio1_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio03Connector,
Tokio03Transport,
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor,
};
#[tokio::main]
@@ -17,16 +17,17 @@ async fn main() {
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new async year")
.body("Be happy with async!")
.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::<Tokio03Connector>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
let mailer: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {

View File

@@ -14,7 +14,7 @@ use crate::Error;
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Envelope {
/// The envelope recipients' addresses
/// The envelope recipient's addresses
///
/// This can not be empty.
forward_path: Vec<Address>,
@@ -27,15 +27,12 @@ impl Envelope {
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// ```rust
/// # use lettre::address::{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 sender = "sender@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients);
/// # Ok(())
@@ -59,15 +56,12 @@ impl Envelope {
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// ```rust
/// # use lettre::address::{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 sender = "from@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
/// assert_eq!(envelope.to(), recipients.as_slice());
@@ -82,15 +76,12 @@ impl Envelope {
///
/// # Examples
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// # use lettre::address::Envelope;
///
/// ```rust
/// # use lettre::address::{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 sender = "from@email.com".parse::<Address>()?;
/// let recipients = vec!["to@email.com".parse::<Address>()?];
///
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
/// assert!(envelope.from().is_some());
@@ -103,6 +94,15 @@ impl Envelope {
pub fn from(&self) -> Option<&Address> {
self.reverse_path.as_ref()
}
#[cfg(feature = "smtp-transport")]
/// 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")]
@@ -112,15 +112,16 @@ impl TryFrom<&Headers> for Envelope {
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()),
Some(sender) => Some(Mailbox::from(sender).email),
// ... else try From
None => match headers.get::<header::From>() {
Some(header::From(a)) => {
let from: Vec<Mailbox> = a.clone().into();
let mut from: Vec<Mailbox> = a.into();
if from.len() > 1 {
return Err(Error::TooManyFrom);
}
Some(from[0].email.clone())
let from = from.pop().expect("From header has 1 Mailbox");
Some(from.email)
}
None => None,
},
@@ -128,18 +129,16 @@ impl TryFrom<&Headers> for Envelope {
fn add_addresses_from_mailboxes(
addresses: &mut Vec<Address>,
mailboxes: Option<&Mailboxes>,
mailboxes: Option<Mailboxes>,
) {
if let Some(mailboxes) = mailboxes {
for mailbox in mailboxes.iter() {
addresses.push(mailbox.email.clone());
}
addresses.extend(mailboxes.into_iter().map(|mb| mb.email));
}
}
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));
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)
}

View File

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

View File

@@ -25,7 +25,7 @@ impl<'de> Deserialize<'de> for Address {
enum Field {
User,
Domain,
};
}
const FIELDS: &[&str] = &["user", "domain"];

View File

@@ -23,10 +23,13 @@ use std::{
/// You can create an `Address` from a user and a domain:
///
/// ```
/// # use lettre::Address;
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.user(), "user");
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
@@ -34,11 +37,13 @@ use std::{
/// You can also create an `Address` from a string literal by parsing it:
///
/// ```
/// use std::str::FromStr;
/// # use lettre::Address;
/// use lettre::Address;
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::from_str("example@email.com")?;
/// let address = "user@email.com".parse::<Address>()?;
/// assert_eq!(address.user(), "user");
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
@@ -50,28 +55,6 @@ pub struct Address {
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
@@ -96,8 +79,8 @@ impl 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()?;
/// let address = Address::new("user", "email.com")?;
/// let expected = "user@email.com".parse::<Address>()?;
/// assert_eq!(expected, address);
/// # Ok(())
/// # }
@@ -115,8 +98,8 @@ impl Address {
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// assert_eq!("example", address.user());
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.user(), "user");
/// # Ok(())
/// # }
/// ```
@@ -133,8 +116,8 @@ impl Address {
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let address = Address::new("example", "email.com")?;
/// assert_eq!("email.com", address.domain());
/// let address = Address::new("user", "email.com")?;
/// assert_eq!(address.domain(), "email.com");
/// # Ok(())
/// # }
/// ```
@@ -173,6 +156,12 @@ impl Address {
Err(AddressError::InvalidDomain)
}
#[cfg(feature = "smtp-transport")]
/// Check if the address contains non-ascii chars
pub(super) fn is_ascii(&self) -> bool {
self.serialized.is_ascii()
}
}
impl Display for Address {
@@ -198,6 +187,28 @@ impl FromStr for Address {
}
}
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(),
})
}
}
impl AsRef<str> for Address {
fn as_ref(&self) -> &str {
&self.serialized
@@ -211,12 +222,16 @@ impl AsRef<OsStr> for Address {
}
#[derive(Debug, PartialEq, Clone, Copy)]
/// Errors in email addresses parsing
pub enum AddressError {
/// Missing domain or user
MissingParts,
/// Unbalanced angle bracket
Unbalanced,
/// Invalid email user
InvalidUser,
/// Invalid email domain
InvalidDomain,
InvalidUtf8b,
}
impl Error for AddressError {}
@@ -228,7 +243,6 @@ impl Display for AddressError {
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"),
}
}
}

View File

@@ -1,3 +1,5 @@
//! Error type for email messages
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},

272
src/executor.rs Normal file
View File

@@ -0,0 +1,272 @@
use async_trait::async_trait;
use std::fmt::Debug;
#[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 executor abstraction trait
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[async_trait]
pub trait Executor: Debug + 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<()>;
}
/// Async [`Executor`] using `tokio` `0.2.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
#[derive(Debug)]
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
}
}
/// Async [`Executor`] using `tokio` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
#[derive(Debug)]
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
}
}
/// Async [`Executor`] using `async-std` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
/// in order to be able to work with different async runtimes.
///
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[allow(missing_copy_implementations)]
#[non_exhaustive]
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
#[derive(Debug)]
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

@@ -4,30 +4,102 @@
//! * Pluggable email transports
//! * Unicode support
//! * Secure defaults
//! * Async support
//!
//! Lettre requires Rust 1.45 or newer.
//! Lettre requires Rust 1.46 or newer.
//!
//! ## Optional features
//! ## Features
//!
//! This section lists each lettre feature and briefly explains it.
//! More info about each module can be found in the corresponding module page.
//!
//! Features with `📫` near them are enabled by default.
//!
//! ### Typed message builder
//!
//! _Strongly typed [`message`] builder_
//!
//! * **builder** 📫: Enable the [`Message`] builder
//! * **hostname** 📫: Try to use the actual system hostname in the `Message-ID` header
//!
//! ### SMTP transport
//!
//! _Send emails using [`SMTP`]_
//!
//! * **smtp-transport** 📫: Enable the SMTP transport
//! * **r2d2** 📫: Connection pool for SMTP transport
//! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
//!
//! #### SMTP over TLS via the native-tls crate
//!
//! _Secure SMTP connections using TLS from the `native-tls` crate_
//!
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
//!
//! * **native-tls** 📫: TLS support for the synchronous version of the API
//! * **tokio02-native-tls**: TLS support for the `tokio02` async version of the API
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
//!
//! NOTE: native-tls isn't supported with `async-std`
//!
//! #### SMTP over TLS via the rustls crate
//!
//! _Secure SMTP connections using TLS from the `rustls-tls` crate_
//!
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
//!
//! * **rustls-tls**: TLS support for the synchronous version of the API
//! * **tokio02-rustls-tls**: TLS support for the `tokio02` async version of the API
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
//!
//! ### Sendmail transport
//!
//! _Send emails using the [`sendmail`] command_
//!
//! * **sendmail-transport**: Enable the `sendmail` transport
//!
//! ### File transport
//!
//! _Save emails as an `.eml` [`file`]_
//!
//! * **file-transport**: Enable the file transport (saves emails into an `.eml` file)
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file (additionally saves envelopes into a `.json` file)
//!
//! ### Async execution runtimes
//!
//! _Use [tokio] or [async-std] as an async execution runtime for sending emails_
//!
//! The correct runtime version must be chosen in order for lettre to work correctly.
//! For example, when sending emails from a Tokio 1.3.0 context, the Tokio 1.x executor
//! ([`Tokio1Executor`]) must be used. Using a different version (for example Tokio 0.2.x),
//! or async-std, would result in a runtime panic.
//!
//! * **tokio02**: Allow to asynchronously send emails using [Tokio 0.2.x]
//! * **tokio1**: Allow to asynchronously send emails 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`
//!
//! ### Misc features
//!
//! _Additional features_
//!
//! * **builder**: Message builder
//! * **file-transport**: Transport that write messages into a file
//! * **smtp-transport**: Transport over SMTP
//! * **sendmail-transport**: Transport over SMTP
//! * **rustls-tls**: TLS support with the `rustls` crate
//! * **native-tls**: TLS support with the `native-tls` crate
//! * **tokio02**: Allow to asyncronously send emails using tokio 0.2.x
//! * **tokio02-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.2
//! * **tokio02-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.2
//! * **tokio03**: Allow to asyncronously send emails using tokio 0.3.x
//! * **tokio03-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.3
//! * **tokio03-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.3
//! * **async-std1**: Allow to asyncronously send emails using async-std 1.x (SMTP isn't supported yet)
//! * **r2d2**: Connection pool for SMTP transport
//! * **tracing**: Logging using the `tracing` crate
//! * **serde**: Serialization/Deserialization of entities
//! * **hostname**: Ability to try to use actual hostname in SMTP transaction
//! * **tracing**: Logging using the `tracing` crate
//!
//! [`SMTP`]: crate::transport::smtp
//! [`sendmail`]: crate::transport::sendmail
//! [`file`]: crate::transport::file
//! [tokio]: https://docs.rs/tokio/1
//! [async-std]: https://docs.rs/async-std/1
//! [ring]: https://github.com/briansmith/ring#ring
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
//! [Tokio 0.2.x]: https://docs.rs/tokio/0.2
//! [Tokio 1.x]: https://docs.rs/tokio/1
//! [async-std 1.x]: https://docs.rs/async-std/1
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-alpha.4")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-beta.4")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
@@ -43,133 +115,69 @@
pub mod address;
pub mod error;
#[cfg(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;
#[cfg(feature = "builder")]
#[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")))]
#[doc(inline)]
pub use self::transport::AsyncTransport;
pub use crate::address::Address;
use crate::address::Envelope;
use crate::error::Error;
#[cfg(feature = "builder")]
#[doc(inline)]
pub use crate::message::Message;
#[cfg(all(
feature = "file-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
#[doc(inline)]
pub use crate::transport::file::AsyncFileTransport;
#[cfg(feature = "file-transport")]
#[doc(inline)]
pub use crate::transport::file::FileTransport;
#[cfg(all(
feature = "sendmail-transport",
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
#[doc(inline)]
pub use crate::transport::sendmail::AsyncSendmailTransport;
#[cfg(feature = "sendmail-transport")]
#[doc(inline)]
pub use crate::transport::sendmail::SendmailTransport;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio02", feature = "tokio03")
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
))]
pub use crate::transport::smtp::AsyncSmtpTransport;
#[doc(inline)]
pub use crate::transport::Transport;
use crate::{address::Envelope, error::Error};
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::SmtpTransport;
#[cfg(all(feature = "smtp-transport", feature = "tokio02"))]
pub use crate::transport::smtp::Tokio02Connector;
#[cfg(all(feature = "smtp-transport", feature = "tokio03"))]
pub use crate::transport::smtp::Tokio03Connector;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use async_trait::async_trait;
use std::error::Error as StdError;
/// Blocking Transport method for emails
pub trait Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
self.send_raw(message.envelope(), &raw)
}
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
/// async-std 1.x based Transport method for emails
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
#[async_trait]
pub trait AsyncStd1Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
/// tokio 0.2.x based Transport method for emails
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
#[async_trait]
pub trait Tokio02Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
/// tokio 0.3.x based Transport method for emails
#[cfg(feature = "tokio03")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio03")))]
#[async_trait]
pub trait Tokio03Transport {
/// Response produced by the Transport
type Ok;
/// Error produced by the Transport
type Error;
/// Sends the email
#[cfg(feature = "builder")]
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
// TODO take &Message
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
let raw = message.formatted();
let envelope = message.envelope();
self.send_raw(&envelope, &raw).await
}
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
}
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
#[cfg(test)]
#[cfg(feature = "builder")]
mod test {
use super::*;
use crate::message::{header, Mailbox, Mailboxes};
use hyperx::header::Headers;
use std::convert::TryFrom;
use super::*;
use crate::message::{header, header::Headers, Mailbox, Mailboxes};
#[test]
fn envelope_from_headers() {
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
@@ -196,9 +204,9 @@ mod test {
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From(from));
headers.set(header::Sender(sender));
headers.set(header::To(to));
headers.set(header::From::from(from));
headers.set(header::Sender::from(sender));
headers.set(header::To::from(to));
assert_eq!(
Envelope::try_from(&headers).unwrap(),
@@ -216,8 +224,8 @@ mod test {
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
let mut headers = Headers::new();
headers.set(header::From(from));
headers.set(header::Sender(sender));
headers.set(header::From::from(from));
headers.set(header::Sender::from(sender));
assert!(Envelope::try_from(&headers).is_err(),);
}

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

@@ -0,0 +1,661 @@
use std::{
io::{self, Write},
mem,
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 data
Binary(Vec<u8>),
/// UTF-8 string
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 `String` is passed, line endings are converted to `CRLF`.
///
/// 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 mut buf: MaybeString = buf.into();
let encoding = buf.encoding();
buf.encode_crlf();
Self::new_impl(buf.into(), encoding)
}
/// Encode the supplied `buf`, using the provided `encoding`.
///
/// [`Body::new`] is generally the better option.
///
/// If `String` is passed, line endings are converted to `CRLF`.
///
/// 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 mut buf: MaybeString = buf.into();
if !buf.is_encoding_ok(encoding) {
return Err(buf.into());
}
buf.encode_crlf();
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,
}
}
/// Encode line endings to CRLF if the variant is `String`
fn encode_crlf(&mut self) {
match self {
Self::String(string) => in_place_crlf_line_endings(string),
Self::Binary(_) => {}
}
}
/// 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 {
/// Encode as valid body
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()
}
}
/// In place conversion to CRLF line endings
fn in_place_crlf_line_endings(string: &mut String) {
let indices = find_all_lf_char_indices(&string);
for i in indices {
// this relies on `indices` being in reverse order
string.insert(i, '\r');
}
}
/// Find indices to all places where `\r` should be inserted
/// in order to make `s` have CRLF line endings
///
/// The list is reversed, which is more efficient.
fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
let mut indices = Vec::new();
let mut found_lf = false;
for (i, c) in s.char_indices().rev() {
if mem::take(&mut found_lf) && c != '\r' {
// the previous character was `\n`, but this isn't a `\r`
indices.push(i + c.len_utf8());
}
found_lf = c == '\n';
}
if found_lf {
// the first character is `\n`
indices.push(0);
}
indices
}
#[cfg(test)]
mod test {
use super::{in_place_crlf_line_endings, 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()
);
}
#[test]
fn crlf() {
let mut string = String::from("Send me a ✉️\nwith\nlettre!\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(string, "Send me a ✉️\r\nwith\r\nlettre!\r\n😀");
}
#[test]
fn harsh_crlf() {
let mut string = String::from("\n\nSend me a ✉️\r\n\nwith\n\nlettre!\n\r\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(
string,
"\r\n\r\nSend me a ✉️\r\n\r\nwith\r\n\r\nlettre!\r\n\r\n😀"
);
}
#[test]
fn crlf_noop() {
let mut string = String::from("\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
in_place_crlf_line_endings(&mut string);
assert_eq!(string, "\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
}
}

View File

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

View File

@@ -1,39 +1,52 @@
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
str::{from_utf8, FromStr},
str::FromStr,
};
header! { (ContentId, "Content-ID") => [String] }
use super::{Header, HeaderName};
use crate::BoxError;
/// `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 {
/// ASCII
SevenBit,
/// Quoted-Printable encoding
QuotedPrintable,
/// base64 encoding
Base64,
// 8BITMIME
/// Requires `8BITMIME`
EightBit,
/// Binary data
Binary,
}
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::SevenBit
impl Header for ContentTransferEncoding {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Content-Transfer-Encoding")
}
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(s.parse()?)
}
fn display(&self) -> String {
self.to_string()
}
}
impl Display for ContentTransferEncoding {
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
use self::ContentTransferEncoding::*;
f.write_str(match *self {
SevenBit => "7bit",
QuotedPrintable => "quoted-printable",
Base64 => "base64",
EightBit => "8bit",
Binary => "binary",
Self::SevenBit => "7bit",
Self::QuotedPrintable => "quoted-printable",
Self::Base64 => "base64",
Self::EightBit => "8bit",
Self::Binary => "binary",
})
}
}
@@ -41,47 +54,27 @@ 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()),
}
}
}
impl Header for ContentTransferEncoding {
fn header_name() -> &'static str {
"Content-Transfer-Encoding"
}
// FIXME HeaderError->HeaderError, same for result
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header))
.and_then(|s| {
s.parse::<ContentTransferEncoding>()
.map_err(|_| HeaderError::Header)
})
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}", self))
impl Default for ContentTransferEncoding {
fn default() -> Self {
ContentTransferEncoding::Base64
}
}
#[cfg(test)]
mod test {
use super::ContentTransferEncoding;
use hyperx::header::Headers;
use crate::message::header::{HeaderName, Headers};
#[test]
fn format_content_transfer_encoding() {
@@ -89,35 +82,35 @@ mod test {
headers.set(ContentTransferEncoding::SevenBit);
assert_eq!(
format!("{}", headers),
"Content-Transfer-Encoding: 7bit\r\n"
);
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: 7bit\r\n");
headers.set(ContentTransferEncoding::Base64);
assert_eq!(
format!("{}", headers),
"Content-Transfer-Encoding: base64\r\n"
);
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: base64\r\n");
}
#[test]
fn parse_content_transfer_encoding() {
let mut headers = Headers::new();
headers.set_raw("Content-Transfer-Encoding", "7bit");
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(&ContentTransferEncoding::SevenBit)
headers.insert_raw(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"7bit".to_string(),
);
headers.set_raw("Content-Transfer-Encoding", "base64");
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(ContentTransferEncoding::SevenBit)
);
headers.insert_raw(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"base64".to_string(),
);
assert_eq!(
headers.get::<ContentTransferEncoding>(),
Some(&ContentTransferEncoding::Base64)
Some(ContentTransferEncoding::Base64)
);
}
}

View File

@@ -0,0 +1,89 @@
use super::{Header, HeaderName};
use crate::BoxError;
/// `Content-Disposition` of an attachment
///
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
#[derive(Debug, Clone, PartialEq)]
pub struct ContentDisposition(String);
impl ContentDisposition {
/// An attachment which should be displayed inline into the message
pub fn inline() -> Self {
Self("inline".into())
}
/// An attachment which should be displayed inline into the message, but that also
/// species the filename in case it were to be downloaded
pub fn inline_with_name(file_name: &str) -> Self {
debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'");
Self(format!("inline; filename=\"{}\"", file_name))
}
/// An attachment which is separate from the body of the message, and can be downloaded separately
pub fn attachment(file_name: &str) -> Self {
debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'");
Self(format!("attachment; filename=\"{}\"", file_name))
}
}
impl Header for ContentDisposition {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Content-Disposition")
}
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(Self(s.into()))
}
fn display(&self) -> String {
self.0.clone()
}
}
#[cfg(test)]
mod test {
use super::ContentDisposition;
use crate::message::header::{HeaderName, Headers};
#[test]
fn format_content_disposition() {
let mut headers = Headers::new();
headers.set(ContentDisposition::inline());
assert_eq!(format!("{}", headers), "Content-Disposition: inline\r\n");
headers.set(ContentDisposition::attachment("something.txt"));
assert_eq!(
format!("{}", headers),
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
);
}
#[test]
fn parse_content_disposition() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Content-Disposition"),
"inline".to_string(),
);
assert_eq!(
headers.get::<ContentDisposition>(),
Some(ContentDisposition::inline())
);
headers.insert_raw(
HeaderName::new_from_ascii_str("Content-Disposition"),
"attachment; filename=\"something.txt\"".to_string(),
);
assert_eq!(
headers.get::<ContentDisposition>(),
Some(ContentDisposition::attachment("something.txt"))
);
}
}

View File

@@ -0,0 +1,123 @@
use std::{
error::Error as StdError,
fmt::{self, Display},
str::FromStr,
};
use mime::Mime;
use super::{Header, HeaderName};
use crate::BoxError;
/// `Content-Type` of the body
///
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
#[derive(Debug, Clone, PartialEq)]
pub struct ContentType(Mime);
impl ContentType {
/// A `ContentType` of type `text/plain; charset=utf-8`
///
/// Indicates that the body is in utf-8 encoded plain text.
pub const TEXT_PLAIN: ContentType = Self::from_mime(mime::TEXT_PLAIN_UTF_8);
/// A `ContentType` of type `text/html; charset=utf-8`
///
/// Indicates that the body is in utf-8 encoded html.
pub const TEXT_HTML: ContentType = Self::from_mime(mime::TEXT_HTML_UTF_8);
/// Parse `s` into `ContentType`
pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
}
pub(crate) const fn from_mime(mime: Mime) -> Self {
Self(mime)
}
pub(crate) fn as_ref(&self) -> &Mime {
&self.0
}
}
impl Header for ContentType {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Content-Type")
}
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(Self(s.parse()?))
}
fn display(&self) -> String {
self.0.to_string()
}
}
impl FromStr for ContentType {
type Err = ContentTypeErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
/// An error occurred while trying to [`ContentType::parse`].
#[derive(Debug)]
pub struct ContentTypeErr(mime::FromStrError);
impl StdError for ContentTypeErr {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.0)
}
}
impl Display for ContentTypeErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
#[cfg(test)]
mod test {
use super::ContentType;
use crate::message::header::{HeaderName, Headers};
#[test]
fn format_content_type() {
let mut headers = Headers::new();
headers.set(ContentType::TEXT_PLAIN);
assert_eq!(
headers.to_string(),
"Content-Type: text/plain; charset=utf-8\r\n"
);
headers.set(ContentType::TEXT_HTML);
assert_eq!(
headers.to_string(),
"Content-Type: text/html; charset=utf-8\r\n"
);
}
#[test]
fn parse_content_type() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Content-Type"),
"text/plain; charset=utf-8".to_string(),
);
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
headers.insert_raw(
HeaderName::new_from_ascii_str("Content-Type"),
"text/html; charset=utf-8".to_string(),
);
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
}
}

133
src/message/header/date.rs Normal file
View File

@@ -0,0 +1,133 @@
use std::time::SystemTime;
use httpdate::HttpDate;
use super::{Header, HeaderName};
use crate::BoxError;
/// Message `Date` header
///
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Date(HttpDate);
impl Date {
/// Build a `Date` from [`SystemTime`]
pub fn new(st: SystemTime) -> Self {
Self(st.into())
}
/// Get the current date
///
/// Shortcut for `Date::new(SystemTime::now())`
pub fn now() -> Self {
Self::new(SystemTime::now())
}
}
impl Header for Date {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("Date")
}
fn parse(s: &str) -> Result<Self, BoxError> {
let mut s = String::from(s);
if s.ends_with(" -0000") {
// The httpdate crate expects the `Date` to end in ` GMT`, but email
// uses `-0000`, so we crudely fix this issue here.
s.truncate(s.len() - "-0000".len());
s.push_str("GMT");
}
Ok(Self(s.parse::<HttpDate>()?))
}
fn display(&self) -> String {
let mut s = self.0.to_string();
if s.ends_with(" GMT") {
// The httpdate crate always appends ` GMT` to the end of the string,
// but this is considered an obsolete date format for email
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
// so we replace `GMT` with `-0000`
s.truncate(s.len() - "GMT".len());
s.push_str("-0000");
}
s
}
}
impl From<SystemTime> for Date {
fn from(st: SystemTime) -> Self {
Self::new(st)
}
}
impl From<Date> for SystemTime {
fn from(this: Date) -> SystemTime {
this.0.into()
}
}
#[cfg(test)]
mod test {
use std::time::{Duration, SystemTime};
use super::Date;
use crate::message::header::{HeaderName, Headers};
#[test]
fn format_date() {
let mut headers = Headers::new();
// Tue, 15 Nov 1994 08:12:31 GMT
headers.set(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
));
assert_eq!(
headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n".to_string()
);
// Tue, 15 Nov 1994 08:12:32 GMT
headers.set(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
));
assert_eq!(
headers.to_string(),
"Date: Tue, 15 Nov 1994 08:12:32 -0000\r\n"
);
}
#[test]
fn parse_date() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:31 -0000".to_string(),
);
assert_eq!(
headers.get::<Date>(),
Some(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
))
);
headers.insert_raw(
HeaderName::new_from_ascii_str("Date"),
"Tue, 15 Nov 1994 08:12:32 -0000".to_string(),
);
assert_eq!(
headers.get::<Date>(),
Some(Date::from(
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
))
);
}
}

View File

@@ -1,12 +1,8 @@
use crate::message::{
mailbox::{Mailbox, Mailboxes},
utf8_b,
use super::{Header, HeaderName};
use crate::{
message::mailbox::{Mailbox, Mailboxes},
BoxError,
};
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{fmt::Result as FmtResult, slice::Iter, str::from_utf8};
/// Header which can contains multiple mailboxes
pub trait MailboxesHeader {
@@ -17,26 +13,34 @@ macro_rules! mailbox_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(pub Mailbox);
pub struct $type_name(Mailbox);
impl Header for $type_name {
fn header_name() -> &'static str {
$header_name
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self> where
T: RawLike<'a>,
Self: Sized {
raw.one()
.ok_or(HeaderError::Header)
.and_then(parse_mailboxes)
.and_then(|mbs| {
mbs.into_single().ok_or(HeaderError::Header)
}).map($type_name)
fn parse(s: &str) -> Result<Self, BoxError> {
let mailbox: Mailbox = s.parse()?;
Ok(Self(mailbox))
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&self.0.recode_name(utf8_b::encode))
fn display(&self) -> String {
self.0.to_string()
}
}
impl std::convert::From<Mailbox> for $type_name {
#[inline]
fn from(mailbox: Mailbox) -> Self {
Self(mailbox)
}
}
impl std::convert::From<$type_name> for Mailbox {
#[inline]
fn from(this: $type_name) -> Mailbox {
this.0
}
}
};
@@ -46,7 +50,7 @@ macro_rules! mailboxes_header {
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(pub Mailboxes);
pub struct $type_name(pub(crate) Mailboxes);
impl MailboxesHeader for $type_name {
fn join_mailboxes(&mut self, other: Self) {
@@ -55,23 +59,31 @@ macro_rules! mailboxes_header {
}
impl Header for $type_name {
fn header_name() -> &'static str {
$header_name
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(parse_mailboxes)
.map($type_name)
fn parse(s: &str) -> Result<Self, BoxError> {
let mailbox: Mailboxes = s.parse()?;
Ok(Self(mailbox))
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
format_mailboxes(self.0.iter(), f)
fn display(&self) -> String {
self.0.to_string()
}
}
impl std::convert::From<Mailboxes> for $type_name {
#[inline]
fn from(mailboxes: Mailboxes) -> Self {
Self(mailboxes)
}
}
impl std::convert::From<$type_name> for Mailboxes {
#[inline]
fn from(this: $type_name) -> Mailboxes {
this.0
}
}
};
@@ -146,26 +158,10 @@ mailboxes_header! {
(Bcc, "Bcc")
}
fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
if let Ok(src) = from_utf8(raw) {
if let Ok(mbs) = src.parse() {
return Ok(mbs);
}
}
Err(HeaderError::Header)
}
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&Mailboxes::from(
mbs.map(|mb| mb.recode_name(utf8_b::encode))
.collect::<Vec<_>>(),
))
}
#[cfg(test)]
mod test {
use super::{From, Mailbox, Mailboxes};
use hyperx::header::Headers;
use crate::message::header::{HeaderName, Headers};
#[test]
fn format_single_without_name() {
@@ -174,7 +170,7 @@ mod test {
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(format!("{}", headers), "From: kayo@example.com\r\n");
assert_eq!(headers.to_string(), "From: kayo@example.com\r\n");
}
#[test]
@@ -184,7 +180,7 @@ mod test {
let mut headers = Headers::new();
headers.set(From(from));
assert_eq!(format!("{}", headers), "From: K. <kayo@example.com>\r\n");
assert_eq!(headers.to_string(), "From: K. <kayo@example.com>\r\n");
}
#[test]
@@ -197,7 +193,7 @@ mod test {
headers.set(From(from));
assert_eq!(
format!("{}", headers),
headers.to_string(),
"From: kayo@example.com, pony@domain.tld\r\n"
);
}
@@ -213,7 +209,7 @@ mod test {
headers.set(From(from.into()));
assert_eq!(
format!("{}", headers),
headers.to_string(),
"From: K. <kayo@example.com>, Pony P. <pony@domain.tld>\r\n"
);
}
@@ -226,7 +222,7 @@ mod test {
headers.set(From(from.into()));
assert_eq!(
format!("{}", headers),
headers.to_string(),
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
);
}
@@ -236,9 +232,12 @@ mod test {
let from = vec!["kayo@example.com".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com");
headers.insert_raw(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com".to_string(),
);
assert_eq!(headers.get::<From>(), Some(&From(from)));
assert_eq!(headers.get::<From>(), Some(From(from)));
}
#[test]
@@ -246,9 +245,12 @@ mod test {
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>");
headers.insert_raw(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>".to_string(),
);
assert_eq!(headers.get::<From>(), Some(&From(from)));
assert_eq!(headers.get::<From>(), Some(From(from)));
}
#[test]
@@ -259,9 +261,12 @@ mod test {
];
let mut headers = Headers::new();
headers.set_raw("From", "kayo@example.com, pony@domain.tld");
headers.insert_raw(
HeaderName::new_from_ascii_str("From"),
"kayo@example.com, pony@domain.tld".to_string(),
);
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
#[test]
@@ -272,18 +277,11 @@ mod test {
];
let mut headers = Headers::new();
headers.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
headers.insert_raw(
HeaderName::new_from_ascii_str("From"),
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_string(),
);
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
}
#[test]
fn parse_single_with_utf8_name() {
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()];
let mut headers = Headers::new();
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>");
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
assert_eq!(headers.get::<From>(), Some(From(from.into())));
}
}

View File

@@ -1,17 +1,800 @@
/*!
//! Headers widely used in email messages
## Headers widely used in email messages
use std::{
borrow::Cow,
error::Error,
fmt::{self, Display, Formatter},
ops::Deref,
};
*/
pub use self::{
content::*,
content_disposition::ContentDisposition,
content_type::{ContentType, ContentTypeErr},
date::Date,
mailbox::*,
special::*,
textual::*,
};
use crate::BoxError;
mod content;
mod content_disposition;
mod content_type;
mod date;
mod mailbox;
mod special;
mod textual;
pub use self::{content::*, mailbox::*, special::*, textual::*};
pub trait Header: Clone {
fn name() -> HeaderName;
pub use hyperx::header::{
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
DispositionType, Header, Headers, HttpDate as EmailDate,
};
fn parse(s: &str) -> Result<Self, BoxError>;
fn display(&self) -> String;
}
/// A set of email headers
#[derive(Debug, Clone, Default)]
pub struct Headers {
headers: Vec<(HeaderName, String)>,
}
impl Headers {
/// Create an empty `Headers`
///
/// This function does not allocate.
#[inline]
pub const fn new() -> Self {
Self {
headers: Vec::new(),
}
}
/// Create an empty `Headers` with a pre-allocated capacity
///
/// Pre-allocates a capacity of at least `capacity`.
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
headers: Vec::with_capacity(capacity),
}
}
/// Returns a copy of an `Header` present in `Headers`
///
/// Returns `None` if `Header` isn't present in `Headers`.
pub fn get<H: Header>(&self) -> Option<H> {
self.get_raw(&H::name()).and_then(|raw| H::parse(raw).ok())
}
/// Sets `Header` into `Headers`, overriding `Header` if it
/// was already present in `Headers`
pub fn set<H: Header>(&mut self, header: H) {
self.insert_raw(H::name(), header.display());
}
/// Remove `Header` from `Headers`, returning it
///
/// Returns `None` if `Header` isn't in `Headers`.
pub fn remove<H: Header>(&mut self) -> Option<H> {
self.remove_raw(&H::name())
.and_then(|(_name, raw)| H::parse(&raw).ok())
}
/// Clears `Headers`, removing all headers from it
///
/// Any pre-allocated capacity is left untouched.
#[inline]
pub fn clear(&mut self) {
self.headers.clear();
}
/// Returns a reference to the raw value of header `name`
///
/// Returns `None` if `name` isn't present in `Headers`.
pub fn get_raw(&self, name: &str) -> Option<&str> {
self.find_header(name).map(|(_name, value)| value)
}
/// Inserts a raw header into `Headers`, overriding `value` if it
/// was already present in `Headers`.
pub fn insert_raw(&mut self, name: HeaderName, value: String) {
match self.find_header_mut(&name) {
Some((_, current_value)) => {
*current_value = value;
}
None => {
self.headers.push((name, value));
}
}
}
/// Appends a raw header into `Headers`
///
/// If a header with a name of `name` is already present,
/// appends `, ` + `value` to it's current value.
pub fn append_raw(&mut self, name: HeaderName, value: String) {
match self.find_header_mut(&name) {
Some((_name, prev_value)) => {
prev_value.push_str(", ");
prev_value.push_str(&value);
}
None => self.headers.push((name, value)),
}
}
/// Remove a raw header from `Headers`, returning it
///
/// Returns `None` if `name` isn't present in `Headers`.
pub fn remove_raw(&mut self, name: &str) -> Option<(HeaderName, String)> {
self.find_header_index(name).map(|i| self.headers.remove(i))
}
fn find_header(&self, name: &str) -> Option<(&HeaderName, &str)> {
self.headers
.iter()
.find(|&(name_, _value)| name.eq_ignore_ascii_case(name_))
.map(|t| (&t.0, t.1.as_str()))
}
fn find_header_mut(&mut self, name: &str) -> Option<(&HeaderName, &mut String)> {
self.headers
.iter_mut()
.find(|(name_, _value)| name.eq_ignore_ascii_case(name_))
.map(|t| (&t.0, &mut t.1))
}
fn find_header_index(&self, name: &str) -> Option<usize> {
self.headers
.iter()
.enumerate()
.find(|&(_i, (name_, _value))| name.eq_ignore_ascii_case(name_))
.map(|(i, _)| i)
}
}
impl Display for Headers {
/// Formats `Headers`, ready to put them into an email
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (name, value) in &self.headers {
Display::fmt(name, f)?;
f.write_str(": ")?;
HeaderValueEncoder::encode(&name, &value, f)?;
f.write_str("\r\n")?;
}
Ok(())
}
}
/// A possible error when converting a `HeaderName` from another type.
// comes from `http` crate
#[allow(missing_copy_implementations)]
#[derive(Clone)]
pub struct InvalidHeaderName {
_priv: (),
}
impl fmt::Debug for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InvalidHeaderName")
// skip _priv noise
.finish()
}
}
impl fmt::Display for InvalidHeaderName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid header name")
}
}
impl Error for InvalidHeaderName {}
/// A valid header name
#[derive(Debug, Clone)]
pub struct HeaderName(Cow<'static, str>);
impl HeaderName {
/// Creates a new header name
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
if !ascii.is_empty()
&& ascii.len() <= 76
&& ascii.is_ascii()
&& !ascii.contains(|c| c == ':' || c == ' ')
{
Ok(Self(Cow::Owned(ascii)))
} else {
Err(InvalidHeaderName { _priv: () })
}
}
/// Creates a new header name, panics on invalid name
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
macro_rules! static_assert {
($condition:expr) => {
let _ = [()][(!($condition)) as usize];
};
}
static_assert!(!ascii.is_empty());
static_assert!(ascii.len() <= 76);
let bytes = ascii.as_bytes();
let mut i = 0;
while i < bytes.len() {
static_assert!(bytes[i].is_ascii());
static_assert!(bytes[i] != b' ');
static_assert!(bytes[i] != b':');
i += 1;
}
Self(Cow::Borrowed(ascii))
}
}
impl Display for HeaderName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self)
}
}
impl Deref for HeaderName {
type Target = str;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<[u8]> for HeaderName {
#[inline]
fn as_ref(&self) -> &[u8] {
let s: &str = self.as_ref();
s.as_bytes()
}
}
impl AsRef<str> for HeaderName {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<HeaderName> for HeaderName {
fn eq(&self, other: &HeaderName) -> bool {
let s1: &str = self.as_ref();
let s2: &str = other.as_ref();
s1 == s2
}
}
impl PartialEq<&str> for HeaderName {
fn eq(&self, other: &&str) -> bool {
let s: &str = self.as_ref();
s == *other
}
}
impl PartialEq<HeaderName> for &str {
fn eq(&self, other: &HeaderName) -> bool {
let s: &str = other.as_ref();
*self == s
}
}
const ENCODING_START_PREFIX: &str = "=?utf-8?b?";
const ENCODING_END_SUFFIX: &str = "?=";
const MAX_LINE_LEN: usize = 76;
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
struct HeaderValueEncoder {
line_len: usize,
encode_buf: String,
}
impl HeaderValueEncoder {
fn encode(name: &str, value: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (words_iter, encoder) = Self::new(name, value);
encoder.format(words_iter, f)
}
fn new<'a>(name: &str, value: &'a str) -> (WordsPlusFillIterator<'a>, Self) {
(
WordsPlusFillIterator { s: value },
Self {
line_len: name.len() + ": ".len(),
encode_buf: String::new(),
},
)
}
fn format(
mut self,
words_iter: WordsPlusFillIterator<'_>,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
/// Estimate if an encoded string of `len` would fix in an empty line
fn would_fit_new_line(len: usize) -> bool {
len < (MAX_LINE_LEN - " ".len())
}
/// Estimate how long a string of `len` would be after base64 encoding plus
/// adding the encoding prefix and suffix to it
fn base64_len(len: usize) -> usize {
ENCODING_START_PREFIX.len() + (len * 4 / 3 + 4) + ENCODING_END_SUFFIX.len()
}
/// Estimate how many more bytes we can fit in the current line
fn available_len_to_max_encode_len(len: usize) -> usize {
len.saturating_sub(
ENCODING_START_PREFIX.len() + (len * 3 / 4 + 4) + ENCODING_END_SUFFIX.len(),
)
}
for next_word in words_iter {
let allowed = allowed_str(next_word);
if allowed {
// This word only contains allowed characters
// the next word is allowed, but we may have accumulated some words to encode
self.flush_encode_buf(f, true)?;
if next_word.len() > self.remaining_line_len() {
// not enough space left on this line to encode word
if self.something_written_to_this_line() && would_fit_new_line(next_word.len())
{
// word doesn't fit this line, but something had already been written to it,
// and word would fit the next line, so go to a new line
// so go to new line
self.new_line(f)?;
} else {
// word neither fits this line and the next one, cut it
// in the middle and make it fit
let mut next_word = next_word;
while !next_word.is_empty() {
if self.remaining_line_len() == 0 {
self.new_line(f)?;
}
let len = self.remaining_line_len().min(next_word.len());
let first_part = &next_word[..len];
next_word = &next_word[len..];
f.write_str(first_part)?;
self.line_len += first_part.len();
}
continue;
}
}
// word fits, write it!
f.write_str(next_word)?;
self.line_len += next_word.len();
} else {
// This word contains unallowed characters
if self.remaining_line_len() >= base64_len(self.encode_buf.len() + next_word.len())
{
// next_word fits
self.encode_buf.push_str(next_word);
continue;
}
// next_word doesn't fit this line
if would_fit_new_line(base64_len(next_word.len())) {
// ...but it would fit the next one
self.flush_encode_buf(f, false)?;
self.new_line(f)?;
self.encode_buf.push_str(next_word);
continue;
}
// ...and also wouldn't fit the next one.
// chop it up into pieces
let mut next_word = next_word;
while !next_word.is_empty() {
if self.remaining_line_len() <= base64_len(1) {
self.flush_encode_buf(f, false)?;
self.new_line(f)?;
}
let mut len = available_len_to_max_encode_len(self.remaining_line_len())
.min(next_word.len());
// avoid slicing on a char boundary
while !next_word.is_char_boundary(len) {
len += 1;
}
let first_part = &next_word[..len];
next_word = &next_word[len..];
self.encode_buf.push_str(first_part);
}
}
}
self.flush_encode_buf(f, false)?;
Ok(())
}
/// Returns the number of bytes left for the current line
fn remaining_line_len(&self) -> usize {
MAX_LINE_LEN - self.line_len
}
/// Returns true if something has been written to the current line
fn something_written_to_this_line(&self) -> bool {
self.line_len > 1
}
fn flush_encode_buf(
&mut self,
f: &mut fmt::Formatter<'_>,
switching_to_allowed: bool,
) -> fmt::Result {
use std::fmt::Write;
if self.encode_buf.is_empty() {
// nothing to encode
return Ok(());
}
let mut write_after = None;
if switching_to_allowed {
// If the next word only contains allowed characters, and the string to encode
// ends with a space, take the space out of the part to encode
let last_char = self.encode_buf.pop().expect("self.encode_buf isn't empty");
if is_space_like(last_char) {
write_after = Some(last_char);
} else {
self.encode_buf.push(last_char);
}
}
f.write_str(ENCODING_START_PREFIX)?;
let encoded = base64::display::Base64Display::with_config(
self.encode_buf.as_bytes(),
base64::STANDARD,
);
Display::fmt(&encoded, f)?;
f.write_str(ENCODING_END_SUFFIX)?;
self.line_len += ENCODING_START_PREFIX.len();
self.line_len += self.encode_buf.len() * 4 / 3 + 4;
self.line_len += ENCODING_END_SUFFIX.len();
if let Some(write_after) = write_after {
f.write_char(write_after)?;
self.line_len += 1;
}
self.encode_buf.clear();
Ok(())
}
fn new_line(&mut self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\r\n ")?;
self.line_len = 1;
Ok(())
}
}
/// Iterator yielding a string split space by space, but including all space
/// characters between it and the next word
struct WordsPlusFillIterator<'a> {
s: &'a str,
}
impl<'a> Iterator for WordsPlusFillIterator<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.s.is_empty() {
return None;
}
let next_word = self
.s
.char_indices()
.skip(1)
.skip_while(|&(_i, c)| !is_space_like(c))
.find(|&(_i, c)| !is_space_like(c))
.map(|(i, _)| i);
let word = &self.s[..next_word.unwrap_or_else(|| self.s.len())];
self.s = &self.s[word.len()..];
Some(word)
}
}
const fn is_space_like(c: char) -> bool {
c == ',' || c == ' '
}
fn allowed_str(s: &str) -> bool {
s.chars().all(allowed_char)
}
const fn allowed_char(c: char) -> bool {
c >= 1 as char && c <= 9 as char
|| c == 11 as char
|| c == 12 as char
|| c >= 14 as char && c <= 127 as char
}
#[cfg(test)]
mod tests {
use super::{HeaderName, Headers};
#[test]
fn valid_headername() {
assert!(HeaderName::new_from_ascii(String::from("From")).is_ok());
}
#[test]
fn non_ascii_headername() {
assert!(HeaderName::new_from_ascii(String::from("🌎")).is_err());
}
#[test]
fn spaces_in_headername() {
assert!(HeaderName::new_from_ascii(String::from("From ")).is_err());
}
#[test]
fn colons_in_headername() {
assert!(HeaderName::new_from_ascii(String::from("From:")).is_err());
}
#[test]
fn empty_headername() {
assert!(HeaderName::new_from_ascii(String::from("")).is_err());
}
#[test]
fn const_valid_headername() {
let _ = HeaderName::new_from_ascii_str("From");
}
#[test]
#[should_panic]
fn const_non_ascii_headername() {
let _ = HeaderName::new_from_ascii_str("🌎");
}
#[test]
#[should_panic]
fn const_spaces_in_headername() {
let _ = HeaderName::new_from_ascii_str("From ");
}
#[test]
#[should_panic]
fn const_colons_in_headername() {
let _ = HeaderName::new_from_ascii_str("From:");
}
#[test]
#[should_panic]
fn const_empty_headername() {
let _ = HeaderName::new_from_ascii_str("");
}
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
#[test]
fn format_ascii() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
"To: John Doe <example@example.com>, Jean Dupont <jean@example.com>\r\n"
);
}
#[test]
fn format_ascii_with_folding() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
concat!(
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith \r\n",
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand \r\n",
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
)
);
}
#[test]
fn format_ascii_with_folding_long_line() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
);
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello! This is lettre, and this \r\n ",
"IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n",
" guess that's it!\r\n"
)
);
}
#[test]
fn format_ascii_with_folding_very_long_line() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Subject"),
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_string()
);
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n",
" ouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know\r\n",
)
);
}
#[test]
fn format_ascii_with_folding_giant_word() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Subject"),
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_string()
);
assert_eq!(
headers.to_string(),
concat!(
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n",
" mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n",
" ghijklmnopqrstuvwxyz\r\n",
)
);
}
#[test]
fn format_special() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"Seán <sean@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
);
}
#[test]
fn format_special_emoji() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"🌎 <world@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
"To: =?utf-8?b?8J+Mjg==?= <world@example.com>\r\n"
);
}
#[test]
fn format_special_with_folding() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(),
);
assert_eq!(
headers.to_string(),
concat!(
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n"
)
);
}
#[test]
fn format_slice_on_char_boundary_bug() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Subject"),
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_string(),
);
assert_eq!(
headers.to_string(),
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n"
);
}
#[test]
fn format_bad_stuff() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Subject"),
"Hello! \r\n This is \" bad \0. 👋".to_string(),
);
assert_eq!(
headers.to_string(),
"Subject: Hello! =?utf-8?b?DQo=?= This is \" bad =?utf-8?b?AC4g8J+Riw==?=\r\n"
);
}
#[test]
fn format_everything() {
let mut headers = Headers::new();
headers.insert_raw(
HeaderName::new_from_ascii_str("Subject"),
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_string()
);
headers.insert_raw(
HeaderName::new_from_ascii_str("To"),
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_string(),
);
headers.insert_raw(
HeaderName::new_from_ascii_str("From"),
"Someone <somewhere@example.com>".to_string(),
);
headers.insert_raw(
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
"quoted-printable".to_string(),
);
assert_eq!(
headers.to_string(),
concat!(
"Subject: Hello! This is lettre, and this \r\n",
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n",
" guess that's it!\r\n",
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
"From: Someone <somewhere@example.com>\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
)
);
}
}

View File

@@ -1,21 +1,55 @@
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
use crate::{
message::header::{Header, HeaderName},
BoxError,
};
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)
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct MimeVersion {
pub major: u8,
pub minor: u8,
major: u8,
minor: u8,
}
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
impl MimeVersion {
pub fn new(major: u8, minor: u8) -> Self {
pub const fn new(major: u8, minor: u8) -> Self {
MimeVersion { major, minor }
}
#[inline]
pub const fn major(self) -> u8 {
self.major
}
#[inline]
pub const fn minor(self) -> u8 {
self.minor
}
}
impl Header for MimeVersion {
fn name() -> HeaderName {
HeaderName::new_from_ascii_str("MIME-Version")
}
fn parse(s: &str) -> Result<Self, BoxError> {
let mut s = s.split('.');
let major = s
.next()
.expect("The first call to next for a Split<char> always succeeds");
let minor = s
.next()
.ok_or_else(|| String::from("MIME-Version header doesn't contain '.'"))?;
let major = major.parse()?;
let minor = minor.parse()?;
Ok(MimeVersion::new(major, minor))
}
fn display(&self) -> String {
format!("{}.{}", self.major, self.minor)
}
}
impl Default for MimeVersion {
@@ -24,36 +58,10 @@ impl Default for MimeVersion {
}
}
impl Header for MimeVersion {
fn header_name() -> &'static str {
"MIME-Version"
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one().ok_or(HeaderError::Header).and_then(|r| {
let mut s = from_utf8(r).map_err(|_| HeaderError::Header)?.split('.');
let major = s.next().ok_or(HeaderError::Header)?;
let minor = s.next().ok_or(HeaderError::Header)?;
let major = major.parse().map_err(|_| HeaderError::Header)?;
let minor = minor.parse().map_err(|_| HeaderError::Header)?;
Ok(MimeVersion::new(major, minor))
})
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&format!("{}.{}", self.major, self.minor))
}
}
#[cfg(test)]
mod test {
use super::{MimeVersion, MIME_VERSION_1_0};
use hyperx::header::Headers;
use crate::message::header::{HeaderName, Headers};
#[test]
fn format_mime_version() {
@@ -61,23 +69,29 @@ mod test {
headers.set(MIME_VERSION_1_0);
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
assert_eq!(headers.to_string(), "MIME-Version: 1.0\r\n");
headers.set(MimeVersion::new(0, 1));
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
assert_eq!(headers.to_string(), "MIME-Version: 0.1\r\n");
}
#[test]
fn parse_mime_version() {
let mut headers = Headers::new();
headers.set_raw("MIME-Version", "1.0");
headers.insert_raw(
HeaderName::new_from_ascii_str("MIME-Version"),
"1.0".to_string(),
);
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0));
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
headers.set_raw("MIME-Version", "0.1");
headers.insert_raw(
HeaderName::new_from_ascii_str("MIME-Version"),
"0.1".to_string(),
);
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
}
}

View File

@@ -1,70 +1,99 @@
use crate::message::utf8_b;
use hyperx::{
header::{Formatter as HeaderFormatter, Header, RawLike},
Error as HeaderError, Result as HyperResult,
};
use std::{fmt::Result as FmtResult, str::from_utf8};
use super::{Header, HeaderName};
use crate::BoxError;
macro_rules! text_header {
( $type_name: ident, $header_name: expr ) => {
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
$(#[$attr])*
#[derive(Debug, Clone, PartialEq)]
pub struct $type_name(pub String);
pub struct $type_name(String);
impl Header for $type_name {
fn header_name() -> &'static str {
$header_name
fn name() -> HeaderName {
HeaderName::new_from_ascii_str($header_name)
}
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
where
T: RawLike<'a>,
Self: Sized,
{
raw.one()
.ok_or(HeaderError::Header)
.and_then(parse_text)
.map($type_name)
fn parse(s: &str) -> Result<Self, BoxError> {
Ok(Self(s.into()))
}
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
fmt_text(&self.0, f)
fn display(&self) -> String {
self.0.clone()
}
}
impl From<String> for $type_name {
#[inline]
fn from(text: String) -> Self {
Self(text)
}
}
impl AsRef<str> for $type_name {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
};
}
text_header!(Subject, "Subject");
text_header!(Comments, "Comments");
text_header!(Keywords, "Keywords");
text_header!(InReplyTo, "In-Reply-To");
text_header!(References, "References");
text_header!(MessageId, "Message-Id");
text_header!(UserAgent, "User-Agent");
fn parse_text(raw: &[u8]) -> HyperResult<String> {
if let Ok(src) = from_utf8(raw) {
if let Some(txt) = utf8_b::decode(src) {
return Ok(txt);
}
}
Err(HeaderError::Header)
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")
);
text_header! {
/// `Content-Id` header,
/// defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
Header(ContentId, "Content-ID")
}
fn fmt_text(s: &str, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
f.fmt_line(&utf8_b::encode(s))
text_header! {
/// `Content-Location` header,
/// defined in [RFC2110](https://tools.ietf.org/html/rfc2110#section-4.3)
Header(ContentLocation, "Content-Location")
}
#[cfg(test)]
mod test {
use super::Subject;
use hyperx::header::Headers;
use crate::message::header::{HeaderName, Headers};
#[test]
fn format_ascii() {
let mut headers = Headers::new();
headers.set(Subject("Sample subject".into()));
assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n");
assert_eq!(headers.to_string(), "Subject: Sample subject\r\n");
}
#[test]
@@ -73,7 +102,7 @@ mod test {
headers.set(Subject("Тема сообщения".into()));
assert_eq!(
format!("{}", headers),
headers.to_string(),
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
);
}
@@ -81,25 +110,14 @@ mod test {
#[test]
fn parse_ascii() {
let mut headers = Headers::new();
headers.set_raw("Subject", "Sample subject");
assert_eq!(
headers.get::<Subject>(),
Some(&Subject("Sample subject".into()))
);
}
#[test]
fn parse_utf8() {
let mut headers = Headers::new();
headers.set_raw(
"Subject",
"=?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=",
headers.insert_raw(
HeaderName::new_from_ascii_str("Subject"),
"Sample subject".to_string(),
);
assert_eq!(
headers.get::<Subject>(),
Some(&Subject("Тема сообщения".into()))
Some(Subject("Sample subject".into()))
);
}
}

View File

@@ -23,7 +23,7 @@ impl<'de> Deserialize<'de> for Mailbox {
enum Field {
Name,
Email,
};
}
const FIELDS: &[&str] = &["name", "email"];

View File

@@ -1,7 +1,4 @@
use crate::{
address::{Address, AddressError},
message::utf8_b,
};
use crate::address::{Address, AddressError};
use std::{
convert::TryFrom,
fmt::{Display, Formatter, Result as FmtResult, Write},
@@ -54,7 +51,7 @@ impl Mailbox {
/// # Examples
///
/// ```
/// use lettre::{Address, message::Mailbox};
/// use lettre::{message::Mailbox, Address};
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
@@ -66,14 +63,6 @@ impl Mailbox {
pub fn new(name: Option<String>, email: Address) -> Self {
Mailbox { name, email }
}
/// Encode addressee name using function
pub(crate) fn recode_name<F>(&self, f: F) -> Self
where
F: FnOnce(&str) -> String,
{
Mailbox::new(self.name.clone().map(|s| f(&s)), self.email.clone())
}
}
impl Display for Mailbox {
@@ -163,7 +152,10 @@ impl Mailboxes {
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
@@ -182,7 +174,10 @@ impl Mailboxes {
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
@@ -201,7 +196,10 @@ impl Mailboxes {
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
@@ -225,7 +223,10 @@ impl Mailboxes {
/// # Examples
///
/// ```
/// use lettre::{Address, message::{Mailbox, Mailboxes}};
/// use lettre::{
/// message::{Mailbox, Mailboxes},
/// Address,
/// };
///
/// # use std::error::Error;
/// # fn main() -> Result<(), Box<dyn Error>> {
@@ -258,26 +259,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
}
}
@@ -320,19 +321,7 @@ impl FromStr for Mailboxes {
fn from_str(src: &str) -> Result<Self, Self::Err> {
src.split(',')
.map(|m| {
m.trim().parse().and_then(|Mailbox { name, email }| {
if let Some(name) = name {
if let Some(name) = utf8_b::decode(&name) {
Ok(Mailbox::new(Some(name), email))
} else {
Err(AddressError::InvalidUtf8b)
}
} else {
Ok(Mailbox::new(None, email))
}
})
})
.map(|m| m.trim().parse())
.collect::<Result<Vec<_>, _>>()
.map(Mailboxes)
}

View File

@@ -1,21 +1,19 @@
use std::io::Write;
use crate::message::{
encoder::codec,
header::{ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat,
EmailFormat, IntoBody,
};
use mime::Mime;
use rand::Rng;
use std::iter::repeat_with;
/// MIME part variants
///
#[derive(Debug, Clone)]
pub enum Part {
/// Single part with content
///
Single(SinglePart),
/// Multiple parts of content
///
Multi(MultiPart),
}
@@ -38,11 +36,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 +65,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>();
let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding());
SinglePart {
headers: self.headers,
body: body.into(),
body: body.into_vec(),
}
}
}
@@ -88,18 +89,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()?))
/// .header(header::ContentTransferEncoding::Binary)
/// .body("Текст письма в уникоде");
/// .header(header::ContentType::TEXT_PLAIN)
/// .body(String::from("Текст письма в уникоде"));
/// # Ok(())
/// # }
/// ```
///
#[derive(Debug, Clone)]
pub struct SinglePart {
headers: Headers,
@@ -107,57 +106,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);
@@ -167,19 +134,15 @@ impl SinglePart {
impl EmailFormat for SinglePart {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
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)]
pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts
@@ -205,31 +168,28 @@ pub enum MultiPartKind {
}
/// Create a random MIME boundary.
/// (Not cryptographically random)
fn make_boundary() -> String {
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(68)
.collect()
repeat_with(fastrand::alphanumeric).take(40).collect()
}
impl MultiPartKind {
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
let boundary = boundary.map_or_else(make_boundary, |s| s.into());
pub(crate) fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
let boundary = boundary.map_or_else(make_boundary, Into::into);
use self::MultiPartKind::*;
format!(
"multipart/{}; boundary=\"{}\"{}",
match self {
Mixed => "mixed",
Alternative => "alternative",
Related => "related",
Encrypted { .. } => "encrypted",
Signed { .. } => "signed",
Self::Mixed => "mixed",
Self::Alternative => "alternative",
Self::Related => "related",
Self::Encrypted { .. } => "encrypted",
Self::Signed { .. } => "signed",
},
boundary,
match self {
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
Signed { protocol, micalg } =>
Self::Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
Self::Signed { protocol, micalg } =>
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
_ => String::new(),
}
@@ -239,18 +199,17 @@ impl MultiPartKind {
}
fn from_mime(m: &Mime) -> Option<Self> {
use self::MultiPartKind::*;
match m.subtype().as_ref() {
"mixed" => Some(Mixed),
"alternative" => Some(Alternative),
"related" => Some(Related),
"mixed" => Some(Self::Mixed),
"alternative" => Some(Self::Alternative),
"related" => Some(Self::Related),
"signed" => m.get_param("protocol").and_then(|p| {
m.get_param("micalg").map(|micalg| Signed {
m.get_param("micalg").map(|micalg| Self::Signed {
protocol: p.as_str().to_owned(),
micalg: micalg.as_str().to_owned(),
})
}),
"encrypted" => m.get_param("protocol").map(|p| Encrypted {
"encrypted" => m.get_param("protocol").map(|p| Self::Encrypted {
protocol: p.as_str().to_owned(),
}),
_ => None,
@@ -258,14 +217,7 @@ impl MultiPartKind {
}
}
impl From<MultiPartKind> for Mime {
fn from(m: MultiPartKind) -> Self {
m.to_mime::<String>(None)
}
}
/// Multipart builder
///
#[derive(Debug, Clone)]
pub struct MultiPartBuilder {
headers: Headers,
@@ -287,17 +239,17 @@ impl MultiPartBuilder {
/// Set `Content-Type` header using [`MultiPartKind`]
pub fn kind(self, kind: MultiPartKind) -> Self {
self.header(ContentType(kind.into()))
self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
}
/// Set custom boundary
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
let kind = {
let mime = &self.headers.get::<ContentType>().unwrap().0;
MultiPartKind::from_mime(mime).unwrap()
let content_type = self.headers.get::<ContentType>().unwrap();
MultiPartKind::from_mime(content_type.as_ref()).unwrap()
};
let mime = kind.to_mime(Some(boundary.as_ref()));
self.header(ContentType(mime))
let mime = kind.to_mime(Some(boundary));
self.header(ContentType::from_mime(mime))
}
/// Creates multipart without parts
@@ -331,7 +283,6 @@ impl Default for MultiPartBuilder {
}
/// Multipart variant with parts
///
#[derive(Debug, Clone)]
pub struct MultiPart {
headers: Headers,
@@ -399,8 +350,13 @@ impl MultiPart {
/// Get the boundary of multipart contents
pub fn boundary(&self) -> String {
let content_type = &self.headers.get::<ContentType>().unwrap().0;
content_type.get_param("boundary").unwrap().as_str().into()
let content_type = self.headers.get::<ContentType>().unwrap();
content_type
.as_ref()
.get_param("boundary")
.unwrap()
.as_str()
.into()
}
/// Get the headers from the multipart
@@ -433,7 +389,8 @@ impl MultiPart {
impl EmailFormat for MultiPart {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
out.extend_from_slice(b"\r\n");
let boundary = self.boundary();
@@ -459,16 +416,14 @@ mod test {
#[test]
fn single_part_binary() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n"
@@ -479,16 +434,14 @@ mod test {
#[test]
fn single_part_quoted_printable() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::QuotedPrintable)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: quoted-printable\r\n",
"\r\n",
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
@@ -500,16 +453,14 @@ mod test {
#[test]
fn single_part_base64() {
let part = SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Base64)
.body(String::from("Текст письма в уникоде"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: base64\r\n",
"\r\n",
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
@@ -520,75 +471,60 @@ mod test {
#[test]
fn multi_part_mixed() {
let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
))
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"example.c".into(),
)],
})
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentDisposition::attachment("example.c"))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")),
);
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"int main() { return 0; }\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: multipart/mixed; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"int main() { return 0; }\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
}
#[test]
fn multi_part_encrypted() {
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"application/pgp-encrypted".parse().unwrap(),
))
.header(header::ContentType::parse("application/pgp-encrypted").unwrap())
.body(String::from("Version: 1")),
))
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/octet-stream; name=\"encrypted.asc\""
.parse()
.header(
ContentType::parse("application/octet-stream; name=\"encrypted.asc\"")
.unwrap(),
)
.header(header::ContentDisposition::inline_with_name(
"encrypted.asc",
))
.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",
@@ -597,25 +533,31 @@ mod test {
))),
);
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/encrypted;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
" protocol=\"application/pgp-encrypted\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/pgp-encrypted\r\n",
"\r\n",
"Version: 1\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
"\r\n",
"-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
"...\r\n",
"-----END PGP MESSAGE-----\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: multipart/encrypted; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n",
" protocol=\"application/pgp-encrypted\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: application/pgp-encrypted\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Version: 1\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\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",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
}
#[test]
fn multi_part_signed() {
@@ -623,27 +565,19 @@ mod test {
"application/pgp-signature".to_owned(),
"pgp-sha256".to_owned(),
)
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType("text/plain".parse().unwrap()))
.header(header::ContentType::TEXT_PLAIN)
.body(String::from("Test email for signature")),
))
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/pgp-signature; name=\"signature.asc\""
.parse()
.header(
ContentType::parse("application/pgp-signature; name=\"signature.asc\"")
.unwrap(),
))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(
header::Charset::Ext("utf-8".into()),
None,
"signature.asc".into(),
)],
})
)
.header(header::ContentDisposition::attachment("signature.asc"))
.body(String::from(concat!(
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
@@ -655,99 +589,102 @@ mod test {
))),
);
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/signed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
" protocol=\"application/pgp-signature\";",
" micalg=\"pgp-sha256\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain\r\n",
"\r\n",
"Test email for signature\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
"\r\n",
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
"=3FYZ\r\n",
"-----END PGP SIGNATURE-----\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
assert_eq!(
String::from_utf8(part.formatted()).unwrap(),
concat!(
"Content-Type: multipart/signed; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n",
" protocol=\"application/pgp-signature\";",
" micalg=\"pgp-sha256\"\r\n",
"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Test email for signature\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\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",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
)
);
}
#[test]
fn multi_part_alternative() {
let part = MultiPart::alternative()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.part(Part::Single(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"))))
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentType::TEXT_HTML)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/alternative;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
concat!("Content-Type: multipart/alternative; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"Текст письма в уникоде\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/html; charset=utf8\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/html; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
}
#[test]
fn multi_part_mixed_related() {
let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.multipart(MultiPart::related()
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentType::TEXT_HTML)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
.singlepart(SinglePart::builder()
.header(header::ContentType("image/png".parse().unwrap()))
.header(header::ContentLocation("/image.png".into()))
.header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentLocation::from(String::from("/image.png")))
.header(header::ContentTransferEncoding::Base64)
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
.singlepart(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentDisposition {
disposition: header::DispositionType::Attachment,
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".into())]
})
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentDisposition::attachment("example.c"))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("int main() { return 0; }")));
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
concat!("Content-Type: multipart/mixed;",
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
concat!("Content-Type: multipart/mixed; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: multipart/related;",
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: multipart/related; \r\n",
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
"\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
"Content-Type: text/html; charset=utf8\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/html; charset=utf-8\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: image/png\r\n",
"Content-Location: /image.png\r\n",
"Content-Transfer-Encoding: base64\r\n",
@@ -755,14 +692,14 @@ mod test {
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: text/plain; charset=utf8\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n",
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
"Content-Type: text/plain; charset=utf-8\r\n",
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
"Content-Transfer-Encoding: binary\r\n",
"\r\n",
"int main() { return 0; }\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
}
#[test]
@@ -777,7 +714,7 @@ mod test {
// Ensure correct length
for boundary in boundaries {
assert_eq!(68, boundary.len());
assert_eq!(40, boundary.len());
}
}
}

View File

@@ -1,16 +1,24 @@
//! 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
//! use lettre::message::Message;
@@ -22,74 +30,41 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")?;
//! .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
//! use lettre::message::{header, Message, SinglePart, Part};
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = 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")
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/plain; charset=utf8".parse()?,
//! )).header(header::ContentTransferEncoding::QuotedPrintable)
//! .body("Привет, мир!"),
//! )?;
//! # Ok(())
//! # }
//! ```
//! use lettre::message::{header, Message, MultiPart, Part, SinglePart};
//!
//! The body will be encoded using selected `Content-Transfer-Encoding`.
//!
//! ```sh
//! From: NoBody <nobody@domain.tld>
//! Reply-To: Yuin <yuin@domain.tld>
//! To: Hei <hei@domain.tld>
//! Subject: Happy new year
//! MIME-Version: 1.0
//! Content-Type: text/plain; charset=utf8
//! Content-Transfer-Encoding: quoted-printable
//!
//! =D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!
//!
//! ```
//!
//! ##### Multiple parts
//!
//! And more advanced way of building message by using multipart MIME contents.
//!
//! ```rust
//! use lettre::message::{header, Message, MultiPart, SinglePart, Part};
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -97,115 +72,181 @@
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .multipart(
//! MultiPart::mixed()
//! .multipart(
//! MultiPart::alternative()
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::quoted_printable()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body("Привет, мир!")
//! SinglePart::builder()
//! .header(header::ContentType::TEXT_PLAIN)
//! .body(String::from("Hello, world! :)")),
//! )
//! .multipart(
//! MultiPart::related()
//! .singlepart(
//! SinglePart::eight_bit()
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
//! .body("<p><b>Hello</b>, <i>world</i>! <img src=smile.png></p>")
//! )
//! .singlepart(
//! SinglePart::base64()
//! .header(header::ContentType("image/png".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
//! })
//! .body("<smile-raw-image-data>")
//! )
//! )
//! )
//! .singlepart(
//! SinglePart::seven_bit()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Attachment,
//! parameters: vec![
//! header::DispositionParam::Filename(
//! header::Charset::Ext("utf-8".into()),
//! None, "example.c".as_bytes().into()
//! )
//! ]
//! })
//! .body("int main() { return 0; }")
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType::TEXT_HTML)
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>",
//! )),
//! ),
//! )?;
//! # 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:33:19 GMT
//! Content-Type: multipart/alternative; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
//!
//! --RTxPCn9p31oAAAAAeQxtr1FbXr/i5vW1hFlH9oJqZRMWxRMK1QLjQ4OPqFk9R+0xUb/m
//! Content-Type: multipart/alternative; boundary="qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy"
//!
//! --qW9QCn9p31oAAAAAodFBg1L1Qrraa5hEl0bDJ6kfJMUcRT2LLSWEoeyhSEbUBIqbjWqy
//! Content-Transfer-Encoding: quoted-printable
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
//! 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"
//!
//! --BV5RCn9p31oAAAAAUt42E9bYMDEAGCOWlxEz89Bv0qFA5Xsy6rOC3zRahMQ39IFZNnp8
//! Content-Transfer-Encoding: 8bit
//! Hello, world! :)
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
//! 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>
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
//! ```
//! </details>
//!
//! ### Complex MIME body
//!
//! This example shows how to include both plain and HTML versions of the body,
//! attachments and inlined images.
//!
//! ```rust
//! # 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()?)
//! .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::builder()
//! .header(header::ContentType::TEXT_PLAIN)
//! .body(String::from("Hello, world! :)")),
//! )
//! .multipart(
//! MultiPart::related()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType::TEXT_HTML)
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
//! )),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType::parse("image/png")?)
//! .header(header::ContentDisposition::inline())
//! .header(header::ContentId::from(String::from("<123>")))
//! .body(image_body),
//! ),
//! ),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType::TEXT_PLAIN)
//! .header(header::ContentDisposition::attachment("example.rs"))
//! .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
//! Date: Sat, 12 Dec 2020 16:30:45 GMT
//! Content-Type: multipart/mixed; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
//!
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
//! Content-Type: multipart/alternative; boundary="EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk"
//!
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
//! Content-Type: text/plain; charset=utf8
//! Content-Transfer-Encoding: 7bit
//!
//! Hello, world! :)
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
//! Content-Type: multipart/related; boundary="eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr"
//!
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
//! Content-Type: text/html; charset=utf8
//! Content-Transfer-Encoding: 7bit
//!
//! <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>
use std::{convert::TryFrom, io::Write, iter, time::SystemTime};
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::{
address::Envelope,
message::header::{EmailDate, Header, Headers, MailboxesHeader},
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
Error as EmailError,
};
use std::{convert::TryFrom, time::SystemTime};
use uuid::Uuid;
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
pub trait EmailFormat {
/// Something that can be formatted as an email message
trait EmailFormat {
// Use a writer?
fn format(&self, out: &mut Vec<u8>);
}
@@ -233,34 +274,36 @@ impl MessageBuilder {
}
/// Add mailbox to header
pub fn mailbox<H: Header + MailboxesHeader>(mut self, header: H) -> Self {
if self.headers.has::<H>() {
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
self
} else {
self.header(header)
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
match self.headers.get::<H>() {
Some(mut header_) => {
header_.join_mailboxes(header);
self.header(header_)
}
None => self.header(header),
}
}
/// Add `Date` header to message
///
/// Shortcut for `self.header(header::Date(date))`.
pub fn date(self, date: EmailDate) -> Self {
self.header(header::Date(date))
/// Shortcut for `self.header(header::Date::new(st))`.
pub fn date(self, st: SystemTime) -> Self {
self.header(header::Date::new(st))
}
/// Set `Date` header using current date/time
///
/// Shortcut for `self.date(SystemTime::now())`.
pub fn date_now(self) -> Self {
self.date(SystemTime::now().into())
self.date(SystemTime::now())
}
/// Set `Subject` header to message
///
/// Shortcut for `self.header(header::Subject(subject.into()))`.
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
self.header(header::Subject(subject.into()))
let s: String = subject.into();
self.header(header::Subject::from(s))
}
/// Set `Mime-Version` header to 1.0
@@ -274,25 +317,25 @@ 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 {
self.header(header::Sender(mbox))
self.header(header::Sender::from(mbox))
}
/// 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 {
self.mailbox(header::From(mbox.into()))
self.mailbox(header::From::from(Mailboxes::from(mbox)))
}
/// 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 {
@@ -323,16 +366,16 @@ impl MessageBuilder {
/// Set or add message id to [`In-Reply-To`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn in_reply_to(self, id: String) -> Self {
self.header(header::InReplyTo(id))
self.header(header::InReplyTo::from(id))
}
/// Set or add message id to [`References`
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
pub fn references(self, id: String) -> Self {
self.header(header::References(id))
self.header(header::References::from(id))
}
/// Set [Message-Id
/// Set [Message-ID
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
///
/// Should generally be inserted by the mail relay.
@@ -341,7 +384,7 @@ impl MessageBuilder {
/// `<UUID@HOSTNAME>`.
pub fn message_id(self, id: Option<String>) -> Self {
match id {
Some(i) => self.header(header::MessageId(i)),
Some(i) => self.header(header::MessageId::from(i)),
None => {
#[cfg(feature = "hostname")]
let hostname = hostname::get()
@@ -351,9 +394,9 @@ impl MessageBuilder {
#[cfg(not(feature = "hostname"))]
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
self.header(header::MessageId(
self.header(header::MessageId::from(
// https://tools.ietf.org/html/rfc5322#section-3.6.4
format!("<{}@{}>", Uuid::new_v4(), hostname),
format!("<{}@{}>", make_message_id(), hostname),
))
}
}
@@ -362,7 +405,7 @@ impl MessageBuilder {
/// Set [User-Agent
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
pub fn user_agent(self, id: String) -> Self {
self.header(header::UserAgent(id))
self.header(header::UserAgent::from(id))
}
/// Force specific envelope (by default it is derived from headers)
@@ -374,7 +417,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
@@ -388,7 +431,7 @@ impl MessageBuilder {
// Fail is missing correct originator (Sender or From)
match res.headers.get::<header::From>() {
Some(header::From(f)) => {
let from: Vec<Mailbox> = f.clone().into();
let from: Vec<Mailbox> = f.into();
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
return Err(EmailError::TooManyFrom);
}
@@ -409,47 +452,43 @@ 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>();
let body = body.into_body(maybe_encoding);
if !&body.is_ascii() {
return Err(EmailError::NonAsciiChars);
}
self.build(Body::Raw(body))
self.headers.set(body.encoding());
self.build(MessageBody::Raw(body.into_vec()))
}
/// 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`][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)))
}
}
/// Email message which can be formatted
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
#[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 {
@@ -478,12 +517,14 @@ impl Message {
impl EmailFormat for Message {
fn format(&self, out: &mut Vec<u8>) {
out.extend_from_slice(self.headers.to_string().as_bytes());
write!(out, "{}", self.headers)
.expect("A Write implementation panicked while formatting headers");
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)
}
}
}
@@ -495,13 +536,23 @@ impl Default for MessageBuilder {
}
}
/// Create a random message id.
/// (Not cryptographically random)
fn make_message_id() -> String {
iter::repeat_with(fastrand::alphanumeric).take(36).collect()
}
#[cfg(test)]
mod test {
use crate::message::{header, mailbox::Mailbox, Message};
use std::time::{Duration, SystemTime};
use super::{header, mailbox::Mailbox, make_message_id, 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]
@@ -509,7 +560,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());
}
@@ -518,13 +569,14 @@ 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());
}
#[test]
fn email_message() {
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
// Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
let email = Message::builder()
.date(date)
@@ -538,20 +590,80 @@ mod test {
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject("яңа ел белән!".into()))
.body("Happy new year!")
.header(header::Subject::from(String::from("яңа ел белән!")))
.body(String::from("Happy new year!"))
.unwrap();
assert_eq!(
String::from_utf8(email.formatted()).unwrap(),
concat!(
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
"To: Pony O.P. <pony@domain.tld>\r\n",
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Happy new year!"
)
);
}
#[test]
fn email_with_png() {
// Tue, 15 Nov 1994 08:12:31 GMT
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
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)
.body(String::from(
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
)),
)
.singlepart(
SinglePart::builder()
.header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentDisposition::inline())
.header(header::ContentId::from(String::from("<123>")))
.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 == 7 || i == 9 || i == 14 || i == 233 {
continue;
}
assert_eq!(line.0, line.1)
}
}
#[test]
fn test_make_message_id() {
let mut ids = std::collections::HashSet::with_capacity(10);
for _ in 0..1000 {
ids.insert(make_message_id());
}
// Ensure there are no duplicates
assert_eq!(1000, ids.len());
// Ensure correct length
for id in ids {
assert_eq!(36, id.len());
}
}
}

View File

@@ -1,60 +0,0 @@
// https://tools.ietf.org/html/rfc1522
fn allowed_char(c: char) -> bool {
c >= 1 as char && c <= 9 as char
|| c == 11 as char
|| c == 12 as char
|| c >= 14 as char && c <= 127 as char
}
pub fn encode(s: &str) -> String {
if s.chars().all(allowed_char) {
s.into()
} else {
format!("=?utf-8?b?{}?=", base64::encode(s))
}
}
pub fn decode(s: &str) -> Option<String> {
s.strip_prefix("=?utf-8?b?")
.and_then(|stripped| stripped.strip_suffix("?="))
.map_or_else(
|| Some(s.into()),
|stripped| {
let decoded = base64::decode(stripped).ok()?;
let decoded = String::from_utf8(decoded).ok()?;
Some(decoded)
},
)
}
#[cfg(test)]
mod test {
use super::{decode, encode};
#[test]
fn encode_ascii() {
assert_eq!(&encode("Kayo. ?"), "Kayo. ?");
}
#[test]
fn decode_ascii() {
assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into()));
}
#[test]
fn encode_utf8() {
assert_eq!(
&encode("Привет, мир!"),
"=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="
);
}
#[test]
fn decode_utf8() {
assert_eq!(
decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="),
Some("Привет, мир!".into())
);
}
}

View File

@@ -1,57 +1,96 @@
//! Error and result type for file transport
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
};
use crate::BoxError;
use std::{error::Error as StdError, fmt};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(&'static str),
/// IO error
Io(io::Error),
/// JSON serialization error
JsonSerialization(serde_json::Error),
/// The Errors that may occur when sending an email over SMTP
pub struct Error {
inner: Box<Inner>,
}
impl Display for 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),
struct Inner {
kind: Kind,
source: Option<BoxError>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
}
}
/// Returns true if the error is a file I/O error
pub fn is_io(&self) -> bool {
matches!(self.inner.kind, Kind::Io)
}
/// Returns true if the error is an envelope serialization or deserialization error
#[cfg(feature = "file-transport-envelope")]
pub fn is_envelope(&self) -> bool {
matches!(self.inner.kind, Kind::Envelope)
}
}
#[derive(Debug)]
pub(crate) enum Kind {
/// File I/O error
Io,
/// Envelope serialization/deserialization error
#[cfg(feature = "file-transport-envelope")]
Envelope,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::file::Error");
builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind {
Kind::Io => f.write_str("response error")?,
#[cfg(feature = "file-transport-envelope")]
Kind::Envelope => f.write_str("internal client error")?,
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
_ => None,
}
self.inner.source.as_ref().map(|e| {
let r: &(dyn std::error::Error + 'static) = &**e;
r
})
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
pub(crate) fn io<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Io, Some(e))
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::JsonSerialization(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
}
#[cfg(feature = "file-transport-envelope")]
pub(crate) fn envelope<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Envelope, Some(e))
}

View File

@@ -1,16 +1,16 @@
//! The file transport writes the emails to the given directory. The name of the file will be
//! `message_id.json`.
//! `message_id.eml`.
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//!
//! ## Sync example
//!
//! ```rust
//! # 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, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
@@ -19,7 +19,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let result = sender.send(&email);
//! assert!(result.is_ok());
@@ -30,25 +30,56 @@
//! # fn main() {}
//! ```
//!
//! ## Sync example with envelope
//!
//! ## Async tokio 0.2
//! 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,no_run
//! ```rust
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "tokio02", feature = "file-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn 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;
//! use lettre::{Tokio02Transport, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! 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("Be happy!")?;
//! .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());
@@ -60,20 +91,20 @@
//!
//! ```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::{AsyncStd1Transport, Message, FileTransport};
//! use lettre::{AsyncTransport, AsyncStd1Executor, Message, AsyncFileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! 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("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
@@ -83,32 +114,32 @@
//!
//! ---
//!
//! Example result
//! 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
//! {
//! "envelope": {
//! "forward_path": [
//! "hei@domain.tld"
//! ],
//! "reverse_path": "nobody@domain.tld"
//! },
//! "raw_message": null,
//! "message": "From: NoBody <nobody@domain.tld>\r\nReply-To: Yuin <yuin@domain.tld>\r\nTo: Hei <hei@domain.tld>\r\nSubject: Happy new year\r\nDate: Tue, 18 Aug 2020 22:50:17 GMT\r\n\r\nBe happy!"
//! }
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
//! ```
pub use self::error::Error;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use 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::{
path::{Path, PathBuf},
str,
@@ -122,51 +153,113 @@ type Id = String;
/// Writes the content and the envelope information to a file
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
pub struct FileTransport {
path: PathBuf,
#[cfg(feature = "file-transport-envelope")]
save_envelope: bool,
}
/// Asynchronously writes the content and the envelope information to a file
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
#[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,
}
}
/// 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).map_err(error::io)?;
let json_file = self.path.join(format!("{}.json", email_id));
let json = fs::read(&json_file).map_err(error::io)?;
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
Ok((envelope, eml))
}
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
self.path.join(format!("{}.{}", email_id, extension))
}
}
#[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>,
}
#[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,
}
}
impl FileTransport {
fn send_raw_impl(
&self,
envelope: &Envelope,
email: &[u8],
) -> Result<(Uuid, PathBuf, String), serde_json::Error> {
let email_id = Uuid::new_v4();
let file = self.path.join(format!("{}.json", email_id));
/// 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,
}
}
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),
}),
}?;
/// 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.map_err(error::io)?;
Ok((email_id, file, serialized))
let json_file = self.inner.path.join(format!("{}.json", email_id));
let json = E::fs_read(&json_file).await.map_err(error::io)?;
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
Ok((envelope, eml))
}
}
@@ -177,57 +270,52 @@ impl Transport for FileTransport {
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use std::fs;
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
let email_id = Uuid::new_v4();
let file = self.path(&email_id, "eml");
fs::write(file, email).map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")]
{
if self.save_envelope {
let file = self.path(&email_id, "json");
let buf = serde_json::to_string(&envelope).map_err(error::envelope)?;
fs::write(file, buf).map_err(error::io)?;
}
}
// use envelope anyway
let _ = envelope;
fs::write(file, serialized)?;
Ok(email_id.to_string())
}
}
#[cfg(feature = "async-std1")]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[async_trait]
impl AsyncStd1Transport 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> {
use async_std::fs;
let email_id = Uuid::new_v4();
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
let file = self.inner.path(&email_id, "eml");
E::fs_write(&file, email).await.map_err(error::io)?;
#[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).map_err(error::envelope)?;
E::fs_write(&file, &buf).await.map_err(error::io)?;
}
}
// use envelope anyway
let _ = envelope;
fs::write(file, serialized).await?;
Ok(email_id.to_string())
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for FileTransport {
type Ok = Id;
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio02_crate::fs;
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
fs::write(file, serialized).await?;
Ok(email_id.to_string())
}
}
#[cfg(feature = "tokio03")]
#[async_trait]
impl Tokio03Transport for FileTransport {
type Ok = Id;
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio03_crate::fs;
let (email_id, file, serialized) = self.send_raw_impl(envelope, email)?;
fs::write(file, serialized).await?;
Ok(email_id.to_string())
}
}

View File

@@ -1,20 +1,110 @@
//! ### Sending Messages
//! ## Transports for sending emails
//!
//! This section explains how to manipulate emails you have created.
//! This module contains `Transport`s for sending emails. A `Transport` implements a high-level API
//! for sending emails. It automatically manages the underlying resources and doesn't require any
//! specific knowledge of email protocols in order to be used.
//!
//! This mailer contains several different transports for your emails. To be sendable, the
//! emails have to implement `Email`, which is the case for emails created with `lettre::builder`.
//! ### Getting started
//!
//! Sending emails from your programs requires using an email relay, as client libraries are not
//! designed to handle email delivery by themselves. Depending on your infrastructure, your relay
//! could be:
//!
//! * a service from your Cloud or hosting provider
//! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either
//! locally on your servers or accessible over the network
//! * a dedicated external service, like Mailchimp, Mailgun, etc.
//!
//! In most cases, the best option is to:
//!
//! * Use the [`SMTP`] transport, with the [`relay`] builder (or one of its async counterparts)
//! with your server's hostname. They provide modern and secure defaults.
//! * Use the [`credentials`] method of the builder to pass your credentials.
//!
//! These should be enough to safely cover most use cases.
//!
//! ### Available transports
//!
//! The following transports are available:
//!
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
//! the preferred way of sending emails.
//! * The `SendmailTransport` uses the sendmail command to send messages. It is an alternative to
//! the SMTP transport.
//! * The `FileTransport` creates a file containing the email content to be sent. It can be used
//! for debugging or if you want to keep all sent emails.
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the
//! logs.
//! | Module | Protocol | Sync API | Async API | Description |
//! | ------------ | -------- | --------------------- | -------------------------- | ------------------------------------------------------- |
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
//! | [`stub`] | Debug | [`StubTransport`] | [`StubTransport`] | Drops the email - Useful for debugging |
//!
//! ## Building an email
//!
//! Emails can either be built though [`Message`], which is a typed API for constructing emails
//! (find out more about it by going over the [`message`][crate::message] module),
//! or via external means.
//!
//! [`Message`]s can be sent via [`Transport::send`] or [`AsyncTransport::send`], while messages
//! built without lettre's [`message`][crate::message] APIs can be sent via [`Transport::send_raw`]
//! or [`AsyncTransport::send_raw`].
//!
//! ## Brief example
//!
//! This example shows how to build an email and send it via an SMTP relay server.
//! It is in no way a complete example, but it shows how to get started with lettre.
//! More examples can be found by looking at the specific modules, linked in the _Module_ column
//! of the [table above](#transports-for-sending-emails).
//!
//! ```rust,no_run
//! # use std::error::Error;
//! #
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::transport::smtp::authentication::Credentials;
//! use lettre::{Message, SmtpTransport, Transport};
//!
//! 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 creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
//!
//! // Open a remote connection to the SMTP relay server
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
//! .credentials(creds)
//! .build();
//!
//! // Send the email
//! match mailer.send(&email) {
//! Ok(_) => println!("Email sent successfully!"),
//! Err(e) => panic!("Could not send email: {:?}", e),
//! }
//! # Ok(())
//! # }
//! # #[cfg(not(all(feature = "builder", feature = "smtp-transport")))]
//! # fn main() {}
//! ```
//!
//! [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent
//! [`SMTP`]: crate::transport::smtp
//! [`relay`]: crate::SmtpTransport::relay
//! [`starttls_relay`]: crate::SmtpTransport::starttls_relay
//! [`credentials`]: crate::transport::smtp::SmtpTransportBuilder::credentials
//! [`Message`]: crate::Message
//! [`file`]: self::file
//! [`SmtpTransport`]: crate::SmtpTransport
//! [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
//! [`SendmailTransport`]: crate::SendmailTransport
//! [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
//! [`FileTransport`]: crate::FileTransport
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
//! [`StubTransport`]: crate::transport::stub::StubTransport
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use async_trait::async_trait;
use crate::Envelope;
#[cfg(feature = "builder")]
use crate::Message;
#[cfg(feature = "file-transport")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
@@ -26,3 +116,47 @@ pub mod sendmail;
#[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

@@ -1,52 +1,92 @@
//! Error and result type for sendmail transport
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
string::FromUtf8Error,
};
use crate::BoxError;
use std::{error::Error as StdError, fmt};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(String),
/// Error parsing UTF8 in response
Utf8Parsing(FromUtf8Error),
/// IO error
Io(io::Error),
/// The Errors that may occur when sending an email over sendmail
pub struct Error {
inner: Box<Inner>,
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
Client(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Io(ref err) => err.fmt(fmt),
struct Inner {
kind: Kind,
source: Option<BoxError>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
}
}
/// Returns true if the error is from client
pub fn is_client(&self) -> bool {
matches!(self.inner.kind, Kind::Client)
}
/// Returns true if the error comes from the response
pub fn is_response(&self) -> bool {
matches!(self.inner.kind, Kind::Response)
}
}
#[derive(Debug)]
pub(crate) enum Kind {
/// Error parsing a response
Response,
/// Internal client error
Client,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::sendmail::Error");
builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind {
Kind::Response => f.write_str("response error")?,
Kind::Client => f.write_str("internal client error")?,
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Io(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
_ => None,
}
self.inner.source.as_ref().map(|e| {
let r: &(dyn std::error::Error + 'static) = &**e;
r
})
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Response, Some(e))
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Client, Some(e))
}

View File

@@ -1,20 +1,20 @@
//! The sendmail transport sends the email using the local sendmail command.
//! The sendmail transport sends the email using the local `sendmail` command.
//!
//! ## Sync example
//!
//! ```rust
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! # use lettre::{Message, Transport, SendmailTransport};
//! use lettre::{Message, SendmailTransport, Transport};
//!
//! 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("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let sender = SendmailTransport::new();
//! let result = sender.send(&email);
@@ -30,19 +30,42 @@
//!
//! ```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, Tokio02Transport, SendmailTransport};
//! 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("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let sender = SendmailTransport::new();
//! 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(())
@@ -53,19 +76,19 @@
//!
//!```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, AsyncStd1Transport, SendmailTransport};
//! 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("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let sender = SendmailTransport::new();
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
@@ -73,38 +96,55 @@
//! ```
pub use self::error::Error;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
use crate::AsyncStd1Executor;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio03"))]
use 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::{
ffi::OsString,
io::prelude::*,
io::Write,
process::{Command, Stdio},
};
mod error;
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
const DEFAULT_SENDMAIL: &str = "/usr/sbin/sendmail";
/// Sends an email using the `sendmail` command
/// Sends emails using the `sendmail` command
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
pub struct SendmailTransport {
command: OsString,
}
/// Asynchronously sends emails using the `sendmail` command
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
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 {
SendmailTransport {
command: DEFAUT_SENDMAIL.into(),
command: DEFAULT_SENDMAIL.into(),
}
}
@@ -128,12 +168,34 @@ impl SendmailTransport {
.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.command);
let mut c = Command::new(&self.inner.command);
c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
@@ -147,11 +209,11 @@ impl SendmailTransport {
c
}
#[cfg(feature = "tokio03")]
fn tokio03_command(&self, envelope: &Envelope) -> tokio03_crate::process::Command {
use tokio03_crate::process::Command;
#[cfg(feature = "tokio1")]
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
use tokio1_crate::process::Command;
let mut c = Command::new(&self.command);
let mut c = Command::new(&self.inner.command);
c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
@@ -164,6 +226,25 @@ impl SendmailTransport {
.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 {
@@ -172,56 +253,76 @@ impl Default for SendmailTransport {
}
}
#[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;
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
// Spawn the sendmail command
let mut process = self.command(envelope).spawn()?;
let mut process = self.command(envelope).spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(email)?;
let output = process.wait_with_output()?;
process
.stdin
.as_mut()
.unwrap()
.write_all(email)
.map_err(error::client)?;
let output = process.wait_with_output().map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
Err(error::Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}
#[cfg(feature = "async-std1")]
#[async_trait]
impl AsyncStd1Transport for SendmailTransport {
impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut command = self.command(envelope);
let email = email.to_vec();
use async_std::io::prelude::WriteExt;
// TODO: Convert to real async, once async-std has a process implementation.
let output = async_std::task::spawn_blocking(move || {
// Spawn the sendmail command
let mut process = command.spawn()?;
let mut command = self.async_std_command(envelope);
process.stdin.as_mut().unwrap().write_all(&email)?;
process.wait_with_output()
})
.await?;
// Spawn the sendmail command
let mut process = command.spawn().map_err(error::client)?;
process
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.await
.map_err(error::client)?;
let output = process.output().await.map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for SendmailTransport {
impl AsyncTransport for AsyncSendmailTransport<Tokio02Executor> {
type Ok = ();
type Error = Error;
@@ -231,40 +332,54 @@ impl Tokio02Transport for SendmailTransport {
let mut command = self.tokio02_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
let mut process = command.spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
process
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.await
.map_err(error::client)?;
let output = process.wait_with_output().await.map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}
#[cfg(feature = "tokio03")]
#[cfg(feature = "tokio1")]
#[async_trait]
impl Tokio03Transport for SendmailTransport {
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 tokio03_crate::io::AsyncWriteExt;
use tokio1_crate::io::AsyncWriteExt;
let mut command = self.tokio03_command(envelope);
let mut command = self.tokio1_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
let mut process = command.spawn().map_err(error::client)?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
process
.stdin
.as_mut()
.unwrap()
.write_all(&email)
.await
.map_err(error::client)?;
let output = process.wait_with_output().await.map_err(error::client)?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}

View File

@@ -1,26 +1,36 @@
use std::{
fmt::{self, Debug},
marker::PhantomData,
};
use async_trait::async_trait;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
use super::Tls;
use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
};
use crate::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Executor;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
#[cfg(feature = "tokio03")]
use crate::Tokio03Transport;
use crate::Tokio02Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Executor;
use crate::{Envelope, Executor};
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct AsyncSmtpTransport<C> {
/// Asynchronously sends emails using the SMTP protocol
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
pub struct AsyncSmtpTransport<E> {
// TODO: pool
inner: AsyncSmtpClient<C>,
inner: AsyncSmtpClient<E>,
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
impl AsyncTransport for AsyncSmtpTransport<Tokio02Executor> {
type Ok = Response;
type Error = Error;
@@ -36,9 +46,9 @@ impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
}
}
#[cfg(feature = "tokio03")]
#[cfg(feature = "tokio1")]
#[async_trait]
impl Tokio03Transport for AsyncSmtpTransport<Tokio03Connector> {
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
type Ok = Response;
type Error = Error;
@@ -54,11 +64,29 @@ impl Tokio03Transport for AsyncSmtpTransport<Tokio03Connector> {
}
}
impl<C> AsyncSmtpTransport<C>
#[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
C: AsyncSmtpConnector,
E: Executor,
{
/// Simple and secure transport, using TLS connections to comunicate with the SMTP server
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
///
/// The right option for most SMTP servers.
///
@@ -67,11 +95,23 @@ where
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
)))
)]
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{TlsParameters, SUBMISSIONS_PORT};
use super::{Tls, TlsParameters, SUBMISSIONS_PORT};
let tls_parameters = TlsParameters::new(relay.into())?;
@@ -94,11 +134,23 @@ where
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-rustls-tls"
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
)))
)]
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
use super::{TlsParameters, SUBMISSION_PORT};
use super::{Tls, TlsParameters, SUBMISSION_PORT};
let tls_parameters = TlsParameters::new(relay.into())?;
@@ -110,7 +162,7 @@ where
/// Creates a new local SMTP client to port 25
///
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
pub fn unencrypted_localhost() -> AsyncSmtpTransport<C> {
pub fn unencrypted_localhost() -> AsyncSmtpTransport<E> {
Self::builder_dangerous("localhost").build()
}
@@ -126,15 +178,40 @@ where
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
let mut new = SmtpInfo::default();
new.server = server.into();
let new = SmtpInfo {
server: server.into(),
..Default::default()
};
AsyncSmtpTransportBuilder { info: new }
}
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
impl<E> Debug for AsyncSmtpTransport<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("AsyncSmtpTransport");
builder.field("inner", &self.inner);
builder.finish()
}
}
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`].
#[derive(Debug, Clone)]
#[cfg_attr(
docsrs,
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
)]
pub struct AsyncSmtpTransportBuilder {
info: SmtpInfo,
}
@@ -169,44 +246,54 @@ impl AsyncSmtpTransportBuilder {
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-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: Tls) -> Self {
#[cfg_attr(
docsrs,
doc(cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-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<C>(self) -> AsyncSmtpTransport<C>
/// Build the transport
pub fn build<E>(self) -> AsyncSmtpTransport<E>
where
C: AsyncSmtpConnector,
E: Executor,
{
let connector = Default::default();
let client = AsyncSmtpClient {
connector,
info: self.info,
marker_: PhantomData,
};
AsyncSmtpTransport { inner: client }
}
}
/// Build client
#[derive(Clone)]
pub struct AsyncSmtpClient<C> {
connector: C,
pub struct AsyncSmtpClient<E> {
info: SmtpInfo,
marker_: PhantomData<E>,
}
impl<C> AsyncSmtpClient<C>
impl<E> AsyncSmtpClient<E>
where
C: AsyncSmtpConnector,
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 = C::connect(
let mut conn = E::connect(
&self.info.server,
self.info.port,
&self.info.hello_name,
@@ -221,108 +308,22 @@ where
}
}
#[async_trait]
pub trait AsyncSmtpConnector: Default + private::Sealed {
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error>;
}
#[derive(Debug, Copy, Clone, Default)]
#[cfg(feature = "tokio02")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
pub struct Tokio02Connector;
#[async_trait]
#[cfg(feature = "tokio02")]
impl AsyncSmtpConnector for Tokio02Connector {
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
.await?;
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
}
Ok(conn)
impl<E> Debug for AsyncSmtpClient<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("AsyncSmtpClient");
builder.field("info", &self.info);
builder.finish()
}
}
#[derive(Debug, Copy, Clone, Default)]
#[cfg(feature = "tokio03")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio03")))]
pub struct Tokio03Connector;
#[async_trait]
#[cfg(feature = "tokio03")]
impl AsyncSmtpConnector for Tokio03Connector {
async fn connect(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls: &Tls,
) -> Result<AsyncSmtpConnection, Error> {
#[allow(clippy::match_single_binding)]
let tls_parameters = match tls {
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
_ => None,
};
#[allow(unused_mut)]
let mut conn =
AsyncSmtpConnection::connect_tokio03(hostname, port, hello_name, tls_parameters)
.await?;
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
match tls {
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
}
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters.clone(), hello_name).await?;
}
_ => (),
impl<E> AsyncSmtpClient<E>
where
E: Executor,
{
fn clone(&self) -> Self {
Self {
info: self.info.clone(),
marker_: PhantomData,
}
Ok(conn)
}
}
mod private {
use super::*;
pub trait Sealed {}
#[cfg(feature = "tokio02")]
impl Sealed for Tokio02Connector {}
#[cfg(feature = "tokio03")]
impl Sealed for Tokio03Connector {}
}

View File

@@ -1,14 +1,14 @@
//! Provides limited SASL authentication mechanisms
use crate::transport::smtp::error::Error;
use std::fmt::{self, Display, Formatter};
use crate::transport::smtp::error::{self, Error};
use std::fmt::{self, Debug, Display, Formatter};
/// Accepted authentication mechanisms
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[derive(PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Credentials {
authentication_identity: String,
@@ -35,19 +35,26 @@ where
}
}
impl Debug for Credentials {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Credentials").finish()
}
}
/// 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,
}
@@ -79,15 +86,15 @@ impl Mechanism {
) -> Result<String, Error> {
match self {
Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
None => Ok(format!(
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
)),
},
Mechanism::Login => {
let decoded_challenge =
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
let decoded_challenge = challenge
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.authentication_identity.to_string());
@@ -97,10 +104,10 @@ impl Mechanism {
return Ok(credentials.secret.to_string());
}
Err(Error::Client("Unrecognized challenge"))
Err(error::client("Unrecognized challenge"))
}
Mechanism::Xoauth2 => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
None => Ok(format!(
"user={}\x01auth=Bearer {}\x01\x01",
credentials.authentication_identity, credentials.secret

View File

@@ -1,18 +1,17 @@
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::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
Envelope,
};
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use std::fmt::Display;
#[cfg(feature = "tracing")]
use super::escape_crlf;
@@ -62,14 +61,28 @@ impl AsyncSmtpConnection {
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio03")]
pub async fn connect_tokio03(
#[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_tokio03(hostname, port, tls_parameters).await?;
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
}
@@ -98,9 +111,32 @@ impl AsyncSmtpConnection {
// Mail
let mut mail_options = vec![];
if self.server_info().supports_feature(Extension::EightBitMime) {
// 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,
@@ -149,7 +185,7 @@ impl AsyncSmtpConnection {
try_smtp!(self.ehlo(hello_name).await, self);
Ok(())
} else {
Err(Error::Client("STARTTLS is not supported on this server"))
Err(error::client("STARTTLS is not supported on this server"))
}
}
@@ -196,12 +232,10 @@ impl AsyncSmtpConnection {
let mechanism = self
.server_info
.get_auth_mechanism(mechanisms)
.ok_or(Error::Client(
"No compatible authentication mechanism was found",
))?;
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
// Limit challenges to avoid blocking
let mut challenges = 10;
let mut challenges: u8 = 10;
let mut response = self
.command(Auth::new(mechanism, credentials.clone(), None)?)
.await?;
@@ -220,7 +254,7 @@ impl AsyncSmtpConnection {
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
Err(error::response("Unexpected number of challenges"))
} else {
Ok(response)
}
@@ -244,8 +278,16 @@ impl AsyncSmtpConnection {
/// 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?;
self.stream
.get_mut()
.write_all(string)
.await
.map_err(error::network)?;
self.stream
.get_mut()
.flush()
.await
.map_err(error::network)?;
#[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
@@ -256,27 +298,33 @@ impl AsyncSmtpConnection {
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 {
while self
.stream
.read_line(&mut buffer)
.await
.map_err(error::network)?
> 0
{
#[cfg(feature = "tracing")]
tracing::debug!("<< {}", escape_crlf(&buffer));
match parse_response(&buffer) {
Ok((_remaining, response)) => {
if response.is_positive() {
return Ok(response);
return if response.is_positive() {
Ok(response)
} else {
Err(error::code(response.code))
}
return Err(response.into());
}
Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.code));
return Err(error::response(e.to_string()));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.code));
return Err(error::response(e.to_string()));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
Err(error::response("incomplete response"))
}
}

View File

@@ -1,40 +1,51 @@
#[cfg(any(feature = "tokio02-rustls-tls", feature = "tokio03-rustls-tls"))]
use std::sync::Arc;
use std::{
net::{Shutdown, SocketAddr},
mem,
net::SocketAddr,
pin::Pin,
task::{Context, Poll},
};
use futures_io::{Error as IoError, ErrorKind, Result as IoResult};
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 = "tokio03")]
use tokio03_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio03ReadBuf};
#[cfg(feature = "tokio03")]
use tokio03_crate::net::TcpStream as Tokio03TcpStream;
#[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 = "tokio03-native-tls")]
use tokio03_native_tls_crate::TlsStream as Tokio03TlsStream;
#[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 = "tokio03-rustls-tls")]
use tokio03_rustls::client::TlsStream as Tokio03RustlsTlsStream;
#[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio03-native-tls",
feature = "tokio03-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;
use crate::transport::smtp::{error, Error};
/// A network stream
pub struct AsyncNetworkStream {
@@ -56,15 +67,24 @@ enum InnerAsyncNetworkStream {
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-rustls-tls")]
Tokio02RustlsTls(Tokio02RustlsTlsStream<Tokio02TcpStream>),
/// Plain Tokio 0.3 TCP stream
#[cfg(feature = "tokio03")]
Tokio03Tcp(Tokio03TcpStream),
/// Encrypted Tokio 0.3 TCP stream
#[cfg(feature = "tokio03-native-tls")]
Tokio03NativeTls(Tokio03TlsStream<Tokio03TcpStream>),
/// Encrypted Tokio 0.3 TCP stream
#[cfg(feature = "tokio03-rustls-tls")]
Tokio03RustlsTls(Tokio03RustlsTlsStream<Tokio03TcpStream>),
/// 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,
}
@@ -89,14 +109,20 @@ impl AsyncNetworkStream {
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref s) => {
#[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 = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref s) => s.get_ref().0.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(
@@ -107,39 +133,15 @@ impl AsyncNetworkStream {
}
}
/// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> IoResult<()> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.shutdown(how),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().shutdown(how)
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.shutdown(how),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref s) => s.shutdown(how),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().shutdown(how)
}
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref s) => s.get_ref().0.shutdown(how),
InnerAsyncNetworkStream::None => {
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
Ok(())
}
}
}
#[cfg(feature = "tokio02")]
pub async fn connect_tokio02(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio02TcpStream::connect((hostname, port)).await?;
let tcp_stream = Tokio02TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
@@ -148,15 +150,34 @@ impl AsyncNetworkStream {
Ok(stream)
}
#[cfg(feature = "tokio03")]
pub async fn connect_tokio03(
#[cfg(feature = "tokio1")]
pub async fn connect_tokio1(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio03TcpStream::connect((hostname, port)).await?;
let tcp_stream = Tokio1TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio03Tcp(tcp_stream));
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
.map_err(error::connection)?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
@@ -177,34 +198,61 @@ impl AsyncNetworkStream {
#[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 = 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?;
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters)
.await
.map_err(error::connection)?;
Ok(())
}
#[cfg(all(
feature = "tokio03",
not(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))
feature = "tokio1",
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio03Tcp(_) => {
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio03-native-tls or the tokio03-rustls-tls feature");
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
}
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
InnerAsyncNetworkStream::Tokio03Tcp(_) => {
#[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 = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio03Tcp(tcp_stream) => tcp_stream,
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio03_tls(tcp_stream, tls_parameters).await?;
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
.await
.map_err(error::connection)?;
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 = 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
.map_err(error::connection)?;
Ok(())
}
_ => Ok(()),
@@ -217,7 +265,7 @@ impl AsyncNetworkStream {
tcp_stream: Tokio02TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
let domain = mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
@@ -230,7 +278,10 @@ impl AsyncNetworkStream {
use tokio02_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
let stream = connector
.connect(&domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
};
}
@@ -243,10 +294,14 @@ impl AsyncNetworkStream {
return {
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let domain =
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
};
}
@@ -254,42 +309,101 @@ impl AsyncNetworkStream {
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio03-native-tls", feature = "tokio03-rustls-tls"))]
async fn upgrade_tokio03_tls(
tcp_stream: Tokio03TcpStream,
#[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);
let domain = mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio03-native-tls"))]
panic!("built without the tokio03-native-tls feature");
#[cfg(not(feature = "tokio1-native-tls"))]
panic!("built without the tokio1-native-tls feature");
#[cfg(feature = "tokio03-native-tls")]
#[cfg(feature = "tokio1-native-tls")]
return {
use tokio03_native_tls_crate::TlsConnector;
use tokio1_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio03NativeTls(stream))
let stream = connector
.connect(&domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio03-rustls-tls"))]
panic!("built without the tokio03-rustls-tls feature");
#[cfg(not(feature = "tokio1-rustls-tls"))]
panic!("built without the tokio1-rustls-tls feature");
#[cfg(feature = "tokio03-rustls-tls")]
#[cfg(feature = "tokio1-rustls-tls")]
return {
use tokio03_rustls::{webpki::DNSNameRef, TlsConnector};
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let domain =
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio03RustlsTls(stream))
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.await
.map_err(error::connection)?;
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 = 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).map_err(error::connection)?;
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
};
}
}
@@ -303,18 +417,24 @@ impl AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(_) => false,
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(_) => true,
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(_) => true,
#[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 futures_io::AsyncRead for AsyncNetworkStream {
impl FuturesAsyncRead for AsyncNetworkStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
@@ -327,33 +447,43 @@ impl futures_io::AsyncRead for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
#[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 = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
#[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 = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => {
let mut b = Tokio03ReadBuf::new(buf);
#[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))
@@ -362,7 +492,7 @@ impl futures_io::AsyncRead for AsyncNetworkStream {
}
}
impl futures_io::AsyncWrite for AsyncNetworkStream {
impl FuturesAsyncWrite for AsyncNetworkStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
@@ -375,12 +505,22 @@ impl futures_io::AsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[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))
@@ -396,12 +536,18 @@ impl futures_io::AsyncWrite for AsyncNetworkStream {
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03")]
InnerAsyncNetworkStream::Tokio03Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03-native-tls")]
InnerAsyncNetworkStream::Tokio03NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio03-rustls-tls")]
InnerAsyncNetworkStream::Tokio03RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[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(()))
@@ -409,7 +555,30 @@ impl futures_io::AsyncWrite for AsyncNetworkStream {
}
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<IoResult<()>> {
Poll::Ready(self.shutdown(Shutdown::Write))
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

@@ -6,13 +6,16 @@ use std::{
};
use super::{ClientCodec, NetworkStream, TlsParameters};
use crate::address::Envelope;
use crate::transport::smtp::{
authentication::{Credentials, Mechanism},
commands::*,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
use crate::{
address::Envelope,
transport::smtp::{
authentication::{Credentials, Mechanism},
commands::*,
error,
error::Error,
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::{parse_response, Response},
},
};
#[cfg(feature = "tracing")]
@@ -64,7 +67,7 @@ impl SmtpConnection {
panic: false,
server_info: ServerInfo::default(),
};
conn.set_timeout(timeout)?;
conn.set_timeout(timeout).map_err(error::network)?;
// TODO log
let _response = conn.read_response()?;
@@ -80,9 +83,32 @@ impl SmtpConnection {
// Mail
let mut mail_options = vec![];
if self.server_info().supports_feature(Extension::EightBitMime) {
// 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
@@ -131,7 +157,7 @@ impl SmtpConnection {
// when a TLS library is enabled
unreachable!("TLS support required but not supported");
} else {
Err(Error::Client("STARTTLS is not supported on this server"))
Err(error::client("STARTTLS is not supported on this server"))
}
}
@@ -184,9 +210,7 @@ impl SmtpConnection {
let mechanism = self
.server_info
.get_auth_mechanism(mechanisms)
.ok_or(Error::Client(
"No compatible authentication mechanism was found",
))?;
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
// Limit challenges to avoid blocking
let mut challenges = 10;
@@ -205,7 +229,7 @@ impl SmtpConnection {
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
Err(error::response("Unexpected number of challenges"))
} else {
Ok(response)
}
@@ -229,8 +253,11 @@ impl SmtpConnection {
/// 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()?;
self.stream
.get_mut()
.write_all(string)
.map_err(error::network)?;
self.stream.get_mut().flush().map_err(error::network)?;
#[cfg(feature = "tracing")]
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
@@ -241,27 +268,27 @@ impl SmtpConnection {
pub fn read_response(&mut self) -> Result<Response, Error> {
let mut buffer = String::with_capacity(100);
while self.stream.read_line(&mut buffer)? > 0 {
while self.stream.read_line(&mut buffer).map_err(error::network)? > 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());
return if response.is_positive() {
Ok(response)
} else {
Err(error::code(response.code))
};
}
Err(nom::Err::Failure(e)) => {
return Err(Error::Parsing(e.code));
return Err(error::response(e.to_string()));
}
Err(nom::Err::Incomplete(_)) => { /* read more */ }
Err(nom::Err::Error(e)) => {
return Err(Error::Parsing(e.code));
return Err(error::response(e.to_string()));
}
}
}
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
Err(error::response("incomplete response"))
}
}

View File

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

View File

@@ -27,32 +27,30 @@
#[cfg(feature = "serde")]
use std::fmt::Debug;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
pub(crate) use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
#[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},
};
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
mod async_connection;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
#[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,
}

View File

@@ -1,7 +1,6 @@
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
use std::{
io::{self, Read, Write},
mem,
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
time::Duration,
};
@@ -14,8 +13,8 @@ use rustls::{ClientSession, StreamOwned};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::InnerTlsParameters;
use super::{MockStream, TlsParameters};
use crate::transport::smtp::Error;
use super::TlsParameters;
use crate::transport::smtp::{error, Error};
/// A network stream
pub struct NetworkStream {
@@ -35,17 +34,17 @@ enum InnerNetworkStream {
/// Encrypted TCP stream
#[cfg(feature = "rustls-tls")]
RustlsTls(StreamOwned<ClientSession, TcpStream>),
/// Mock stream
Mock(MockStream),
/// Can't be built
None,
}
impl NetworkStream {
fn new(inner: InnerNetworkStream) -> Self {
NetworkStream { inner }
}
if let InnerNetworkStream::None = inner {
debug_assert!(false, "InnerNetworkStream::None must never be built");
}
pub fn new_mock(mock: MockStream) -> Self {
Self::new(InnerNetworkStream::Mock(mock))
NetworkStream { inner }
}
/// Returns peer's address
@@ -56,10 +55,13 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
InnerNetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
))),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
)))
}
}
}
@@ -71,7 +73,10 @@ impl NetworkStream {
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
InnerNetworkStream::Mock(_) => Ok(()),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
}
}
@@ -84,18 +89,18 @@ impl NetworkStream {
server: T,
timeout: Duration,
) -> Result<TcpStream, Error> {
let addrs = server.to_socket_addrs()?;
let addrs = server.to_socket_addrs().map_err(error::connection)?;
for addr in addrs {
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
return Ok(result);
}
}
Err(Error::Client("Could not connect"))
Err(error::connection("Could not connect"))
}
let tcp_stream = match timeout {
Some(t) => try_connect_timeout(server, t)?,
None => TcpStream::connect(server)?,
None => TcpStream::connect(server).map_err(error::connection)?,
};
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
@@ -116,8 +121,7 @@ impl NetworkStream {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
InnerNetworkStream::Tcp(_) => {
// get owned TcpStream
let tcp_stream =
std::mem::replace(&mut self.inner, InnerNetworkStream::Mock(MockStream::new()));
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
@@ -140,18 +144,16 @@ impl NetworkStream {
InnerTlsParameters::NativeTls(connector) => {
let stream = connector
.connect(tls_parameters.domain(), tcp_stream)
.map_err(|err| Error::Io(io::Error::new(io::ErrorKind::Other, err)))?;
.map_err(error::connection)?;
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,
);
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())
.map_err(error::connection)?;
let stream = StreamOwned::new(ClientSession::new(&connector, domain), tcp_stream);
InnerNetworkStream::RustlsTls(stream)
}
@@ -160,11 +162,15 @@ impl NetworkStream {
pub fn is_encrypted(&self) -> bool {
match self.inner {
InnerNetworkStream::Tcp(_) | InnerNetworkStream::Mock(_) => false,
InnerNetworkStream::Tcp(_) => false,
#[cfg(feature = "native-tls")]
InnerNetworkStream::NativeTls(_) => true,
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(_) => true,
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
false
}
}
}
@@ -179,7 +185,10 @@ impl NetworkStream {
InnerNetworkStream::RustlsTls(ref mut stream) => {
stream.get_ref().set_read_timeout(duration)
}
InnerNetworkStream::Mock(_) => Ok(()),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
}
}
@@ -197,7 +206,10 @@ impl NetworkStream {
stream.get_ref().set_write_timeout(duration)
}
InnerNetworkStream::Mock(_) => Ok(()),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
}
}
}
@@ -210,7 +222,10 @@ impl Read for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
InnerNetworkStream::Mock(ref mut s) => s.read(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
}
}
}
@@ -223,7 +238,10 @@ impl Write for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
InnerNetworkStream::Mock(ref mut s) => s.write(buf),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(0)
}
}
}
@@ -234,7 +252,10 @@ impl Write for NetworkStream {
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
#[cfg(feature = "rustls-tls")]
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
InnerNetworkStream::Mock(ref mut s) => s.flush(),
InnerNetworkStream::None => {
debug_assert!(false, "InnerNetworkStream::None must never be built");
Ok(())
}
}
}
}

View File

@@ -1,15 +1,15 @@
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use crate::transport::smtp::{error, Error};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "rustls-tls")]
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
use std::fmt::{self, Debug};
#[cfg(feature = "rustls-tls")]
use std::sync::Arc;
#[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
@@ -24,18 +24,34 @@ pub enum Tls {
None,
/// Start with insecure connection and use `STARTTLS` when available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(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"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
Required(TlsParameters),
/// Use TLS wrapped connection
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
Wrapper(TlsParameters),
}
impl Debug for Tls {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::None => f.pad("None"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Self::Opportunistic(_) => f.pad("Opportunistic"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Self::Required(_) => f.pad("Required"),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Self::Wrapper(_) => f.pad("Wrapper"),
}
}
}
/// 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
@@ -43,7 +59,7 @@ pub struct TlsParameters {
}
/// Builder for `TlsParameters`
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct TlsParametersBuilder {
domain: String,
root_certs: Vec<Certificate>,
@@ -65,7 +81,7 @@ impl TlsParametersBuilder {
/// Add a custom root certificate
///
/// Can be used to safely connect to a server using a self signed certificate, for example.
pub fn add_root_certificate(&mut self, cert: Certificate) -> &mut Self {
pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
self.root_certs.push(cert);
self
}
@@ -85,10 +101,7 @@ impl TlsParametersBuilder {
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
#[cfg(feature = "native-tls")]
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
pub fn dangerous_accept_invalid_hostnames(
&mut self,
accept_invalid_hostnames: bool,
) -> &mut Self {
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
self.accept_invalid_hostnames = accept_invalid_hostnames;
self
}
@@ -109,7 +122,7 @@ impl TlsParametersBuilder {
///
/// This method should only be used as a last resort, as it introduces
/// significant vulnerabilities to man-in-the-middle attacks.
pub fn dangerous_accept_invalid_certs(&mut self, accept_invalid_certs: bool) -> &mut Self {
pub fn dangerous_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
self.accept_invalid_certs = accept_invalid_certs;
self
}
@@ -119,11 +132,11 @@ impl TlsParametersBuilder {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn build(self) -> Result<TlsParameters, Error> {
#[cfg(feature = "native-tls")]
return self.build_native();
#[cfg(not(feature = "native-tls"))]
#[cfg(feature = "rustls-tls")]
return self.build_rustls();
#[cfg(not(feature = "rustls-tls"))]
return self.build_native();
}
/// Creates a new `TlsParameters` using native-tls with the provided configuration
@@ -139,7 +152,7 @@ impl TlsParametersBuilder {
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()?;
let connector = tls_builder.build().map_err(error::tls)?;
Ok(TlsParameters {
connector: InnerTlsParameters::NativeTls(connector),
domain: self.domain,
@@ -156,9 +169,7 @@ impl TlsParametersBuilder {
for cert in self.root_certs {
for rustls_cert in cert.rustls {
tls.root_store
.add(&rustls_cert)
.map_err(|_| Error::InvalidCertificate)?;
tls.root_store.add(&rustls_cert).map_err(error::tls)?;
}
}
if self.accept_invalid_certs {
@@ -168,7 +179,7 @@ impl TlsParametersBuilder {
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
Ok(TlsParameters {
connector: InnerTlsParameters::RustlsTls(tls),
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
domain: self.domain,
})
}
@@ -179,7 +190,7 @@ pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")]
NativeTls(TlsConnector),
#[cfg(feature = "rustls-tls")]
RustlsTls(ClientConfig),
RustlsTls(Arc<ClientConfig>),
}
impl TlsParameters {
@@ -224,12 +235,12 @@ pub struct Certificate {
rustls: Vec<rustls::Certificate>,
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
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)?;
let native_tls_cert = native_tls::Certificate::from_der(&der).map_err(error::tls)?;
Ok(Self {
#[cfg(feature = "native-tls")]
@@ -242,8 +253,7 @@ impl Certificate {
/// 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)?;
let native_tls_cert = native_tls::Certificate::from_pem(pem).map_err(error::tls)?;
#[cfg(feature = "rustls-tls")]
let rustls_cert = {
@@ -251,7 +261,7 @@ impl Certificate {
use std::io::Cursor;
let mut pem = Cursor::new(pem);
pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)?
pemfile::certs(&mut pem).map_err(|_| error::tls("invalid certificates"))?
};
Ok(Self {
@@ -263,6 +273,12 @@ impl Certificate {
}
}
impl Debug for Certificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Certificate").finish()
}
}
#[cfg(feature = "rustls-tls")]
struct InvalidCertsVerifier;

View File

@@ -1,13 +1,13 @@
//! SMTP commands
use crate::{
address::Address,
transport::smtp::{
authentication::{Credentials, Mechanism},
error::Error,
error::{self, Error},
extension::{ClientId, MailParameter, RcptParameter},
response::Response,
},
Address,
};
use std::fmt::{self, Display, Formatter};
@@ -55,7 +55,7 @@ impl Display for Mail {
write!(
f,
"MAIL FROM:<{}>",
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
self.sender.as_ref().map_or("", |s| s.as_ref())
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
@@ -261,16 +261,17 @@ impl Auth {
response: &Response,
) -> Result<Auth, Error> {
if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge"));
return Err(error::response("Expecting a challenge"));
}
let encoded_challenge = response
.first_word()
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
.ok_or_else(|| error::response("Could not read auth challenge"))?;
#[cfg(feature = "tracing")]
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
let decoded_base64 = base64::decode(&encoded_challenge).map_err(error::response)?;
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
#[cfg(feature = "tracing")]
tracing::debug!("auth decoded challenge: {}", decoded_challenge);

View File

@@ -1,162 +1,184 @@
//! Error and result type for SMTP clients
use self::Error::*;
use crate::transport::smtp::response::{Response, Severity};
use base64::DecodeError;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
io,
string::FromUtf8Error,
use crate::{
transport::smtp::response::{Code, Severity},
BoxError,
};
use std::{error::Error as StdError, fmt};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Response),
/// Error parsing a response
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(DecodeError),
/// Error parsing UTF8in response
Utf8Parsing(FromUtf8Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
Resolution,
/// IO error
Io(io::Error),
/// TLS error
#[cfg(feature = "native-tls")]
#[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),
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
/// The Errors that may occur when sending an email over SMTP
pub struct Error {
inner: Box<Inner>,
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "transient error during SMTP transaction",
struct Inner {
kind: Kind,
source: Option<BoxError>,
}
impl Error {
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
where
E: Into<BoxError>,
{
Error {
inner: Box::new(Inner {
kind,
source: source.map(Into::into),
}),
Permanent(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "permanent error during SMTP transaction",
}),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Resolution => fmt.write_str("could not resolve hostname"),
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
#[cfg(feature = "native-tls")]
Tls(ref err) => err.fmt(fmt),
Parsing(ref err) => fmt.write_str(err.description()),
#[cfg(feature = "rustls-tls")]
InvalidDNSName(ref err) => err.fmt(fmt),
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
InvalidCertificate => fmt.write_str("invalid certificate"),
#[cfg(feature = "r2d2")]
Pool(ref err) => err.fmt(fmt),
}
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err),
#[cfg(feature = "native-tls")]
Tls(ref err) => Some(&*err),
/// Returns true if the error is from response
pub fn is_response(&self) -> bool {
matches!(self.inner.kind, Kind::Response)
}
/// Returns true if the error is from client
pub fn is_client(&self) -> bool {
matches!(self.inner.kind, Kind::Client)
}
/// Returns true if the error is a transient SMTP error
pub fn is_transient(&self) -> bool {
matches!(self.inner.kind, Kind::Transient(_))
}
/// Returns true if the error is a permanent SMTP error
pub fn is_permanent(&self) -> bool {
matches!(self.inner.kind, Kind::Permanent(_))
}
/// Returns true if the error is caused by a timeout
pub fn is_timeout(&self) -> bool {
let mut source = self.source();
while let Some(err) = source {
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
return io_err.kind() == std::io::ErrorKind::TimedOut;
}
source = err.source();
}
false
}
/// Returns true if the error is from TLS
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn is_tls(&self) -> bool {
matches!(self.inner.kind, Kind::Tls)
}
/// Returns the status code, if the error was generated from a response.
pub fn status(&self) -> Option<Code> {
match self.inner.kind {
Kind::Transient(code) | Kind::Permanent(code) => Some(code),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
#[derive(Debug)]
pub(crate) enum Kind {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Code),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Code),
/// Error parsing a response
Response,
/// Internal client error
Client,
/// Connection error
Connection,
/// Underlying network i/o error
Network,
/// TLS error
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls,
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut builder = f.debug_struct("lettre::transport::smtp::Error");
builder.field("kind", &self.inner.kind);
if let Some(ref source) = self.inner.source {
builder.field("source", source);
}
builder.finish()
}
}
#[cfg(feature = "native-tls")]
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Tls(err)
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.inner.kind {
Kind::Response => f.write_str("response error")?,
Kind::Client => f.write_str("internal client error")?,
Kind::Network => f.write_str("network error")?,
Kind::Connection => f.write_str("Connection error")?,
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Kind::Tls => f.write_str("tls error")?,
Kind::Transient(ref code) => {
write!(f, "transient error ({})", code)?;
}
Kind::Permanent(ref code) => {
write!(f, "permanent error ({})", code)?;
}
};
if let Some(ref e) = self.inner.source {
write!(f, ": {}", e)?;
}
Ok(())
}
}
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(e) => e.code,
nom::Err::Error(e) => e.code,
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.inner.source.as_ref().map(|e| {
let r: &(dyn std::error::Error + 'static) = &**e;
r
})
}
}
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Error {
ChallengeParsing(err)
pub(crate) fn code(c: Code) -> Error {
match c.severity {
Severity::TransientNegativeCompletion => Error::new::<Error>(Kind::Transient(c), None),
Severity::PermanentNegativeCompletion => Error::new::<Error>(Kind::Permanent(c), None),
_ => client("Unknown error code"),
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Response, Some(e))
}
#[cfg(feature = "rustls-tls")]
impl From<webpki::InvalidDNSNameError> for Error {
fn from(err: webpki::InvalidDNSNameError) -> Error {
InvalidDNSName(err)
}
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Client, Some(e))
}
#[cfg(feature = "r2d2")]
impl From<r2d2::Error> for Error {
fn from(err: r2d2::Error) -> Error {
Pool(err)
}
pub(crate) fn network<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Network, Some(e))
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.code.severity {
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
}
}
pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Connection, Some(e))
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Tls, Some(e))
}

View File

@@ -1,7 +1,10 @@
//! ESMTP features
use crate::transport::smtp::{
authentication::Mechanism, error::Error, response::Response, util::XText,
authentication::Mechanism,
error::{self, Error},
response::Response,
util::XText,
};
use std::{
collections::HashSet,
@@ -58,6 +61,7 @@ impl Display for ClientId {
}
impl ClientId {
#[doc(hidden)]
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
/// Creates a new `ClientId` from a fully qualified domain name
pub fn new(domain: String) -> Self {
@@ -71,15 +75,15 @@ 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),
@@ -126,7 +130,7 @@ impl ServerInfo {
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(Error::ResponseParsing("Could not read server name")),
None => return Err(error::response("Could not read server name")),
};
let mut features: HashSet<Extension> = HashSet::new();

View File

@@ -40,11 +40,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")
//! .expect("relay valid")
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! .build();
//! // Send the email via remote relay
//! let result = sender.send(&email);
@@ -52,116 +51,73 @@
//! # Ok(())
//! # }
//! ```
//! #### Complete example
//!
//! ```todo
//! # #[cfg(feature = "smtp-transport")]
//! # {
//! use lettre::transport::smtp::authentication::{Credentials, Mechanism};
//! use lettre::{Email, Envelope, Transport, SmtpClient};
//! use lettre::transport::smtp::extension::ClientId;
//! #### Authentication
//!
//! let email_1 = Email::new(
//! Envelope::new(
//! Some(EmailAddress::new("user@localhost".to_string())?),
//! vec![EmailAddress::new("root@localhost".to_string())?],
//! )?,
//! "id1".to_string(),
//! "Hello world".to_string().into_bytes(),
//! );
//! Example with authentication and connection pool:
//!
//! let email_2 = Email::new(
//! Envelope::new(
//! Some(EmailAddress::new("user@localhost".to_string())?),
//! vec![EmailAddress::new("root@localhost".to_string())?],
//! )?,
//! "id2".to_string(),
//! "Hello world a second time".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}}};
//!
//! // Connect to a remote server on a custom port
//! let mut mailer = SmtpClient::new_simple("server.tld")?
//! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name(ClientId::Domain("my.hostname.tld".to_string()))
//! // Add credentials for authentication
//! .credentials(Credentials::new("username".to_string(), "password".to_string()))
//! // Enable SMTPUTF8 if the server supports it
//! .smtp_utf8(true)
//! // Configure expected authentication mechanism
//! .authentication_mechanism(Mechanism::Plain)
//! // Enable connection reuse
//! .connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
//! let 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_1 = mailer.send(&email_1);
//! assert!(result_1.is_ok());
//! // 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()))
//! // Configure expected authentication mechanism
//! .authentication(vec![Mechanism::Plain])
//! // Connection pool settings
//! .pool_config( PoolConfig::new().max_size(20))
//! .build();
//!
//! // 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};
//! ```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::client::{TlsParameters, Tls}};
//!
//! let email = Email::new(
//! Envelope::new(
//! Some(EmailAddress::new("user@localhost".to_string())?),
//! vec![EmailAddress::new("root@localhost".to_string())?],
//! )?,
//! "message_id".to_string(),
//! "Hello world".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!"))?;
//!
//! 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()?
//! );
//! // Custom TLS configuration
//! let tls = TlsParameters::builder("smtp.example.com".to_string())
//! .dangerous_accept_invalid_certs(true).build()?;
//!
//! let mut mailer = SmtpClient::new(
//! ("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
//! )?
//! .authentication_mechanism(Mechanism::Login)
//! .credentials(Credentials::new(
//! "example_username".to_string(), "example_password".to_string()
//! ))
//! .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
//! .transport();
//! // Create TLS transport on port 465
//! let sender = SmtpTransport::relay("smtp.example.com")?
//! // Custom TLS configuration
//! .tls(Tls::Required(tls))
//! .build();
//!
//! let result = mailer.send(&email);
//!
//! assert!(result.is_ok());
//!
//! mailer.close();
//! // Send the email via remote relay
//! let result = sender.send(&email);
//! assert!(result.is_ok());
//! # Ok(())
//! # }
//! ```
//!
#[cfg(feature = "tokio02")]
pub use self::async_transport::Tokio02Connector;
#[cfg(feature = "tokio03")]
pub use self::async_transport::Tokio03Connector;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
pub use self::async_transport::{
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder,
};
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
#[cfg(feature = "r2d2")]
pub use self::pool::PoolConfig;
#[cfg(feature = "r2d2")]
@@ -181,7 +137,7 @@ use crate::transport::smtp::{
use client::Tls;
use std::time::Duration;
#[cfg(any(feature = "tokio02", feature = "tokio03"))]
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
mod async_transport;
pub mod authentication;
pub mod client;
@@ -204,14 +160,13 @@ 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);
#[allow(missing_debug_implementations)]
#[derive(Clone)]
#[derive(Debug, Clone)]
struct SmtpInfo {
/// Name sent during EHLO
hello_name: ClientId,

View File

@@ -1,8 +1,8 @@
use std::time::Duration;
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
use crate::transport::smtp::{client::SmtpConnection, error, error::Error, SmtpClient};
use r2d2::{ManageConnection, Pool};
use r2d2::{CustomizeConnection, ManageConnection, Pool};
/// Configuration for a connection pool
#[derive(Debug, Clone)]
@@ -16,6 +16,11 @@ pub struct PoolConfig {
}
impl PoolConfig {
/// Create a new pool configuration with default values
pub fn new() -> Self {
Self::default()
}
/// Minimum number of idle connections
///
/// Defaults to `0`
@@ -28,7 +33,7 @@ impl PoolConfig {
///
/// Defaults to `10`
pub fn max_size(mut self, max_size: u32) -> Self {
self.min_idle = max_size;
self.max_size = max_size;
self
}
@@ -48,12 +53,16 @@ impl PoolConfig {
self
}
pub(crate) fn build<C: ManageConnection>(&self, client: C) -> Pool<C> {
pub(crate) fn build<C: ManageConnection<Connection = SmtpConnection, Error = Error>>(
&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))
.connection_customizer(Box::new(SmtpConnectionQuitter))
.build_unchecked(client)
}
}
@@ -81,10 +90,22 @@ impl ManageConnection for SmtpClient {
if conn.test_connected() {
return Ok(());
}
Err(Error::Client("is not connected anymore"))
Err(error::network("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
conn.has_broken()
}
}
#[derive(Copy, Clone, Debug)]
struct SmtpConnectionQuitter;
impl CustomizeConnection<SmtpConnection, Error> for SmtpConnectionQuitter {
fn on_release(&self, conn: SmtpConnection) {
let mut conn = conn;
if !conn.has_broken() {
let _quit = conn.quit();
}
}
}

View File

@@ -1,7 +1,7 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use crate::transport::smtp::Error;
use crate::transport::smtp::{error, Error};
use nom::{
branch::alt,
bytes::streaming::{tag, take_until},
@@ -120,6 +120,14 @@ impl Code {
detail,
}
}
/// Tells if the response is positive
pub fn is_positive(self) -> bool {
matches!(
self.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
}
}
/// Contains an SMTP reply, with separated code and message
@@ -139,7 +147,9 @@ impl FromStr for Response {
type Err = Error;
fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
parse_response(s)
.map(|(_, r)| r)
.map_err(|e| error::response(e.to_string()))
}
}
@@ -151,10 +161,7 @@ impl Response {
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
matches!(
self.code.severity,
Severity::PositiveCompletion | Severity::PositiveIntermediate
)
self.code.is_positive()
}
/// Tests code equality

View File

@@ -5,13 +5,13 @@ use r2d2::Pool;
#[cfg(feature = "r2d2")]
use super::PoolConfig;
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use crate::address::Envelope;
use crate::Transport;
use super::{error, Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
use crate::{address::Envelope, Transport};
#[allow(missing_debug_implementations)]
/// Sends emails using the SMTP protocol
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
#[derive(Clone)]
pub struct SmtpTransport {
#[cfg(feature = "r2d2")]
@@ -27,7 +27,7 @@ impl Transport for SmtpTransport {
/// 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()?;
let mut conn = self.inner.get().map_err(error::client)?;
#[cfg(not(feature = "r2d2"))]
let mut conn = self.inner.connection()?;
@@ -41,13 +41,14 @@ impl Transport for SmtpTransport {
}
impl SmtpTransport {
/// Simple and secure transport, using TLS connections to comunicate with the SMTP server
/// 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"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?;
@@ -68,6 +69,7 @@ impl SmtpTransport {
/// 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"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
let tls_parameters = TlsParameters::new(relay.into())?;
@@ -96,8 +98,10 @@ impl SmtpTransport {
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
/// if possible.
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let mut new = SmtpInfo::default();
new.server = server.into();
let new = SmtpInfo {
server: server.into(),
..Default::default()
};
SmtpTransportBuilder {
info: new,
@@ -107,9 +111,9 @@ impl SmtpTransport {
}
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
/// Contains client configuration.
/// Instances of this struct can be created using functions of [`SmtpTransport`].
#[derive(Debug, Clone)]
pub struct SmtpTransportBuilder {
info: SmtpInfo,
#[cfg(feature = "r2d2")]
@@ -150,6 +154,7 @@ impl SmtpTransportBuilder {
/// Set the TLS settings to use
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
pub fn tls(mut self, tls: Tls) -> Self {
self.info.tls = tls;
self
@@ -181,7 +186,7 @@ impl SmtpTransportBuilder {
}
/// Build client
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct SmtpClient {
info: SmtpInfo,
}

View File

@@ -9,8 +9,7 @@
//! ```rust
//! # #[cfg(feature = "builder")]
//! # {
//! use lettre::{Message, Transport};
//! use lettre::transport::stub::StubTransport;
//! use lettre::{transport::stub::StubTransport, Message, Transport};
//!
//! # use std::error::Error;
//! # fn main() -> Result<(), Box<dyn Error>> {
@@ -19,7 +18,7 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .body("Be happy!")?;
//! .body(String::from("Be happy!"))?;
//!
//! let mut sender = StubTransport::new_ok();
//! let result = sender.send(&email);
@@ -29,13 +28,10 @@
//! # }
//! ```
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
#[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};
@@ -57,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 }
}
@@ -84,20 +80,9 @@ impl Transport for StubTransport {
}
}
#[cfg(feature = "async-std1")]
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[async_trait]
impl AsyncStd1Transport for StubTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
self.response
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for StubTransport {
impl AsyncTransport for StubTransport {
type Ok = ();
type Error = Error;

234
testdata/email_with_png.eml vendored Normal file
View File

@@ -0,0 +1,234 @@
Date: Tue, 15 Nov 1994 08:12:31 -0000
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="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
Content-Type: text/html; charset=utf-8
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
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
QKADFNbY6KUYAACBDlBcU1NT6QkAACQg0AEK69evX+kJAAAkINABCnMFHQCACIEOUJxABwAgQqAD
FOeIOwAAEQIdoDhPcaenbN++vfQEAOAD8K4QoDBX0Okpb731VukJAMAHINABChPo9BSBDgDVTaAD
FOYhcfSU7du3R3d3d+kZAMBeEugAhQl0epL70AGgegl0gML69+9fegI1pKurq/QEAGAvCXSAwgYP
HhwNDQ2lZ1AjXn/99dITAIC9JNABCmtsbIyBAweWnkGNEOgAUL0EOkACzc3NpSdQIwQ6AFQvgQ6Q
gECnpwh0AKheAh0ggSFDhpSeQI144403Sk8AAPaSQAdIwBV0esprr71WegIAsJcEOkACAp2esmbN
mtITAIC9JNABEnDEnZ4i0AGgegl0gAQGDx5cegI1QqADQPUS6AAJuIJOTxHoAFC9BDpAAgKdnvLa
a6/F1q1bS88AAPaCQAdIYPjw4aUnUCMqlUqsXLmy9AwAYC8IdIAEWlpaSk+ghrz44oulJwAAe0Gg
AyQwcuTI0hOoIQIdAKqTQAdIQKDTkwQ6AFQngQ6QgCPu9KTly5eXngAA7AWBDpDAyJEjo7HRSzI9
46WXXoo33nij9AwAYA95NwiQQGNjoye502O6u7tdRQeAKiTQAZJwzJ2e9Nxzz5WeAADsIYEOkMSI
ESNKT6CGLF26tPQEAGAPCXSAJFxBpyd1dHREpVIpPQMA2AMCHSAJgU5P2rBhQ6xYsaL0DABgDwh0
gCRaW1tLT6DGdHR0lJ4AAOwBgQ6QxJgxY0pPoMYsXry49AQAYA8IdIAk9t9//9ITqDEdHR3x5ptv
lp4BALxPAh0gCYFOT9uyZUssWrSo9AwA4H0S6ABJtLa2Rv/+/UvPoMb85S9/KT0BAHifBDpAEo2N
jTF69OjSM6gxc+fOjW3btpWeAQC8DwIdIBEPiqOnvf766x4WBwBVQqADJOI+dHrDQw89VHoCAPA+
CHSARPbbb7/SE6hBbW1tsX79+tIzAID3INABEnEFnd6wbdu2mDNnTukZAMB7EOgAibgHnd7ywAMP
RHd3d+kZAMC7EOgAiRx44IGlJ1CjOjs7Y+HChaVnAADvQqADJDJq1KgYOnRo6RnUqHvvvbf0BADg
XQh0gGQOOuig0hOoUY8//ng8//zzpWcAALsh0AGSmTBhQukJ1LBf/epXpScAALsh0AGScQWd3vTI
I4+4ig4ASQl0gGTGjx9fegI1rLu7O2bPnl16BgCwCwIdIBlH3Oltc+fOjWeffbb0DADg/yPQAZLZ
f//9Y+DAgaVnUONuueWWqFQqpWcAAP9FoAMk09jYGOPGjSs9gxq3bNmyePDBB0vPAAD+i0AHSMiD
4ugLP/vZz2L9+vWlZwAA/yHQARI6+OCDS0+gDmzcuDFmzZpVegYA8B8CHSChQw89tPQE6sTDDz8c
jz76aOkZAEAIdICUJk2aVHoCdeTmm2+OtWvXlp4BAHVPoAMkNGLEiBgzZkzpGdSJ9evXxw033OCp
7gBQmEAHSOqwww4rPYE6smDBgrjnnntKzwCAuibQAZIS6PS1X/ziF9He3l56BgDULYEOkJT70Olr
lUolrrvuuli5cmXpKQBQlwQ6QFKTJk2KhoaG0jOoMxs2bIgrrrgi1q1bV3oKANQdgQ6Q1NChQ+OA
Aw4oPYM6tHr16rjyyivjzTffLD0FAOqKQAdIzOehU8pzzz0X3/72t6Orq6v0FACoGwIdILHDDz+8
9ATq2MKFC+Oaa64R6QDQRwQ6QGJTpkwpPYE6197eHtdee61IB4A+INABEjvkkENiyJAhpWdQ5xYs
WBCXXnpprF+/vvQUAKhpAh0gsaampjjiiCNKz4B49tlnY+bMmfHyyy+XngIANUugAyR31FFHlZ4A
ERHR2dkZM2bMiKVLl5aeAgA1SaADJHfkkUeWngA7rFmzJmbOnBkPPPBA6Skk0dXV5RkFAD1EoAMk
d9hhh8XgwYNLz4Adurq64qabboof/OAHPiu9jj333HNxyy23xCmnnBJz584tPQegJgh0gOT69esX
kydPLj0DdvL73/8+zjnnnFiyZEnpKfSRtWvXxn333Rfnn39+XHTRRXH//ffHxo0bBTpAD+lXegAA
7+2oo46KRYsWlZ4BO1m9enVceuml8ZWvfCUuueSS6N+/f+lJ9LCurq6YP39+PPTQQ9He3h7bt2/f
6dfMnz8/tm/fHk1NTQUWAtQOgQ5QBdyHTmZvvfVW3HHHHfHII4/E9773vZgyZUrpSfSAZcuWxR//
+MeYM2dObNiw4V1/7caNG2PJkiVeqwA+IIEOUAUmTZoUgwYNii1btpSeArvV2dkZM2fOjNNPPz0u
v/zy2HfffUtPYg8tW7Ys/vznP8ef/vSnWLly5R793ra2NoEO8AE1dHR0dJceAcB7u+qqq9znSdUY
PHhwnH322TFjxowYMmRI6Tm8i87Oznj00Udjzpw5exzl/23s2LHx4IMP9uAygPoj0AGqxAMPPBA3
3XRT6RmwR1pbW+Piiy+OM888MwYNGlR6DhFRqVTimWee2XGl/JVXXumxr33//ffHhAkTeuzrAdQb
gQ5QJdasWROnnnpqdHd72ab6tLS0xAUXXBBf+MIXorm5ufScurN58+b461//GgsXLoy2trZ49dVX
e+X7XHHFFXHJJZf0ytcGqAcCHaCKnH/++fH888+XngF7rbm5OU455ZQ499xzXWntRZVKJZYvXx6L
Fy+ORYsWxVNPPRXbtm3r9e87efLkuOeee3r9+wDUKoEOUEVuv/12b36pCQ0NDXHMMcfE6aefHscf
f3wMHDiw9KSq9+qrr8bjjz8eCxcujMWLF8f69ev7fENjY2PMmTMnWlpa+vx7A9QCgQ5QRZ555pmY
MWNG6RnQo4YOHRrTp0+Pk08+OY466qhobGwsPSm97u7ueOmll+LZZ5+NZ555JpYsWRKdnZ2lZ0VE
xHXXXRef/exnS88AqEoCHaCKVCqVOOmkk4pcGYO+MGLEiJg2bVqccMIJcfTRR8fgwYNLT0rhzTff
jOeeey6WLFmyI8rf67PJS5k2bVrMmjWr9AyAqiTQAarM9ddfHw8//HDpGdDrBgwYEJMnT46Pf/zj
cfTRR8ehhx4a/fr1Kz2r161bty5WrFgRnZ2d8cILL8SyZcvi73//e2zfvr30tPdl0KBB8eijj3pq
P8BeqP2fcgA15thjjxXo1IWurq7o6OiIjo6OiHg7/CZNmhRHHHFEHHLIITFx4sQ44IADqjbaN27c
GC+++GKsWLEiXnjhhejs7IwVK1bEv//979LTPpAtW7bE4sWLY9q0aaWnAFSd6vyJBlDHpk6dGgMG
DIiurq7SU6BPbdmyJZ588sl48sknd/y9/v37x7hx42LChAkxceLEGD9+fIwfPz5Gjx4dQ4YMKba1
UqnE2rVr41//+le8/PLL8fLLL+/489WrV8fLL78cmzZtKravt7W1tQl0gL0g0AGqzNChQ2Pq1Knx
2GOPlZ4CxW3bti1WrFgRK1as2OmfDRw4MFpbW6O1tTX23XffaG1tjZaWlmhpaYmmpqYYMmRINDQ0
xLBhwyLi7f+2Ghoa3vG1t2zZEhERW7duja1bt0bE2/eDb9q0KTZu3Ljjjw0bNsTrr78eGzZs2PHX
lUqlD/4N5DRv3ryoVCoe+AewhwQ6QBU66aSTBDq8h61bt8aqVati1apVpafUnXXr1sXSpUtj8uTJ
pacAVBX/WxOgCk2bNi2am5tLzwDYrXnz5pWeAFB1BDpAFRo0aFAce+yxpWcA7FZbW1vpCQBVR6AD
VKmTTjqp9ASA3ers7IyVK1eWngFQVQQ6QJWaOnXqjodbAWTkmDvAnhHoAFVqwIABcfzxx5eeAbBb
c+fOLT0BoKoIdIAq9ulPf7r0BIDdeuqpp2LDhg2lZwBUDYEOUMU+9rGPRUtLS+kZALtUqVRiwYIF
pWcAVA2BDlDF+vXrF6eddlrpGQC75Zg7wPsn0AGq3FlnnRUNDQ2lZwDsUnt7e3R1dZWeAVAVBDpA
lRs7dmwcddRRpWcA7NLmzZvjiSeeKD0DoCoIdIAacOaZZ5aeALBbPm4N4P0R6AA14MQTT4yRI0eW
ngGwS4899lh0d3eXngGQnkAHqAH9+/ePU089tfQMgF1as2ZNLF++vPQMgPQEOkCNOPPMMz0sDkjL
09wB3ptAB6gR48aNiylTppSeAbBLAh3gvQl0gBry+c9/vvQEgF1avnx5vPLKK6VnAKQm0AFqyPTp
02P//fcvPQNgJ93d3a6iA7wHgQ5QQ5qamuK8884rPQNglwQ6wLsT6AA15nOf+1wMGzas9AyAnXR0
dMSmTZtKzwBIS6AD1Jjm5uY466yzSs8A2Mm2bdti4cKFpWcApCXQAWrQl770pRgwYEDpGQA7ccwd
YPcEOkANamlpiVNOOaX0DICdzJ8/P7Zv3156BkBKAh2gRl100UXR2OhlHshl48aN8fTTT5eeAZCS
d24ANeqggw6KT3ziE6VnAOzEMXeAXRPoADVsxowZpScA7KStra30BICUBDpADTviiCNi6tSppWcA
vMM///nP6OzsLD0DIB2BDlDjvv71r0dDQ0PpGQDv4Co6wM4EOkCNO/TQQ+OEE04oPQPgHebNm1d6
AkA6Ah2gDlx++eXR1NRUegbADkuXLo21a9eWngGQikAHqAPjxo2Lk08+ufQMgB0qlUrMnz+/9AyA
VAQ6QJ247LLLon///qVnAOzgPnSAdxLoAHVizJgxcfrpp5eeAbDDokWLYsuWLaVnAKQh0AHqyFe/
+tUYPHhw6RkAERGxdevWWLx4cekZAGkIdIA6Mnr06Pjyl79cegbADo65A/w/Ah2gzlx44YVx4IEH
lp4BEBFvf9xapVIpPQMgBYEOUGf69+8f3/rWt0rPAIiIiHXr1sXSpUtLzwBIQaAD1KHjjjsupk6d
WnoGQES8fRUdAIEOULeuvfbaGDBgQOkZAO5DB/gPgQ5Qpw444IA455xzSs8AiM7Ozli5cmXpGQDF
CXSAOva1r30tRo0aVXoGgGPuACHQAepac3NzfPOb3yw9AyDmzp1begJAcQIdoM595jOfiRNOOKH0
DKDOPfXUU7Fhw4bSMwCKEugAxHe+850YPnwtwJqJAAAO10lEQVR46RlAHatUKrFgwYLSMwCKEugA
RGtra1x11VWlZwB1zjF3oN4JdAAiIuK0006L4447rvQMoI61t7dHV1dX6RkAxQh0AHb47ne/66g7
UMzmzZvjiSeeKD0DoBiBDsAOo0aNiiuvvLL0DKCOOeYO1DOBDsA7nHHGGTF16tTSM4A61dbWFt3d
3aVnABQh0AHYyfe///3Yd999S88A6tCaNWti+fLlpWcAFCHQAdhJS0tL/PCHP4zGRj8mgL7nmDtQ
r7zzAmCXPvrRj8bFF19cegZQhwQ6UK8EOgC7ddlll8WUKVNKzwDqzN/+9rdYvXp16RkAfU6gA7Bb
TU1NceONN8aIESNKTwHqzPz580tPAOhzAh2AdzV69Oi44YYboqGhofQUoI7Mmzev9ASAPifQAXhP
U6dOjQsuuKD0DKCOdHR0xKZNm0rPAOhTAh2A9+Xyyy+PI488svQMoE5s27YtFi5cWHoGQJ8S6AC8
L/369Ytbb701DjzwwNJTgDrhae5AvRHoALxvw4cPj1mzZsWwYcNKTwHqwPz582P79u2lZwD0GYEO
wB456KCD4uabb46mpqbSU4Aat3Hjxnj66adLzwDoMwIdgD12zDHHxNVXX116BlAH2traSk8A6DMC
HYC9cvbZZ8cXv/jF0jOAGifQgXoi0AHYa9dee2188pOfLD0DqGGrVq2Kzs7O0jMA+oRAB2CvNTY2
xo033hgf/vCHS08Bapir6EC9EOgAfCBDhw6N2267LcaPH196ClCj5s2bV3oCQJ8Q6AB8YCNHjozb
brstxowZU3oKUIOWLl0aa9euLT0DoNcJdAB6xOjRo+P222+P1tbW0lOAGlOpVFxFB+qCQAegx4wd
OzZuu+222GeffUpPAWrM3LlzS08A6HUCHYAeNXHixPjJT34Szc3NpacANWTRokWxZcuW0jMAepVA
B6DHHX744TFr1qwYNGhQ6SlAjdi6dWssXry49AyAXiXQAegVRx99dPz0pz+NIUOGlJ4C1AgftwbU
OoEOQK+ZMmVKzJ49O0aMGFF6ClAD5s2bF5VKpfQMgF4j0AHoVYcddljceeedMXr06NJTgCq3bt26
WLp0aekZAL1GoAPQ68aPHx933XVXjB07tvQUoMr5uDWglgl0APrEmDFj4q677oqJEyeWngJUMfeh
A7VMoAPQZ1pbW+PnP/95HHLIIaWnAFWqs7Mz/vGPf5SeAdArBDoAfaqlpSXuvPPOOO6440pPAarU
3LlzS08A6BUCHYA+19zcHLfeemvMnDmz9BSgCgl0oFYJdACKaGhoiJkzZ8b1118f/fv3Lz0HqCJP
PfVUrF+/vvQMgB4n0AEo6owzzojZs2fHyJEjS08BqkSlUon29vbSMwB6nEAHoLgjjjgifvnLX8b4
8eNLTwGqhGPuQC0S6ACkMHbs2Ljnnnti6tSppacAVaC9vT26urpKzwDoUQIdgDSGDRsWP/7xj+Pq
q6+Ofv36lZ4DJLZ58+Z44oknSs8A6FECHYBUGhoa4rzzzou77747xowZU3oOkJhj7kCtEegApHT4
4YfHr3/96/jUpz5VegqQVFtbW3R3d5eeAdBjBDoAae2zzz4xa9asuPrqq30UG7CTNWvWxPLly0vP
AOgxAh2A1P77yPuHPvSh0nOAZBxzB2qJQAegKkyaNCnuvffeOPfcc6Ox0Y8v4G1tbW2lJwD0GO9w
AKgaQ4YMiWuuuSbuuOOOOPDAA0vPARJYvnx5rF69uvQMgB4h0AGoOlOmTIn77rsvLr74YlfTgZg/
f37pCQA9wrsaAKrSwIED4xvf+EbcddddcdBBB5WeAxQ0b9680hMAeoRAB6CqTZ48OX7729/GhRde
6Go61KmOjo7YvHlz6RkAH1hDR0eHD48EoCa8+OKLMWvWrGhvby89BegDEyZMiOnTp8f06dNjwoQJ
pecAfGACHYCaM3fu3Pif//mfWLVqVekpQA9qbGyMj3zkIzFt2rQ44YQTPCwSqDkCHYCatG3btvjd
734Xs2fPjk2bNpWeA+yl/4vy/7tSPmrUqNKTAHqNQAegpr322mvxi1/8b3t391J1usZx+M6VQrqW
tbSyLEgtCUtIijRNI6K/sKPaMOcdVVBBUVTUQVRSIpLlQfRGlNILaEYWug82IxMje2aY6nfbui74
4WKh+D3TDz74/CfOnTsXi4uLRc8B/oaGhoYYGBiIkZGROHr0aLS0tBQ9CeCnEOgA1ISJiYk4ceJE
3L9/v+gpwAqamppieHg4jh07FkNDQ7Fu3bqiJwH8dAIdgJoyNjYWp06dinv37hU9BWpec3NzDA8P
x/Hjx+PQoUPR0NBQ9CSAQgl0AGrS2NhYnDx5MkZHR4ueAjVl27ZtMTIyEkeOHIn9+/fH2rVri54E
kIZAB6CmCXX4serq6mL37t0xMjLiOjSAvyDQASAibt++Hb/99luMjY0VPQVWvXK5HENDQ3HkyJEY
GhqK5ubmoicBrAoCHQD+4NGjR3HmzJm4cOFCLCwsFD0HVo329vY4dOhQDA8Px+DgYNTX1xc9CWDV
EegAsIJ3797F+fPn4/Tp0zEzM1P0HEjnj0fXR0ZGoqenp+hJAKueQAeA/2NhYSEuXboUp0+fjqmp
qaLnQKE2bNgQ/f39MTQ0FIcPH45qtVr0JIBfikAHgL9pdHQ0zpw5Ezdu3HD8nZpQKpWit7c3BgcH
Y3BwMHp6eqKurq7oWQC/LIEOAP/Q7OxsXLlyJS5evBjj4+OxtORHKb+O9vb2GBgYiMHBwTh48GBU
KpWiJwHUDIEOAP/C69ev4/Lly3H27Nl4+fJl0XPgH6tWq3HgwIHo7++Pvr4+16ABFEigA8B3sLi4
GA8ePIgLFy7E9evXY25uruhJsKJyuRz79++PgwcPRn9/f3R1dcWaNWuKngVACHQA+O4WFxdjfHw8
rl69GteuXYvp6emiJ1HDWlpaYu/evdHX1xf79u2L3t7eWLt2bdGzAFiBQAeAH+zJkydx9erVuHXr
VkxOThY9h1/cxo0bo6+vb/nIemdnp7+QA6wSAh0AfqLnz5/HjRs34ubNmzExMRFfv34tehKrWKVS
ib1790Zvb+/yR1efAaxeAh0ACjI/Px9jY2MxOjoao6Oj8ejRo1hcXCx6FknV19fHrl27vonxHTt2
+Os4wC9EoANAEvPz8zExMRF37tyJu3fvxuPHjwV7jSqXy7Fz587o6emJrq6u6Orqij179kRDQ0PR
0wD4gQQ6ACT1/v37GB8fj4cPH8bk5GQ8fPgwPnz4UPQsvqOGhobo6OiIjo6O2LlzZ3R3d0d3d3ds
3bq16GkAFECgA8Aq8urVq+VYn5ycjMnJyZidnS16Fn+hUqnEjh07oqurKzo6OqKzszM6Ozujvb09
6urqip4HQBICHQBWsaWlpXjx4kVMTU3F8+fP4+nTp/H06dN49uxZfPz4seh5NaNUKkVbW1ts3749
tm3b9s2zffv2aG5uLnoiAKuAQAeAX9TMzEw8e/Zs+fk93Kenp+PLly9Fz1s1SqVStLa2xpYtW2Lj
xo3R1tYWmzdvjk2bNi2/bmtrc7c4AP+aQAeAGvT27duYmZmJ6enpmJ6ejpmZmXjz5s03r+fn54ue
+UPU19dHpVKJDRs2LD/VajWq1WqsX79++b3W1taoVqvR2trqGDoAP4VABwBWND8/H7Ozs988c3Nz
MTc3t+L7S0tLMTc3t/z1s7OzsbT0v18zPn36tHzn++fPn+PLly9RLpdX/L7lcvlPV4f9fkS8Uqks
f05dXV00NTVFqVSKxsbGKJVK0dTU9KenUqlEuVyOpqamKJfL/hM6AGk5iwUArKixsTEaGxujra2t
6CkAUBOc1wIAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCA
QAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAE
BDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAk
INABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAg
AYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAA
CQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAA
SECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAA
QAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAA
ABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAA
AJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcA
AIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoA
AAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINAB
AAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEO
AAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0
AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECg
AwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAIC
HQAAABIQ6AAAAJDAfwHNjj3TR6+CggAAAABJRU5ErkJggg==
--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--

View File

@@ -1,98 +1,246 @@
#[cfg(all(feature = "file-transport", feature = "builder"))]
fn default_date() -> std::time::SystemTime {
use std::time::{Duration, SystemTime};
// Tue, 15 Nov 1994 08:12:31 GMT
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151)
}
#[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder"))]
mod test {
use lettre::{transport::file::FileTransport, Message};
mod sync {
use crate::default_date;
use lettre::{FileTransport, Message, Transport};
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;
let sender = FileTransport::new(temp_dir());
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.date(default_date())
.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 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
#[cfg(feature = "async-std1")]
#[async_attributes::test]
async fn file_transport_asyncstd1() {
use lettre::AsyncStd1Transport;
let sender = FileTransport::new(temp_dir());
#[test]
#[cfg(feature = "file-transport-envelope")]
fn file_transport_with_envelope() {
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!")
.date(default_date())
.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 -0000\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(test)]
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio02"))]
mod tokio_02 {
use crate::default_date;
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio02Executor};
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
use tokio02_crate as tokio;
#[tokio::test]
async fn file_transport_tokio02() {
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(default_date())
.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 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
}
#[cfg(test)]
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use crate::default_date;
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio1Executor};
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
use tokio1_crate as tokio;
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn file_transport_tokio02() {
use lettre::Tokio02Transport;
let sender = FileTransport::new(temp_dir());
async fn file_transport_tokio1() {
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.date(default_date())
.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 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
}
#[cfg(test)]
#[cfg(all(
feature = "file-transport",
feature = "builder",
feature = "async-std1"
))]
mod asyncstd_1 {
use crate::default_date;
use lettre::{AsyncFileTransport, AsyncStd1Executor, AsyncTransport, Message};
use std::{
env::temp_dir,
fs::{read_to_string, remove_file},
};
#[async_std::test]
async fn file_transport_asyncstd1() {
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(default_date())
.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 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
}

View File

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

View File

@@ -1,6 +1,6 @@
#[cfg(test)]
#[cfg(all(feature = "smtp-transport", feature = "builder"))]
mod test {
mod sync {
use lettre::{Message, SmtpTransport, Transport};
#[test]
@@ -10,12 +10,89 @@ 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_dangerous("127.0.0.1")
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
.port(2525)
.build()
.send(&email)
.unwrap();
.build();
sender.send(&email).unwrap();
}
}
#[cfg(test)]
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio02"))]
mod tokio_02 {
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio02Executor};
use tokio02_crate as tokio;
#[tokio::test]
async fn smtp_transport_simple_tokio02() {
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 sender: AsyncSmtpTransport<Tokio02Executor> =
AsyncSmtpTransport::<Tokio02Executor>::builder_dangerous("127.0.0.1")
.port(2525)
.build();
sender.send(email).await.unwrap();
}
}
#[cfg(test)]
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use tokio1_crate as tokio;
#[tokio::test]
async fn smtp_transport_simple_tokio1() {
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 sender: AsyncSmtpTransport<Tokio1Executor> =
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("127.0.0.1")
.port(2525)
.build();
sender.send(email).await.unwrap();
}
}
#[cfg(test)]
#[cfg(all(
feature = "smtp-transport",
feature = "builder",
feature = "async-std1"
))]
mod asyncstd_1 {
use lettre::{AsyncSmtpTransport, AsyncStd1Executor, AsyncTransport, Message};
#[async_std::test]
async fn smtp_transport_simple_asyncstd1() {
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 sender: AsyncSmtpTransport<AsyncStd1Executor> =
AsyncSmtpTransport::<AsyncStd1Executor>::builder_dangerous("127.0.0.1")
.port(2525)
.build();
sender.send(email).await.unwrap();
}
}

View File

@@ -1,8 +1,6 @@
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
mod test {
use lettre::address::Envelope;
use lettre::{SmtpTransport, Transport};
mod sync {
use lettre::{address::Envelope, SmtpTransport, Transport};
use std::{sync::mpsc, thread};
fn envelope() -> Envelope {

View File

@@ -1,14 +1,10 @@
#[cfg(test)]
#[cfg(feature = "builder")]
mod test {
use lettre::{transport::stub::StubTransport, Message};
#[cfg(feature = "tokio02")]
use tokio02_crate as tokio;
mod sync {
use lettre::{transport::stub::StubTransport, Message, Transport};
#[test]
fn stub_transport() {
use lettre::Transport;
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let email = Message::builder()
@@ -16,38 +12,23 @@ 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();
sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err();
}
}
#[cfg(feature = "async-std1")]
#[async_attributes::test]
async fn stub_transport_asyncstd1() {
use lettre::AsyncStd1Transport;
#[cfg(test)]
#[cfg(all(feature = "builder", feature = "tokio02"))]
mod tokio_02 {
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body("Be happy!")
.unwrap();
use tokio02_crate as tokio;
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()
@@ -55,8 +36,53 @@ mod test {
.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();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
}
}
#[cfg(test)]
#[cfg(all(feature = "builder", feature = "tokio1"))]
mod tokio_1 {
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
use tokio1_crate as tokio;
#[tokio::test]
async fn stub_transport_tokio1() {
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body(String::from("Be happy!"))
.unwrap();
sender_ok.send(email.clone()).await.unwrap();
sender_ko.send(email).await.unwrap_err();
}
}
#[cfg(test)]
#[cfg(all(feature = "builder", feature = "async-std1"))]
mod asyncstd_1 {
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
#[async_std::test]
async fn stub_transport_asyncstd1() {
let sender_ok = StubTransport::new_ok();
let sender_ko = StubTransport::new_error();
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.body(String::from("Be happy!"))
.unwrap();
sender_ok.send(email.clone()).await.unwrap();