Compare commits

...

60 Commits

Author SHA1 Message Date
Alexis Mousset
47cad567b0 Prepare 0.10.0-rc.3 (#629) 2021-05-22 19:52:38 +02:00
Alexis Mousset
b0e2fc9bca fix(transport-smtp): Fix transparency codec (#627)
It fails to add transparency when a period is preceded by two
successive CRLF.

Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2021-05-22 19:41:29 +02:00
Alexis Mousset
1d8249165c Makes more things private and add missing docs (#621) 2021-05-19 18:51:03 +02:00
Alexis Mousset
98fc0cb2f3 Prepare 0.10.0-rc.2 (#624) 2021-05-18 18:12:10 +02:00
Alexis Mousset
0439bab874 fix(builder): Don't include Bcc headers in formatted messages (#623)
fixes #622
2021-05-18 18:03:20 +02:00
Alexis Mousset
504fc51b26 Prepare 0.10.0-rc.1 (#620) 2021-05-14 17:48:42 +02:00
Paolo Barbolini
d54343cf00 Remove Part from the public API (#619) 2021-05-14 17:27:03 +02:00
Alexis Mousset
904789ac3d feat(builder): Add helper methods for attachments and text (#618) 2021-05-14 16:59:08 +02:00
Paolo Barbolini
94cae6df0d Drop tokio 0.2 support (#617) 2021-05-12 19:09:30 +02:00
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
63 changed files with 3751 additions and 2374 deletions

View File

@@ -79,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,7 @@
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
* MSRV is now 1.45.2
* MSRV is now 1.46
* 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,
@@ -14,7 +14,7 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
#### Features
* Add `tokio` 0.2 and 1.0 support
* Add `tokio` 1 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

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.5"
version = "0.10.0-rc.3"
description = "Email client"
readme = "README.md"
homepage = "https://lettre.rs"
@@ -22,21 +22,21 @@ 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.8", 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 = { 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
@@ -57,9 +57,6 @@ async-std = { version = "1.8", optional = true, features = ["unstable"] }
async-rustls = { version = "0.2", optional = true }
## tokio
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["fs", "process", "tcp", "dns", "io-util"], optional = true }
tokio02_native_tls_crate = { package = "tokio-native-tls", version = "0.1", optional = true }
tokio02_rustls = { package = "tokio-rustls", version = "0.15", optional = true }
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "process", "net", "io-util"], optional = true }
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
tokio1_rustls = { package = "tokio-rustls", version = "0.22", optional = true }
@@ -69,7 +66,6 @@ criterion = "0.3"
tracing-subscriber = "0.2.10"
glob = "0.3"
walkdir = "2"
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
async-std = { version = "1.8", features = ["attributes"] }
serde_json = "1"
@@ -80,11 +76,11 @@ harness = false
name = "transport_smtp"
[features]
default = ["file-transport", "file-transport-envelope", "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 = []
file-transport = ["uuid"]
file-transport-envelope = ["serde", "serde_json", "file-transport"]
sendmail-transport = []
smtp-transport = ["base64", "nom"]
@@ -95,9 +91,6 @@ rustls-tls = ["webpki", "webpki-roots", "rustls"]
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"]
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"]
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"]
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"]
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
@@ -130,14 +123,6 @@ required-features = ["smtp-transport", "native-tls", "builder"]
name = "smtp_selfsigned"
required-features = ["smtp-transport", "native-tls", "builder"]
[[example]]
name = "tokio02_smtp_tls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]]
name = "tokio02_smtp_starttls"
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
[[example]]
name = "tokio1_smtp_tls"
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]

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-rc.3">
<img src="https://deps.rs/crate/lettre/0.10.0-rc.3/status.svg"
alt="dependency status" />
</a>
</div>
@@ -37,10 +37,15 @@
---
**NOTE**: this readme refers to the 0.10 version of lettre, which is
still being worked on. The master branch and the alpha releases will see
API breaking changes and some features may be missing or incomplete until
the stable 0.10.0 release is out.
Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x) branch for stable releases.
in release candidate state. Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x)
branch for the previous stable release.
0.10 is already widely used and is already thought to be more reliable than 0.9, so it should generally be used
for new projects.
We'd love to hear your feedback about 0.10 design and APIs before final release!
Start a [discussion](https://github.com/lettre/lettre/discussions) in the repository, whether for
feedback or if you need help or advice using or upgrading lettre 0.10.
---
@@ -60,13 +65,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-rc.3"
```
```rust,no_run
@@ -78,7 +83,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

@@ -14,7 +14,7 @@ This folder contains examples showing how to use lettre in your own projects.
- [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password.
- [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password.
- The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/).
These files are prefixed with `tokio02_`, `tokio1_` or `asyncstd1_`.
These files are prefixed with `tokio1_` or `asyncstd1_`.
[basic_html.rs]: ./basic_html.rs
[maud_html.rs]: ./maud_html.rs

View File

@@ -1,6 +1,6 @@
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Connector,
AsyncStd1Transport, Message,
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
AsyncTransport, Message,
};
#[async_std::main]
@@ -18,10 +18,11 @@ async fn main() {
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer = AsyncSmtpTransport::<AsyncStd1Connector>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
AsyncSmtpTransport::<AsyncStd1Executor>::starttls_relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {

View File

@@ -1,6 +1,6 @@
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Connector,
AsyncStd1Transport, Message,
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Executor,
AsyncTransport, Message,
};
#[async_std::main]
@@ -18,10 +18,11 @@ async fn main() {
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer = AsyncSmtpTransport::<AsyncStd1Connector>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
AsyncSmtpTransport::<AsyncStd1Executor>::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(email).await {

View File

@@ -1,5 +1,7 @@
use lettre::message::{header, MultiPart, SinglePart};
use lettre::{FileTransport, Message, Transport};
use lettre::{
message::{header, MultiPart, SinglePart},
FileTransport, Message, Transport,
};
fn main() {
// The html we want to send.
@@ -27,16 +29,12 @@ fn main() {
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.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; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_HTML)
.body(String::from(html)),
),
)

View File

@@ -1,5 +1,7 @@
use lettre::message::{header, MultiPart, SinglePart};
use lettre::{FileTransport, Message, Transport};
use lettre::{
message::{header, MultiPart, SinglePart},
FileTransport, Message, Transport,
};
use maud::html;
fn main() {
@@ -36,16 +38,12 @@ fn main() {
MultiPart::alternative() // This is composed of two parts.
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.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; charset=utf8".parse().unwrap(),
))
.header(header::ContentType::TEXT_HTML)
.body(html.into_string()),
),
)

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,
};
@@ -20,9 +22,10 @@ fn main() {
// 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

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

View File

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

View File

@@ -4,8 +4,8 @@
use tokio1_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Connector,
Tokio1Transport,
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor,
};
#[tokio::main]
@@ -23,10 +23,11 @@ async fn main() {
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail using STARTTLS
let mailer = AsyncSmtpTransport::<Tokio1Connector>::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

@@ -4,8 +4,8 @@
use tokio1_crate as tokio;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Connector,
Tokio1Transport,
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message,
Tokio1Executor,
};
#[tokio::main]
@@ -23,10 +23,11 @@ async fn main() {
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
// Open a remote connection to gmail
let mailer = AsyncSmtpTransport::<Tokio1Connector>::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

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

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

200
src/executor.rs Normal file
View File

@@ -0,0 +1,200 @@
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 = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::client::AsyncSmtpConnection;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::client::Tls;
#[cfg(all(
feature = "smtp-transport",
any(feature = "tokio1", feature = "async-std1")
))]
use crate::transport::smtp::extension::ClientId;
#[cfg(all(
feature = "smtp-transport",
any(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 = "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` `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 = "tokio1")]
impl Sealed for Tokio1Executor {}
#[cfg(feature = "async-std1")]
impl Sealed for AsyncStd1Executor {}
}

View File

@@ -4,33 +4,98 @@
//! * 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
//! * **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
//! * **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.x 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.
//!
//! * **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
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file
//! * **smtp-transport**: Transport over SMTP
//! * **sendmail-transport**: Transport over SMTP
//! * **rustls-tls**: TLS support with the `rustls` crate
//! * **native-tls**: TLS support with the `native-tls` crate
//! * **tokio02**: Allow to asyncronously send emails using tokio 0.2.x
//! * **tokio02-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.2
//! * **tokio02-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.2
//! * **tokio1**: Allow to asyncronously send emails using tokio 1.x
//! * **tokio1-rustls-tls**: Async TLS support with the `rustls` crate using tokio 1.x
//! * **tokio1-native-tls**: Async TLS support with the `native-tls` crate using tokio 1.x
//! * **async-std1**: Allow to asynchronously send emails using async-std 1.x
//! * NOTE: native-tls isn't supported with async-std at the moment
//! * **async-std1-rustls-tls**: Async TLS support with the `rustls` crate using async-std 1.x
//! * **r2d2**: Connection pool for SMTP transport
//! * **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 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.5")]
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.3")]
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
#![forbid(unsafe_code)]
@@ -46,135 +111,67 @@
pub mod address;
pub mod error;
#[cfg(any(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 = "tokio1", feature = "async-std1")))]
pub use self::executor::Executor;
#[cfg(feature = "tokio1")]
pub use self::executor::Tokio1Executor;
#[cfg(all(any(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 = "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 = "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 = "tokio1")
any(feature = "tokio1", feature = "async-std1")
))]
pub use crate::transport::smtp::AsyncSmtpTransport;
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
pub use crate::transport::smtp::AsyncStd1Connector;
#[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 = "tokio1"))]
pub use crate::transport::smtp::Tokio1Connector;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
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 1.x based Transport method for emails
#[cfg(feature = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
#[async_trait]
pub trait Tokio1Transport {
/// 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());
@@ -201,9 +198,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(),
@@ -221,8 +218,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(),);
}

