Compare commits
60 Commits
v0.10.0-al
...
v0.10.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47cad567b0 | ||
|
|
b0e2fc9bca | ||
|
|
1d8249165c | ||
|
|
98fc0cb2f3 | ||
|
|
0439bab874 | ||
|
|
504fc51b26 | ||
|
|
d54343cf00 | ||
|
|
904789ac3d | ||
|
|
94cae6df0d | ||
|
|
f17dccc46d | ||
|
|
7e7f05eb45 | ||
|
|
99df9e8d7c | ||
|
|
1b5109b6ac | ||
|
|
a4be3c4cd8 | ||
|
|
4586f2ad8a | ||
|
|
31de9e508b | ||
|
|
69334fe5eb | ||
|
|
2ad2444183 | ||
|
|
8afa442e93 | ||
|
|
486e0f9d50 | ||
|
|
acc4ff4898 | ||
|
|
1728d57c34 | ||
|
|
53bfb65423 | ||
|
|
61b08814c9 | ||
|
|
0e74042b4e | ||
|
|
29affe9398 | ||
|
|
b10f6ff8de | ||
|
|
2002a9d75a | ||
|
|
1193e1134d | ||
|
|
7c6ade7afe | ||
|
|
3bc729ca64 | ||
|
|
f041c00df7 | ||
|
|
fe8dc4967d | ||
|
|
137566a4e4 | ||
|
|
216c612931 | ||
|
|
a429a24913 | ||
|
|
648bf2b2f6 | ||
|
|
509a623a27 | ||
|
|
a681c6b49d | ||
|
|
22efe341fe | ||
|
|
97fba6a47e | ||
|
|
f7066ac858 | ||
|
|
9379f2e328 | ||
|
|
05133a7102 | ||
|
|
d7d05bf48a | ||
|
|
34ac265d60 | ||
|
|
bbf56de83d | ||
|
|
b594945695 | ||
|
|
5c83120986 | ||
|
|
d4df9a2965 | ||
|
|
d2aa959845 | ||
|
|
d1f016e8e2 | ||
|
|
9146212a3e | ||
|
|
a04866acfb | ||
|
|
be88aabae2 | ||
|
|
6fbb3bf440 | ||
|
|
9d8c31bef8 | ||
|
|
0ea3bfbd13 | ||
|
|
a0980d017b | ||
|
|
40c8a9d000 |
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
33
Cargo.toml
33
Cargo.toml
@@ -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"]
|
||||
|
||||
23
README.md
23
README.md
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
200
src/executor.rs
Normal 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 {}
|
||||
}
|
||||
259
src/lib.rs
259
src/lib.rs
@@ -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
91
src/message/attachment.rs
Normal 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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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😀");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
89
src/message/header/content_disposition.rs
Normal file
89
src/message/header/content_disposition.rs
Normal 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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
123
src/message/header/content_type.rs
Normal file
123
src/message/header/content_type.rs
Normal 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
133
src/message/header/date.rs
Normal 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),
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
7
testdata/email_with_png.eml
vendored
7
testdata/email_with_png.eml
vendored
@@ -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>
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user