91
src/message/attachment.rs Normal file
View File

@@ -0,0 +1,91 @@
use crate::message::{
header::{self, ContentType},
IntoBody, SinglePart,
};
/// `SinglePart` builder for attachments
///
/// Allows building attachment parts easily.
#[derive(Clone)]
pub struct Attachment {
disposition: Disposition,
}
#[derive(Clone)]
enum Disposition {
/// file name
Attached(String),
/// content id
Inline(String),
}
impl Attachment {
/// Creates a new attachment
pub fn new(filename: String) -> Self {
Attachment {
disposition: Disposition::Attached(filename),
}
}
/// Creates a new inline attachment
pub fn new_inline(content_id: String) -> Self {
Attachment {
disposition: Disposition::Inline(content_id),
}
}
/// Build the attachment part
pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
let mut builder = SinglePart::builder();
builder = match self.disposition {
Disposition::Attached(filename) => {
builder.header(header::ContentDisposition::attachment(&filename))
}
Disposition::Inline(content_id) => builder
.header(header::ContentId::from(format!("<{}>", content_id)))
.header(header::ContentDisposition::inline()),
};
builder = builder.header(content_type);
builder.body(content)
}
}
#[cfg(test)]
mod tests {
use crate::message::header::ContentType;
#[test]
fn attachment() {
let part = super::Attachment::new(String::from("test.txt")).body(
String::from("Hello world!"),
ContentType::parse("text/plain").unwrap(),
);
assert_eq!(
&String::from_utf8_lossy(&part.formatted()),
concat!(
"Content-Disposition: attachment; filename=\"test.txt\"\r\n",
"Content-Type: text/plain\r\n",
"Content-Transfer-Encoding: 7bit\r\n\r\n",
"Hello world!\r\n",
)
);
}
#[test]
fn attachment_inline() {
let part = super::Attachment::new_inline(String::from("id")).body(
String::from("Hello world!"),
ContentType::parse("text/plain").unwrap(),
);
assert_eq!(
&String::from_utf8_lossy(&part.formatted()),
concat!(
"Content-ID: <id>\r\n",
"Content-Disposition: inline\r\n",
"Content-Type: text/plain\r\n",
"Content-Transfer-Encoding: 7bit\r\n\r\n",
"Hello world!\r\n"
)
);
}
}

View File

@@ -1,5 +1,8 @@
use std::io::{self, Write};
use std::ops::Deref;
use std::{
io::{self, Write},
mem,
ops::Deref,
};
use crate::message::header::ContentTransferEncoding;
@@ -16,7 +19,9 @@ pub struct Body {
/// 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),
}
@@ -28,13 +33,16 @@ impl Body {
/// 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 buf: MaybeString = buf.into();
let mut buf: MaybeString = buf.into();
let encoding = buf.encoding();
buf.encode_crlf();
Self::new_impl(buf.into(), encoding)
}
@@ -42,6 +50,8 @@ impl Body {
///
/// [`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.
@@ -49,12 +59,13 @@ impl Body {
buf: B,
encoding: ContentTransferEncoding,
) -> Result<Self, Vec<u8>> {
let buf: MaybeString = buf.into();
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))
}
@@ -160,6 +171,14 @@ impl MaybeString {
}
}
/// 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 {
@@ -187,6 +206,7 @@ impl MaybeString {
/// **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;
}
@@ -320,9 +340,44 @@ where
}
}
/// 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::{Body, ContentTransferEncoding};
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
#[test]
fn seven_bit_detect() {
@@ -576,4 +631,31 @@ mod test {
.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,13 +1,10 @@
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
///
@@ -16,17 +13,29 @@ header! { (ContentId, "Content-ID") => [String] }
/// 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::Base64
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()
}
}
@@ -56,35 +65,16 @@ impl FromStr for ContentTransferEncoding {
}
}
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() {
@@ -92,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,803 @@
/*!
//! 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::*};
/// Represents an email header
///
/// Email header as defined in [RFC5322](https://datatracker.ietf.org/doc/html/rfc5322) and extensions.
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,58 @@
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 };
/// MIME version 1.0
///
/// Should be used in all MIME messages.
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 +61,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 +72,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

@@ -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>> {
@@ -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,20 +1,19 @@
use std::io::Write;
use crate::message::{
header::{ContentTransferEncoding, ContentType, Header, Headers},
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
EmailFormat, IntoBody,
};
use mime::Mime;
use rand::Rng;
use std::iter::repeat_with;
/// MIME part variants
///
#[derive(Debug, Clone)]
pub enum Part {
pub(super) enum Part {
/// Single part with content
///
Single(SinglePart),
/// Multiple parts of content
///
Multi(MultiPart),
}
@@ -27,21 +26,7 @@ impl EmailFormat for Part {
}
}
impl Part {
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
self.format(&mut out);
out
}
}
/// Parts of multipart body
///
pub type Parts = Vec<Part>;
/// Creates builder for single part
///
#[derive(Debug, Clone)]
pub struct SinglePartBuilder {
headers: Headers,
@@ -69,7 +54,7 @@ impl SinglePartBuilder {
/// Build singlepart using body
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding());
@@ -92,17 +77,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()?))
/// .body(String::from("Текст письма в уникоде"));
/// .header(header::ContentType::TEXT_PLAIN)
/// .body(String::from("Текст письма в уникоде"));
/// # Ok(())
/// # }
/// ```
///
#[derive(Debug, Clone)]
pub struct SinglePart {
headers: Headers,
@@ -116,28 +100,18 @@ impl SinglePart {
SinglePartBuilder::new()
}
#[doc(hidden)]
#[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"]
pub fn seven_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::SevenBit)
/// Directly create a `SinglePart` from an plain UTF-8 content
pub fn plain<T: IntoBody>(body: T) -> Self {
Self::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(body)
}
#[doc(hidden)]
#[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"]
pub fn quoted_printable() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::QuotedPrintable)
}
#[doc(hidden)]
#[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"]
pub fn base64() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::Base64)
}
#[doc(hidden)]
#[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"]
pub fn eight_bit() -> SinglePartBuilder {
Self::builder().header(ContentTransferEncoding::EightBit)
/// Directly create a `SinglePart` from an UTF-8 HTML content
pub fn html<T: IntoBody>(body: T) -> Self {
Self::builder()
.header(header::ContentType::TEXT_HTML)
.body(body)
}
/// Get the headers from singlepart
@@ -162,7 +136,8 @@ 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");
out.extend_from_slice(&self.body);
out.extend_from_slice(b"\r\n");
@@ -170,7 +145,6 @@ impl EmailFormat for SinglePart {
}
/// The kind of multipart
///
#[derive(Debug, Clone)]
pub enum MultiPartKind {
/// Mixed kind to combine unrelated content parts
@@ -196,32 +170,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(40)
.map(char::from)
.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(),
}
@@ -231,18 +201,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,
@@ -250,14 +219,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,
@@ -279,17 +241,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
@@ -300,11 +262,6 @@ impl MultiPartBuilder {
}
}
/// Creates multipart using part
pub fn part(self, part: Part) -> MultiPart {
self.build().part(part)
}
/// Creates multipart using singlepart
pub fn singlepart(self, part: SinglePart) -> MultiPart {
self.build().singlepart(part)
@@ -323,11 +280,10 @@ impl Default for MultiPartBuilder {
}
/// Multipart variant with parts
///
#[derive(Debug, Clone)]
pub struct MultiPart {
headers: Headers,
parts: Parts,
parts: Vec<Part>,
}
impl MultiPart {
@@ -371,10 +327,11 @@ impl MultiPart {
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
}
/// Add part to multipart
pub fn part(mut self, part: Part) -> Self {
self.parts.push(part);
self
/// Alias for HTML and plain text versions of an email
pub fn alternative_plain_html<T: IntoBody, V: IntoBody>(plain: T, html: V) -> Self {
Self::alternative()
.singlepart(SinglePart::plain(plain))
.singlepart(SinglePart::html(html))
}
/// Add single part to multipart
@@ -391,8 +348,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
@@ -405,16 +367,6 @@ impl MultiPart {
&mut self.headers
}
/// Get the parts from the multipart
pub fn parts(&self) -> &Parts {
&self.parts
}
/// Get a mutable reference to the parts
pub fn parts_mut(&mut self) -> &mut Parts {
&mut self.parts
}
/// Get message content formatted for SMTP
pub fn formatted(&self) -> Vec<u8> {
let mut out = Vec::new();
@@ -425,7 +377,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();
@@ -451,16 +404,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"
@@ -471,16 +422,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",
@@ -492,16 +441,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"
@@ -512,75 +459,60 @@ mod test {
#[test]
fn multi_part_mixed() {
let part = MultiPart::mixed()
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
))
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.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::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")),
)
.singlepart(
SinglePart::builder()
.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")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType(
"application/pgp-encrypted".parse().unwrap(),
))
.body(String::from("Version: 1")),
))
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/octet-stream; name=\"encrypted.asc\""
.parse()
.header(header::ContentType::parse("application/pgp-encrypted").unwrap())
.body(String::from("Version: 1")),
)
.singlepart(
SinglePart::builder()
.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",
@@ -589,27 +521,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",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Version: 1\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"-----BEGIN PGP MESSAGE-----\r\n",
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
"...\r\n",
"-----END PGP MESSAGE-----\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
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() {
@@ -617,27 +553,19 @@ mod test {
"application/pgp-signature".to_owned(),
"pgp-sha256".to_owned(),
)
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
.part(Part::Single(
SinglePart::builder()
.header(header::ContentType("text/plain".parse().unwrap()))
.body(String::from("Test email for signature")),
))
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(
SinglePart::builder()
.header(ContentType(
"application/pgp-signature; name=\"signature.asc\""
.parse()
.header(header::ContentType::TEXT_PLAIN)
.body(String::from("Test email for signature")),
)
.singlepart(
SinglePart::builder()
.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",
@@ -649,101 +577,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",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Test email for signature\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"-----BEGIN PGP SIGNATURE-----\r\n",
"\r\n",
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
"=3FYZ\r\n",
"-----END PGP SIGNATURE-----\r\n",
"\r\n",
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
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")
.part(Part::Single(SinglePart::builder()
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде"))))
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
.singlepart(SinglePart::builder()
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
.header(header::ContentType::TEXT_PLAIN)
.header(header::ContentTransferEncoding::Binary)
.body(String::from("Текст письма в уникоде")))
.singlepart(SinglePart::builder()
.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",
@@ -751,14 +680,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]

View File

@@ -63,7 +63,7 @@
//!
//! ```rust
//! # use std::error::Error;
//! use lettre::message::{header, Message, MultiPart, Part, SinglePart};
//! use lettre::message::{header, Message, MultiPart, SinglePart};
//!
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let m = Message::builder()
@@ -71,21 +71,10 @@
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
//! .to("Hei <hei@domain.tld>".parse()?)
//! .subject("Happy new year")
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body(String::from("Hello, world! :)")),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>",
//! )),
//! ),
//! )?;
//! .multipart(MultiPart::alternative_plain_html(
//! String::from("Hello, world! :)"),
//! String::from("<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>"),
//! ))?;
//! # Ok(())
//! # }
//! ```
@@ -114,7 +103,6 @@
//!
//! <p><b>Hello</b>, <i>world</i>! <img src="cid:123"></p>
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
//!
//! ```
//! </details>
//!
@@ -125,8 +113,8 @@
//!
//! ```rust
//! # use std::error::Error;
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
//! use std::fs;
//! use lettre::message::{Body, header, Message, MultiPart, Part, SinglePart};
//!
//! # fn main() -> Result<(), Box<dyn Error>> {
//! let image = fs::read("docs/lettre.png")?;
@@ -145,47 +133,22 @@
//! MultiPart::mixed()
//! .multipart(
//! MultiPart::alternative()
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
//! .body(String::from("Hello, world! :)")),
//! )
//! .singlepart(SinglePart::plain(String::from("Hello, world! :)")))
//! .multipart(
//! MultiPart::related()
//! .singlepart(SinglePart::html(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
//! )))
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType(
//! "text/html; charset=utf8".parse()?,
//! ))
//! .body(String::from(
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
//! )),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .header(header::ContentType("image/png".parse()?))
//! .header(header::ContentDisposition {
//! disposition: header::DispositionType::Inline,
//! parameters: vec![],
//! })
//! .header(header::ContentId("<123>".into()))
//! .body(image_body),
//! Attachment::new_inline(String::from("123"))
//! .body(image_body, "image/png".parse().unwrap()),
//! ),
//! ),
//! )
//! .singlepart(
//! SinglePart::builder()
//! .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.rs".as_bytes().into(),
//! )],
//! })
//! .body(String::from("fn main() { println!(\"Hello, World!\") }")),
//! ),
//! .singlepart(Attachment::new(String::from("example.rs")).body(
//! String::from("fn main() { println!(\"Hello, World!\") }"),
//! "text/plain".parse().unwrap(),
//! )),
//! )?;
//! # Ok(())
//! # }
@@ -236,36 +199,32 @@
//!
//! fn main() { println!("Hello, World!") }
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
//!
//! ```
//! </details>
use std::{convert::TryFrom, io::Write, iter, time::SystemTime};
pub use attachment::Attachment;
pub use body::{Body, IntoBody, MaybeString};
pub use mailbox::*;
pub use mimebody::*;
pub use mime;
mod attachment;
mod body;
pub mod header;
mod mailbox;
mod mimebody;
mod utf8_b;
use std::convert::TryFrom;
use std::time::SystemTime;
use uuid::Uuid;
use crate::{
address::Envelope,
message::header::{ContentTransferEncoding, EmailDate, Header, Headers, MailboxesHeader},
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
Error as EmailError,
};
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
pub trait EmailFormat {
/// Something that can be formatted as an email message
trait EmailFormat {
// Use a writer?
fn format(&self, out: &mut Vec<u8>);
}
@@ -293,37 +252,40 @@ 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())`.
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
/// if no date has been provided.
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
/// Set `MIME-Version` header to 1.0
///
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
///
@@ -334,25 +296,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 {
@@ -383,16 +345,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.
@@ -401,7 +363,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()
@@ -411,9 +373,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),
))
}
}
@@ -422,7 +384,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)
@@ -439,7 +401,7 @@ impl MessageBuilder {
// https://tools.ietf.org/html/rfc5322#section-3.6
// Insert Date if missing
let res = if self.headers.get::<header::Date>().is_none() {
let mut res = if self.headers.get::<header::Date>().is_none() {
self.date_now()
} else {
self
@@ -448,7 +410,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);
}
@@ -462,6 +424,10 @@ impl MessageBuilder {
Some(e) => e,
None => Envelope::try_from(&res.headers)?,
};
// Remove `Bcc` headers now the envelope is set
res.headers.remove::<header::Bcc>();
Ok(Message {
headers: res.headers,
body,
@@ -475,7 +441,7 @@ impl MessageBuilder {
/// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
/// for `body`.
pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
let maybe_encoding = self.headers.get::<ContentTransferEncoding>().copied();
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
let body = body.into_body(maybe_encoding);
self.headers.set(body.encoding());
@@ -494,6 +460,7 @@ impl MessageBuilder {
}
/// Email message which can be formatted
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
#[derive(Clone, Debug)]
pub struct Message {
headers: Headers,
@@ -533,7 +500,8 @@ 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 {
MessageBody::Mime(p) => p.format(out),
@@ -551,9 +519,17 @@ 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, MultiPart, SinglePart};
use std::time::{Duration, SystemTime};
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
#[test]
fn email_missing_originator() {
@@ -582,10 +558,12 @@ mod test {
#[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)
.bcc("hidden@example.com".parse().unwrap())
.header(header::From(
vec![Mailbox::new(
Some("Каи".into()),
@@ -596,14 +574,14 @@ mod test {
.header(header::To(
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
))
.header(header::Subject("яңа ел белән!".into()))
.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",
@@ -616,7 +594,8 @@ mod test {
#[test]
fn email_with_png() {
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 img = std::fs::read("./docs/lettre.png").unwrap();
let m = Message::builder()
.date(date)
@@ -628,21 +607,16 @@ mod test {
MultiPart::related()
.singlepart(
SinglePart::builder()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.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("image/png".parse().unwrap()))
.header(header::ContentDisposition {
disposition: header::DispositionType::Inline,
parameters: vec![],
})
.header(header::ContentId("<123>".into()))
.header(header::ContentType::parse("image/png").unwrap())
.header(header::ContentDisposition::inline())
.header(header::ContentId::from(String::from("<123>")))
.body(img),
),
)
@@ -653,11 +627,27 @@ mod test {
let expected = String::from_utf8(file_expected).unwrap();
for (i, line) in output.lines().zip(expected.lines()).enumerate() {
if i == 6 || i == 8 || i == 13 || i == 232 {
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,61 +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 error
#[cfg(feature = "file-transport-envelope")]
Json(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),
#[cfg(feature = "file-transport-envelope")]
Json(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),
#[cfg(feature = "file-transport-envelope")]
Json(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))
}
#[cfg(feature = "file-transport-envelope")]
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::Json(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
}
pub(crate) fn envelope<E: Into<BoxError>>(e: E) -> Error {
Error::new(Kind::Envelope, Some(e))
}

View File

@@ -6,11 +6,11 @@
//!
//! ```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());
@@ -38,11 +38,11 @@
//!
//! ```rust
//! # use std::error::Error;
//!
//! #
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
//! # fn main() -> Result<(), Box<dyn Error>> {
//! use lettre::{FileTransport, Message, Transport};
//! use std::env::temp_dir;
//! use lettre::{Transport, Message, FileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::with_envelope(temp_dir());
@@ -66,14 +66,14 @@
//!
//! ```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::{Tokio1Transport, Message, FileTransport};
//! use lettre::{AsyncTransport, Tokio1Executor, Message, AsyncFileTransport};
//!
//! // Write to the local temp directory
//! let sender = FileTransport::new(temp_dir());
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
@@ -91,14 +91,14 @@
//!
//! ```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()?)
@@ -133,16 +133,13 @@
//! ```
pub use self::error::Error;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
#[cfg(feature = "tokio1")]
use crate::Tokio1Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use std::marker::PhantomData;
use std::{
path::{Path, PathBuf},
str,
@@ -156,12 +153,23 @@ 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 = "tokio1", feature = "async-std1"))))]
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
pub struct AsyncFileTransport<E: Executor> {
inner: FileTransport,
marker_: PhantomData<E>,
}
impl FileTransport {
/// Creates a new transport to the given directory
///
@@ -187,11 +195,71 @@ impl FileTransport {
}
}
/// 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))
}
}
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
impl<E> AsyncFileTransport<E>
where
E: Executor,
{
/// Creates a new transport to the given directory
///
/// Writes the email content in eml format.
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self {
inner: FileTransport::new(path),
marker_: PhantomData,
}
}
/// Creates a new transport to the given directory
///
/// Writes the email content in eml format and the envelope
/// in json format.
#[cfg(feature = "file-transport-envelope")]
pub fn with_envelope<P: AsRef<Path>>(path: P) -> Self {
Self {
inner: FileTransport::with_envelope(path),
marker_: PhantomData,
}
}
/// Read a message that was written using the file transport.
///
/// Reads the envelope and the raw message content.
#[cfg(feature = "file-transport-envelope")]
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
let eml_file = self.inner.path.join(format!("{}.eml", email_id));
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
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))
}
}
impl Transport for FileTransport {
type Ok = Id;
type Error = Error;
@@ -202,13 +270,14 @@ impl Transport for FileTransport {
let email_id = Uuid::new_v4();
let file = self.path(&email_id, "eml");
fs::write(file, email)?;
fs::write(file, email).map_err(error::io)?;
#[cfg(feature = "file-transport-envelope")]
{
if self.save_envelope {
let file = self.path(&email_id, "json");
fs::write(file, serde_json::to_string(&envelope)?)?;
let buf = serde_json::to_string(&envelope).map_err(error::envelope)?;
fs::write(file, buf).map_err(error::io)?;
}
}
// use envelope anyway
@@ -218,80 +287,27 @@ impl Transport for FileTransport {
}
}
#[cfg(feature = "async-std1")]
#[cfg(any(feature = "async-std1", 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 file = self.path(&email_id, "eml");
fs::write(file, email).await?;
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.save_envelope {
let file = self.path(&email_id, "json");
fs::write(file, serde_json::to_string(&envelope)?).await?;
}
}
// use envelope anyway
let _ = envelope;
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 = Uuid::new_v4();
let file = self.path(&email_id, "eml");
fs::write(file, email).await?;
#[cfg(feature = "file-transport-envelope")]
{
if self.save_envelope {
let file = self.path(&email_id, "json");
fs::write(file, serde_json::to_string(&envelope)?).await?;
}
}
// use envelope anyway
let _ = envelope;
Ok(email_id.to_string())
}
}
#[cfg(feature = "tokio1")]
#[async_trait]
impl Tokio1Transport for FileTransport {
type Ok = Id;
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio1_crate::fs;
let email_id = Uuid::new_v4();
let file = self.path(&email_id, "eml");
fs::write(file, email).await?;
#[cfg(feature = "file-transport-envelope")]
{
if self.save_envelope {
let file = self.path(&email_id, "json");
fs::write(file, serde_json::to_string(&envelope)?).await?;
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

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 = "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,44 @@ 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 = "tokio1", feature = "async-std1"))]
#[cfg_attr(docsrs, doc(cfg(any(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,13 +1,13 @@
//! 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()?)
@@ -26,37 +26,14 @@
//! # fn main() {}
//! ```
//!
//! ## Async tokio 0.2 example
//!
//! ```rust,no_run
//! # use std::error::Error;
//!
//! # #[cfg(all(feature = "tokio02", feature = "sendmail-transport", feature = "builder"))]
//! # async fn run() -> Result<(), Box<dyn Error>> {
//! use lettre::{Message, Tokio02Transport, 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 = SendmailTransport::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, Tokio1Transport, SendmailTransport};
//! use lettre::{Message, AsyncTransport, Tokio1Executor, AsyncSendmailTransport, SendmailTransport};
//!
//! let email = Message::builder()
//! .from("NoBody <nobody@domain.tld>".parse()?)
@@ -65,7 +42,7 @@
//! .subject("Happy new year")
//! .body(String::from("Be happy!"))?;
//!
//! let sender = SendmailTransport::new();
//! let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
//! let result = sender.send(email).await;
//! assert!(result.is_ok());
//! # Ok(())
@@ -76,10 +53,10 @@
//!
//!```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()?)
@@ -88,7 +65,7 @@
//! .subject("Happy new year")
//! .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(())
@@ -96,38 +73,50 @@
//! ```
pub use self::error::Error;
use crate::address::Envelope;
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
use crate::AsyncStd1Executor;
#[cfg(feature = "tokio1")]
use crate::Tokio1Transport;
use crate::Transport;
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
use crate::Tokio1Executor;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use crate::{AsyncTransport, Executor};
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
use async_trait::async_trait;
#[cfg(any(feature = "async-std1", 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 = "tokio1"))]
#[cfg_attr(docsrs, doc(cfg(any(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(),
}
}
@@ -151,30 +140,34 @@ impl SendmailTransport {
.stderr(Stdio::piped());
c
}
}
#[cfg(feature = "tokio02")]
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
use tokio02_crate::process::Command;
let mut c = Command::new(&self.command);
c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
c.arg("-f").arg(from);
#[cfg(any(feature = "async-std1", 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,
}
c.arg("--")
.args(envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
c
}
#[cfg(feature = "tokio1")]
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
use tokio1_crate::process::Command;
let mut c = Command::new(&self.command);
let mut c = Command::new(&self.inner.command);
c.kill_on_drop(true);
c.arg("-i");
if let Some(from) = envelope.from() {
@@ -192,7 +185,7 @@ impl SendmailTransport {
fn async_std_command(&self, envelope: &Envelope) -> async_std::process::Command {
use async_std::process::Command;
let mut c = Command::new(&self.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");
@@ -214,28 +207,44 @@ impl Default for SendmailTransport {
}
}
#[cfg(any(feature = "async-std1", 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;
@@ -245,47 +254,29 @@ impl AsyncStd1Transport for SendmailTransport {
let mut command = self.async_std_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.output().await?;
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)?))
}
}
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for SendmailTransport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
use tokio02_crate::io::AsyncWriteExt;
let mut command = self.tokio02_command(envelope);
// Spawn the sendmail command
let mut process = command.spawn()?;
process.stdin.as_mut().unwrap().write_all(&email).await?;
let output = process.wait_with_output().await?;
if output.status.success() {
Ok(())
} else {
Err(Error::Client(String::from_utf8(output.stderr)?))
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
Err(error::client(stderr))
}
}
}
#[cfg(feature = "tokio1")]
#[async_trait]
impl Tokio1Transport for SendmailTransport {
impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
type Ok = ();
type Error = Error;
@@ -295,15 +286,22 @@ impl Tokio1Transport for SendmailTransport {
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,46 +1,31 @@
use std::{
fmt::{self, Debug},
marker::PhantomData,
};
use async_trait::async_trait;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
use super::Tls;
use super::{
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
};
#[cfg(feature = "async-std1")]
use crate::AsyncStd1Transport;
use crate::Envelope;
#[cfg(feature = "tokio02")]
use crate::Tokio02Transport;
use crate::AsyncStd1Executor;
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport;
#[cfg(feature = "tokio1")]
use crate::Tokio1Transport;
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 = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransport<E> {
// TODO: pool
inner: AsyncSmtpClient<C>,
}
#[cfg(feature = "tokio02")]
#[async_trait]
impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
type Ok = Response;
type Error = Error;
/// Sends an email
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
let mut conn = self.inner.connection().await?;
let result = conn.send(envelope, email).await?;
conn.quit().await?;
Ok(result)
}
inner: AsyncSmtpClient<E>,
}
#[cfg(feature = "tokio1")]
#[async_trait]
impl Tokio1Transport for AsyncSmtpTransport<Tokio1Connector> {
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
type Ok = Response;
type Error = Error;
@@ -58,7 +43,7 @@ impl Tokio1Transport for AsyncSmtpTransport<Tokio1Connector> {
#[cfg(feature = "async-std1")]
#[async_trait]
impl AsyncStd1Transport for AsyncSmtpTransport<AsyncStd1Connector> {
impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
type Ok = Response;
type Error = Error;
@@ -74,9 +59,9 @@ impl AsyncStd1Transport for AsyncSmtpTransport<AsyncStd1Connector> {
}
}
impl<C> AsyncSmtpTransport<C>
impl<E> AsyncSmtpTransport<E>
where
C: AsyncSmtpConnector,
E: Executor,
{
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
///
@@ -85,15 +70,21 @@ where
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
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())?;
@@ -114,15 +105,21 @@ where
/// An error is returned if the connection can't be upgraded. No credentials
/// or emails will be sent to the server, protecting from downgrade attacks.
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
#[cfg_attr(
docsrs,
doc(cfg(any(
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())?;
@@ -134,7 +131,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()
}
@@ -158,10 +155,29 @@ where
}
}
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`].
#[allow(missing_debug_implementations)]
#[derive(Clone)]
#[derive(Debug, Clone)]
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransportBuilder {
info: SmtpInfo,
}
@@ -194,48 +210,52 @@ impl AsyncSmtpTransportBuilder {
/// Set the TLS settings to use
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
feature = "async-std1-rustls-tls"
))]
pub fn tls(mut self, tls: Tls) -> Self {
#[cfg_attr(
docsrs,
doc(cfg(any(
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,
@@ -250,152 +270,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 = "tokio1")]
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
pub struct Tokio1Connector;
#[async_trait]
#[cfg(feature = "tokio1")]
impl AsyncSmtpConnector for Tokio1Connector {
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?;
}
_ => (),
impl<E> AsyncSmtpClient<E>
where
E: Executor,
{
fn clone(&self) -> Self {
Self {
info: self.info.clone(),
marker_: PhantomData,
}
Ok(conn)
}
}
#[derive(Debug, Copy, Clone, Default)]
#[cfg(feature = "async-std1")]
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
pub struct AsyncStd1Connector;
#[async_trait]
#[cfg(feature = "async-std1")]
impl AsyncSmtpConnector for AsyncStd1Connector {
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)
}
}
mod private {
use super::*;
pub trait Sealed {}
#[cfg(feature = "tokio02")]
impl Sealed for Tokio02Connector {}
#[cfg(feature = "tokio1")]
impl Sealed for Tokio1Connector {}
#[cfg(feature = "async-std1")]
impl Sealed for AsyncStd1Connector {}
}

View File

@@ -1,14 +1,15 @@
//! 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 +36,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 +87,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 +105,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;
@@ -45,20 +44,6 @@ impl AsyncSmtpConnection {
&self.server_info
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
#[cfg(feature = "tokio02")]
pub async fn connect_tokio02(
hostname: &str,
port: u16,
hello_name: &ClientId,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncSmtpConnection, Error> {
let stream = AsyncNetworkStream::connect_tokio02(hostname, port, tls_parameters).await?;
Self::connect_impl(stream, hello_name).await
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
@@ -112,9 +97,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,
@@ -163,7 +171,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"))
}
}
@@ -210,12 +218,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?;
@@ -234,7 +240,7 @@ impl AsyncSmtpConnection {
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
Err(error::response("Unexpected number of challenges"))
} else {
Ok(response)
}
@@ -258,8 +264,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)));
@@ -270,27 +284,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,46 +1,33 @@
#[cfg(any(
feature = "tokio02-rustls-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-rustls-tls"
))]
use std::sync::Arc;
use std::{
mem,
net::SocketAddr,
pin::Pin,
task::{Context, Poll},
};
use futures_io::{AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite};
use futures_io::{Error as IoError, ErrorKind, Result as IoResult};
#[cfg(feature = "tokio02")]
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
use futures_io::{
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
Result as IoResult,
};
#[cfg(feature = "tokio1")]
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
#[cfg(feature = "async-std1")]
use async_std::net::TcpStream as AsyncStd1TcpStream;
#[cfg(feature = "tokio02")]
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
#[cfg(feature = "tokio1")]
use tokio1_crate::net::TcpStream as Tokio1TcpStream;
#[cfg(feature = "async-std1-native-tls")]
use async_native_tls::TlsStream as AsyncStd1TlsStream;
#[cfg(feature = "tokio02-native-tls")]
use tokio02_native_tls_crate::TlsStream as Tokio02TlsStream;
#[cfg(feature = "tokio1-native-tls")]
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "async-std1-rustls-tls")]
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio02-rustls-tls")]
use tokio02_rustls::client::TlsStream as Tokio02RustlsTlsStream;
#[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
#[cfg(any(
feature = "tokio02-native-tls",
feature = "tokio02-rustls-tls",
feature = "tokio1-native-tls",
feature = "tokio1-rustls-tls",
feature = "async-std1-native-tls",
@@ -48,7 +35,7 @@ use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
))]
use super::InnerTlsParameters;
use super::TlsParameters;
use crate::transport::smtp::Error;
use crate::transport::smtp::{error, Error};
/// A network stream
pub struct AsyncNetworkStream {
@@ -61,15 +48,6 @@ pub struct AsyncNetworkStream {
#[allow(clippy::large_enum_variant)]
#[allow(dead_code)]
enum InnerAsyncNetworkStream {
/// Plain Tokio 0.2 TCP stream
#[cfg(feature = "tokio02")]
Tokio02Tcp(Tokio02TcpStream),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-native-tls")]
Tokio02NativeTls(Tokio02TlsStream<Tokio02TcpStream>),
/// Encrypted Tokio 0.2 TCP stream
#[cfg(feature = "tokio02-rustls-tls")]
Tokio02RustlsTls(Tokio02RustlsTlsStream<Tokio02TcpStream>),
/// Plain Tokio 1.x TCP stream
#[cfg(feature = "tokio1")]
Tokio1Tcp(Tokio1TcpStream),
@@ -104,14 +82,6 @@ impl AsyncNetworkStream {
/// Returns peer's address
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
s.get_ref().get_ref().get_ref().peer_addr()
}
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
#[cfg(feature = "tokio1-native-tls")]
@@ -136,28 +106,15 @@ impl AsyncNetworkStream {
}
}
#[cfg(feature = "tokio02")]
pub async fn connect_tokio02(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio02TcpStream::connect((hostname, port)).await?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
stream.upgrade_tls(tls_parameters).await?;
}
Ok(stream)
}
#[cfg(feature = "tokio1")]
pub async fn connect_tokio1(
hostname: &str,
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = Tokio1TcpStream::connect((hostname, port)).await?;
let tcp_stream = Tokio1TcpStream::connect((hostname, port))
.await
.map_err(error::connection)?;
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
if let Some(tls_parameters) = tls_parameters {
@@ -172,7 +129,9 @@ impl AsyncNetworkStream {
port: u16,
tls_parameters: Option<TlsParameters>,
) -> Result<AsyncNetworkStream, Error> {
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port)).await?;
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 {
@@ -183,27 +142,6 @@ impl AsyncNetworkStream {
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
match &self.inner {
#[cfg(all(
feature = "tokio02",
not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))
))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
let _ = tls_parameters;
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-rustls-tls feature");
}
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = match tcp_stream {
InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
Ok(())
}
#[cfg(all(
feature = "tokio1",
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))
@@ -216,13 +154,15 @@ impl AsyncNetworkStream {
#[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::Tokio1Tcp(tcp_stream) => tcp_stream,
_ => unreachable!(),
};
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters).await?;
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
.await
.map_err(error::connection)?;
Ok(())
}
#[cfg(all(
@@ -237,68 +177,28 @@ impl AsyncNetworkStream {
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
// get owned TcpStream
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
let tcp_stream = 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?;
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
.await
.map_err(error::connection)?;
Ok(())
}
_ => Ok(()),
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
async fn upgrade_tokio02_tls(
tcp_stream: Tokio02TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
InnerTlsParameters::NativeTls(connector) => {
#[cfg(not(feature = "tokio02-native-tls"))]
panic!("built without the tokio02-native-tls feature");
#[cfg(feature = "tokio02-native-tls")]
return {
use tokio02_native_tls_crate::TlsConnector;
let connector = TlsConnector::from(connector);
let stream = connector.connect(&domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
};
}
#[cfg(feature = "rustls-tls")]
InnerTlsParameters::RustlsTls(config) => {
#[cfg(not(feature = "tokio02-rustls-tls"))]
panic!("built without the tokio02-rustls-tls feature");
#[cfg(feature = "tokio02-rustls-tls")]
return {
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
let connector = TlsConnector::from(Arc::new(config));
let stream = connector.connect(domain, tcp_stream).await?;
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
};
}
}
}
#[allow(unused_variables)]
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
async fn upgrade_tokio1_tls(
tcp_stream: Tokio1TcpStream,
mut tls_parameters: TlsParameters,
) -> Result<InnerAsyncNetworkStream, Error> {
let domain = std::mem::take(&mut tls_parameters.domain);
let domain = mem::take(&mut tls_parameters.domain);
match tls_parameters.connector {
#[cfg(feature = "native-tls")]
@@ -311,7 +211,10 @@ impl AsyncNetworkStream {
use tokio1_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::Tokio1NativeTls(stream))
};
}
@@ -324,10 +227,14 @@ impl AsyncNetworkStream {
return {
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?;
let connector = TlsConnector::from(config);
let stream = connector
.connect(domain, tcp_stream)
.await
.map_err(error::connection)?;
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
};
}
@@ -340,7 +247,7 @@ impl AsyncNetworkStream {
tcp_stream: AsyncStd1TcpStream,
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")]
@@ -370,13 +277,16 @@ impl AsyncNetworkStream {
#[cfg(feature = "async-std1-rustls-tls")]
return {
use async_rustls::webpki::DNSNameRef;
use async_rustls::TlsConnector;
use async_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::AsyncStd1RustlsTls(stream))
};
}
@@ -385,12 +295,6 @@ impl AsyncNetworkStream {
pub fn is_encrypted(&self) -> bool {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(_) => false,
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
#[cfg(feature = "tokio1-native-tls")]
@@ -415,12 +319,6 @@ impl FuturesAsyncRead for AsyncNetworkStream {
buf: &mut [u8],
) -> Poll<IoResult<usize>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => {
let mut b = Tokio1ReadBuf::new(buf);
@@ -473,12 +371,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
buf: &[u8],
) -> Poll<IoResult<usize>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
#[cfg(feature = "tokio1-native-tls")]
@@ -504,12 +396,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
match self.inner {
#[cfg(feature = "tokio02")]
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-native-tls")]
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio02-rustls-tls")]
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1")]
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
#[cfg(feature = "tokio1-native-tls")]
@@ -531,12 +417,6 @@ impl FuturesAsyncWrite for AsyncNetworkStream {
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")]

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 = "tokio1", feature = "async-std1"))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub(crate) use self::async_connection::AsyncSmtpConnection;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg(any(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 = "tokio1", feature = "async-std1"))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_connection;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg(any(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,
}
@@ -80,7 +78,15 @@ impl ClientCodec {
match self.escape_count {
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
2 => self.escape_count = if *byte == b'.' { 3 } else { 0 },
2 => {
self.escape_count = if *byte == b'.' {
3
} else if *byte == b'\r' {
1
} else {
0
}
}
_ => unreachable!(),
}
if self.escape_count == 3 {
@@ -113,6 +119,7 @@ mod test {
let mut buf: Vec<u8> = vec![];
codec.encode(b"test\r\n", &mut buf);
codec.encode(b"test\r\n\r\n", &mut buf);
codec.encode(b".\r\n", &mut buf);
codec.encode(b"\r\ntest", &mut buf);
codec.encode(b"te\r\n.\r\nst", &mut buf);
@@ -123,7 +130,7 @@ mod test {
codec.encode(b"test", &mut buf);
assert_eq!(
String::from_utf8(buf).unwrap(),
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
"test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
);
}

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
}
@@ -118,18 +131,12 @@ impl TlsParametersBuilder {
/// depending on which one is available
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
// TODO: remove below line once native-tls is supported with async-std
#[allow(unreachable_code)]
pub fn build(self) -> Result<TlsParameters, Error> {
// TODO: remove once native-tls is supported with async-std
#[cfg(all(feature = "rustls-tls", feature = "async-std1"))]
#[cfg(feature = "rustls-tls")]
return self.build_rustls();
#[cfg(feature = "native-tls")]
#[cfg(not(feature = "rustls-tls"))]
return self.build_native();
#[cfg(not(feature = "native-tls"))]
return self.build_rustls();
}
/// Creates a new `TlsParameters` using native-tls with the provided configuration
@@ -145,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,
@@ -162,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 {
@@ -174,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,
})
}
@@ -185,7 +190,7 @@ pub enum InnerTlsParameters {
#[cfg(feature = "native-tls")]
NativeTls(TlsConnector),
#[cfg(feature = "rustls-tls")]
RustlsTls(ClientConfig),
RustlsTls(Arc<ClientConfig>),
}
impl TlsParameters {
@@ -230,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")]
@@ -248,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 = {
@@ -257,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 {
@@ -269,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 UTF8 in 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(
err.first_line()
.unwrap_or("transient error during SMTP transaction"),
),
Permanent(ref err) => fmt.write_str(
err.first_line()
.unwrap_or("permanent error during SMTP transaction"),
),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
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),
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),
}),
}
}
}
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 {
@@ -68,18 +72,19 @@ impl ClientId {
/// Supported ESMTP keywords
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
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),
@@ -103,11 +108,11 @@ pub struct ServerInfo {
/// Server name
///
/// The name given in the server banner
pub name: String,
name: String,
/// ESMTP features supported by the server
///
/// It contains the features supported by the server and known by the `Extension` module.
pub features: HashSet<Extension>,
features: HashSet<Extension>,
}
impl Display for ServerInfo {
@@ -126,12 +131,12 @@ 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();
for line in response.message.as_slice() {
for line in response.message() {
if line.is_empty() {
continue;
}
@@ -193,6 +198,11 @@ impl ServerInfo {
}
None
}
/// The name given in the server banner
pub fn name(&self) -> &str {
self.name.as_ref()
}
}
/// A `MAIL FROM` extension parameter

View File

@@ -43,8 +43,7 @@
//! .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,118 +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 = "async-std1")]
pub use self::async_transport::AsyncStd1Connector;
#[cfg(feature = "tokio02")]
pub use self::async_transport::Tokio02Connector;
#[cfg(feature = "tokio1")]
pub use self::async_transport::Tokio1Connector;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
pub use self::async_transport::{
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder,
};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
#[cfg(feature = "r2d2")]
pub use self::pool::PoolConfig;
#[cfg(feature = "r2d2")]
@@ -183,7 +137,7 @@ use crate::transport::smtp::{
use client::Tls;
use std::time::Duration;
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
mod async_transport;
pub mod authentication;
pub mod client;
@@ -194,7 +148,7 @@ pub mod extension;
mod pool;
pub mod response;
mod transport;
pub mod util;
pub(super) mod util;
// Registered port numbers:
// https://www.iana.
@@ -206,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);
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
@@ -129,17 +137,19 @@ impl Code {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Response {
/// Response code
pub code: Code,
code: Code,
/// Server response string (optional)
/// Handle multiline responses
pub message: Vec<String>,
message: Vec<String>,
}
impl FromStr for Response {
type Err = Error;
fn from_str(s: &str) -> result::Result<Response, Error> {
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
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
@@ -173,6 +180,16 @@ impl Response {
pub fn first_line(&self) -> Option<&str> {
self.message.first().map(String::as_str)
}
/// Response code
pub fn code(&self) -> Code {
self.code
}
/// Server response string (array of lines)
pub fn message(&self) -> impl Iterator<Item = &str> {
self.message.iter().map(String::as_str)
}
}
// Parsers (originally from tokio-smtp)

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()?;
@@ -48,6 +48,7 @@ impl SmtpTransport {
/// 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())?;
@@ -111,8 +113,7 @@ impl SmtpTransport {
/// Contains client configuration.
/// Instances of this struct can be created using functions of [`SmtpTransport`].
#[allow(missing_debug_implementations)]
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct SmtpTransportBuilder {
info: SmtpInfo,
#[cfg(feature = "r2d2")]
@@ -153,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
@@ -184,7 +186,7 @@ impl SmtpTransportBuilder {
}
/// Build client
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct SmtpClient {
info: SmtpInfo,
}

View File

@@ -4,7 +4,6 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> {

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>> {
@@ -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 = "tokio1", feature = "async-std1"))]
use crate::AsyncTransport;
use crate::{address::Envelope, Transport};
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
use async_trait::async_trait;
use std::{error::Error as StdError, fmt};
@@ -84,20 +80,9 @@ impl Transport for StubTransport {
}
}
#[cfg(feature = "async-std1")]
#[cfg(any(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;

View File

@@ -1,13 +1,14 @@
Date: Tue, 15 Nov 1994 08:12:31 GMT
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="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
Content-Type: multipart/related;
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
Content-Type: text/html; charset=utf8
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 7bit
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>

View File

@@ -1,25 +1,30 @@
#[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::{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())
.date(default_date())
.body(String::from("Be happy!"))
.unwrap();
@@ -36,7 +41,7 @@ mod test {
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
@@ -48,14 +53,13 @@ mod test {
#[test]
#[cfg(feature = "file-transport-envelope")]
fn file_transport_with_envelope() {
use lettre::Transport;
let sender = FileTransport::with_envelope(temp_dir());
let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
.to("Hei <hei@domain.tld>".parse().unwrap())
.subject("Happy new year")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.date(default_date())
.body(String::from("Be happy!"))
.unwrap();
@@ -75,70 +79,49 @@ mod test {
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"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();
assert_eq!(
json,
"{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"}"
);
let (e, m) = sender.read(&id).unwrap();
assert_eq!(&e, email.envelope());
assert_eq!(m, email.formatted());
remove_file(eml_file).unwrap();
remove_file(json_file).unwrap();
}
}
#[cfg(feature = "async-std1")]
#[async_std::test]
async fn file_transport_asyncstd1() {
use lettre::AsyncStd1Transport;
#[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},
};
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(String::from("Be happy!"))
.unwrap();
use tokio1_crate as tokio;
let result = sender.send(email).await;
let id = result.unwrap();
let eml_file = temp_dir().join(format!("{}.eml", id));
let eml = read_to_string(&eml_file).unwrap();
assert_eq!(
eml,
concat!(
"From: NoBody <nobody@domain.tld>\r\n",
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"Content-Transfer-Encoding: 7bit\r\n",
"\r\n",
"Be happy!"
)
);
remove_file(eml_file).unwrap();
}
#[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())
.date(default_date())
.body(String::from("Be happy!"))
.unwrap();
@@ -155,7 +138,56 @@ mod test {
"Reply-To: Yuin <yuin@domain.tld>\r\n",
"To: Hei <hei@domain.tld>\r\n",
"Subject: Happy new year\r\n",
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
"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!"

View File

@@ -1,14 +1,10 @@
#[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())
@@ -22,39 +18,52 @@ mod test {
println!("{:?}", result);
assert!(result.is_ok());
}
}
#[cfg(feature = "async-std1")]
#[async_std::test]
async fn sendmail_transport_asyncstd1() {
use lettre::AsyncStd1Transport;
#[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;
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(String::from("Be happy!"))
.unwrap();
let result = sender.send(email).await;
println!("{:?}", result);
assert!(result.is_ok());
}
#[cfg(feature = "tokio02")]
#[tokio::test]
async fn sendmail_transport_tokio02() {
use lettre::Tokio02Transport;
let sender = SendmailTransport::new();
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")
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
.body(String::from("Be happy!"))
.unwrap();

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]
@@ -12,10 +12,62 @@ mod test {
.subject("Happy new year")
.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 = "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()
@@ -22,32 +18,39 @@ mod test {
sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err();
}
}
#[cfg(feature = "async-std1")]
#[async_std::test]
async fn stub_transport_asyncstd1() {
use lettre::AsyncStd1Transport;
#[cfg(test)]
#[cfg(all(feature = "builder", feature = "tokio1"))]
mod tokio_1 {
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(String::from("Be happy!"))
.unwrap();
use tokio1_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;
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()
@@ -55,7 +58,6 @@ 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(String::from("Be happy!"))
.unwrap();