Compare commits
35 Commits
v0.10.0-be
...
v0.10.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94cc0149d1 | ||
|
|
a89383cdb6 | ||
|
|
592593f4b8 | ||
|
|
97d3c760c0 | ||
|
|
8f28b0c341 | ||
|
|
dc9c5df210 | ||
|
|
c9b3fa0baa | ||
|
|
addf8754dd | ||
|
|
af157c5f26 | ||
|
|
3e8988ae55 | ||
|
|
941a00bcaa | ||
|
|
14079bff8c | ||
|
|
696c06e8d7 | ||
|
|
d4f7618898 | ||
|
|
e0a0a2e624 | ||
|
|
9ab6bb56d3 | ||
|
|
e1d3778329 | ||
|
|
623d69c553 | ||
|
|
55c2618201 | ||
|
|
9f550bce86 | ||
|
|
e875d9ff64 | ||
|
|
aadcc0f83c | ||
|
|
b534a18017 | ||
|
|
0684bccd47 | ||
|
|
4471759221 | ||
|
|
ed454819ee | ||
|
|
47cad567b0 | ||
|
|
b0e2fc9bca | ||
|
|
1d8249165c | ||
|
|
98fc0cb2f3 | ||
|
|
0439bab874 | ||
|
|
504fc51b26 | ||
|
|
d54343cf00 | ||
|
|
904789ac3d | ||
|
|
94cae6df0d |
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
- master
|
||||
|
||||
env:
|
||||
RUSTFLAGS: "--cfg lettre_ignore_tls_mismatch"
|
||||
RUSTDOCFLAGS: "--cfg lettre_ignore_tls_mismatch"
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
jobs:
|
||||
@@ -79,8 +81,8 @@ jobs:
|
||||
rust: stable
|
||||
- name: beta
|
||||
rust: beta
|
||||
- name: 1.46.0
|
||||
rust: 1.46.0
|
||||
- name: 1.52.1
|
||||
rust: 1.52.1
|
||||
|
||||
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.52.1
|
||||
* 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
|
||||
@@ -31,7 +31,8 @@ Several breaking changes were made between 0.9 and 0.10, but changes should be s
|
||||
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
|
||||
* The `new` method of `ClientId` is deprecated
|
||||
* Rename `serde-impls` feature to `serde`
|
||||
|
||||
* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of
|
||||
`/usr/bin/sendmail`.
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
|
||||
45
Cargo.toml
45
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-beta.4"
|
||||
version = "0.10.0-rc.4"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "https://lettre.rs"
|
||||
@@ -19,6 +19,7 @@ maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
idna = "0.2"
|
||||
once_cell = "1"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
||||
|
||||
# builder
|
||||
@@ -27,7 +28,6 @@ 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
|
||||
@@ -36,15 +36,14 @@ serde = { version = "1", optional = true, features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
# smtp
|
||||
nom = { version = "6", default-features = false, features = ["alloc", "std"], optional = true }
|
||||
r2d2 = { version = "0.8", optional = true } # feature
|
||||
nom = { version = "7", optional = true }
|
||||
hostname = { version = "0.3", optional = true } # feature
|
||||
|
||||
## tls
|
||||
native-tls = { version = "0.2", optional = true } # feature
|
||||
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true }
|
||||
webpki = { version = "0.21", optional = true }
|
||||
webpki-roots = { version = "0.21", optional = true }
|
||||
rustls = { version = "0.20", features = ["dangerous_configuration"], optional = true }
|
||||
rustls-pemfile = { version = "0.2.1", optional = true }
|
||||
webpki-roots = { version = "0.22", optional = true }
|
||||
|
||||
# async
|
||||
futures-io = { version = "0.3.7", optional = true }
|
||||
@@ -54,22 +53,18 @@ async-trait = { version = "0.1", optional = true }
|
||||
## async-std
|
||||
async-std = { version = "1.8", optional = true, features = ["unstable"] }
|
||||
#async-native-tls = { version = "0.3.3", optional = true }
|
||||
async-rustls = { version = "0.2", optional = true }
|
||||
futures-rustls = { version = "0.22", 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_crate = { package = "tokio", version = "1", features = ["fs", "process", "time", "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 }
|
||||
tokio1_rustls = { package = "tokio-rustls", version = "0.23", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
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,8 +75,9 @@ harness = false
|
||||
name = "transport_smtp"
|
||||
|
||||
[features]
|
||||
default = ["smtp-transport", "native-tls", "hostname", "r2d2", "builder"]
|
||||
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
||||
builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable"]
|
||||
mime03 = ["mime"]
|
||||
|
||||
# transports
|
||||
file-transport = ["uuid"]
|
||||
@@ -89,22 +85,21 @@ file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
||||
sendmail-transport = []
|
||||
smtp-transport = ["base64", "nom"]
|
||||
|
||||
rustls-tls = ["webpki", "webpki-roots", "rustls"]
|
||||
pool = ["futures-util"]
|
||||
|
||||
rustls-tls = ["webpki-roots", "rustls", "rustls-pemfile"]
|
||||
|
||||
# async
|
||||
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
|
||||
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"]
|
||||
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"]
|
||||
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "futures-rustls"]
|
||||
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
|
||||
|
||||
[[example]]
|
||||
name = "basic_html"
|
||||
@@ -130,14 +125,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"]
|
||||
|
||||
24
README.md
24
README.md
@@ -28,8 +28,8 @@
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.10.0-beta.4">
|
||||
<img src="https://deps.rs/crate/lettre/0.10.0-beta.4/status.svg"
|
||||
<a href="https://deps.rs/crate/lettre/0.10.0-rc.4">
|
||||
<img src="https://deps.rs/crate/lettre/0.10.0-rc.4/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.46 or newer.
|
||||
This library requires Rust 1.52.1 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.10.0-beta.4"
|
||||
lettre = "0.10.0-rc.4"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
@@ -98,7 +103,8 @@ match mailer.send(&email) {
|
||||
|
||||
## Testing
|
||||
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
|
||||
such a server can be launched with `python -m smtpd -n -c DebuggingServer localhost:2525`
|
||||
|
||||
Alternatively only unit tests can be run by doing `cargo test --lib`.
|
||||
|
||||
|
||||
@@ -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,37 +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, AsyncTransport, Message,
|
||||
Tokio02Executor,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer: AsyncSmtpTransport<Tokio02Executor> =
|
||||
AsyncSmtpTransport::<Tokio02Executor>::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
@@ -1,37 +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, AsyncTransport, Message,
|
||||
Tokio02Executor,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer: AsyncSmtpTransport<Tokio02Executor> =
|
||||
AsyncSmtpTransport::<Tokio02Executor>::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {:?}", e),
|
||||
}
|
||||
}
|
||||
@@ -174,15 +174,10 @@ impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, AddressError> {
|
||||
let mut parts = val.rsplitn(2, '@');
|
||||
let domain = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
let user = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
|
||||
Address::check_user(user)?;
|
||||
Address::check_domain(domain)?;
|
||||
let at_start = check_address(val)?;
|
||||
Ok(Address {
|
||||
serialized: val.into(),
|
||||
at_start: user.len(),
|
||||
at_start,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -209,6 +204,18 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Address {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(serialized: String) -> Result<Self, AddressError> {
|
||||
let at_start = check_address(&serialized)?;
|
||||
Ok(Address {
|
||||
serialized,
|
||||
at_start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Address {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.serialized
|
||||
@@ -221,6 +228,16 @@ impl AsRef<OsStr> for Address {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_address(val: &str) -> Result<usize, AddressError> {
|
||||
let mut parts = val.rsplitn(2, '@');
|
||||
let domain = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
let user = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
|
||||
Address::check_user(user)?;
|
||||
Address::check_domain(domain)?;
|
||||
Ok(user.len())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
/// Errors in email addresses parsing
|
||||
pub enum AddressError {
|
||||
|
||||
194
src/executor.rs
194
src/executor.rs
@@ -1,29 +1,35 @@
|
||||
use async_trait::async_trait;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
use futures_util::future::BoxFuture;
|
||||
|
||||
use std::fmt::Debug;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "file-transport")]
|
||||
use std::io::Result as IoResult;
|
||||
#[cfg(feature = "file-transport")]
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::client::AsyncSmtpConnection;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::client::Tls;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::extension::ClientId;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
@@ -35,17 +41,31 @@ use crate::transport::smtp::Error;
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
|
||||
)]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[async_trait]
|
||||
pub trait Executor: Debug + Send + Sync + private::Sealed {
|
||||
pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle: SpawnHandle;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep: Future<Output = ()> + Send + 'static;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error>;
|
||||
@@ -59,70 +79,11 @@ pub trait Executor: Debug + Send + Sync + private::Sealed {
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `tokio` `0.2.x`
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
|
||||
#[derive(Debug)]
|
||||
pub struct Tokio02Executor;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl Executor for Tokio02Executor {
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||
tokio02_crate::fs::read(path).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||
tokio02_crate::fs::write(path, contents).await
|
||||
}
|
||||
pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
|
||||
async fn shutdown(self);
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `tokio` `1.x`
|
||||
@@ -143,11 +104,33 @@ pub struct Tokio1Executor;
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Executor for Tokio1Executor {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle = tokio1_crate::task::JoinHandle<()>;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep = tokio1_crate::time::Sleep;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
tokio1_crate::spawn(fut)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep {
|
||||
tokio1_crate::time::sleep(duration)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
@@ -158,8 +141,13 @@ impl Executor for Tokio1Executor {
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_tokio1(hostname, port, hello_name, tls_parameters).await?;
|
||||
let mut conn = AsyncSmtpConnection::connect_tokio1(
|
||||
(hostname, port),
|
||||
timeout,
|
||||
hello_name,
|
||||
tls_parameters,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
match tls {
|
||||
@@ -190,6 +178,14 @@ impl Executor for Tokio1Executor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
#[async_trait]
|
||||
impl SpawnHandle for tokio1_crate::task::JoinHandle<()> {
|
||||
async fn shutdown(self) {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `async-std` `1.x`
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
@@ -208,11 +204,34 @@ pub struct AsyncStd1Executor;
|
||||
#[async_trait]
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Executor for AsyncStd1Executor {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle = async_std::task::JoinHandle<()>;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep = BoxFuture<'static, ()>;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
async_std::task::spawn(fut)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep {
|
||||
let fut = async move { async_std::task::sleep(duration).await };
|
||||
Box::pin(fut)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
@@ -223,9 +242,13 @@ impl Executor for AsyncStd1Executor {
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_asyncstd1(hostname, port, hello_name, tls_parameters)
|
||||
.await?;
|
||||
let mut conn = AsyncSmtpConnection::connect_asyncstd1(
|
||||
(hostname, port),
|
||||
timeout,
|
||||
hello_name,
|
||||
tls_parameters,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
match tls {
|
||||
@@ -256,17 +279,28 @@ impl Executor for AsyncStd1Executor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
#[async_trait]
|
||||
impl SpawnHandle for async_std::task::JoinHandle<()> {
|
||||
async fn shutdown(self) {
|
||||
self.cancel().await;
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl Sealed for Tokio02Executor {}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Sealed for Tokio1Executor {}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Sealed for AsyncStd1Executor {}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
impl Sealed for async_std::task::JoinHandle<()> {}
|
||||
}
|
||||
|
||||
79
src/lib.rs
79
src/lib.rs
@@ -6,7 +6,7 @@
|
||||
//! * Secure defaults
|
||||
//! * Async support
|
||||
//!
|
||||
//! Lettre requires Rust 1.46 or newer.
|
||||
//! Lettre requires Rust 1.52.1 or newer.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
@@ -27,7 +27,7 @@
|
||||
//! _Send emails using [`SMTP`]_
|
||||
//!
|
||||
//! * **smtp-transport** 📫: Enable the SMTP transport
|
||||
//! * **r2d2** 📫: Connection pool for SMTP transport
|
||||
//! * **pool** 📫: 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
|
||||
@@ -37,7 +37,6 @@
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL on Linux.
|
||||
//!
|
||||
//! * **native-tls** 📫: TLS support for the synchronous version of the API
|
||||
//! * **tokio02-native-tls**: TLS support for the `tokio02` async version of the API
|
||||
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
|
||||
//!
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
@@ -49,7 +48,6 @@
|
||||
//! Rustls uses [ring] as the cryptography implementation. As a result, [not all Rust's targets are supported][ring-support].
|
||||
//!
|
||||
//! * **rustls-tls**: TLS support for the synchronous version of the API
|
||||
//! * **tokio02-rustls-tls**: TLS support for the `tokio02` async version of the API
|
||||
//! * **tokio1-rustls-tls**: TLS support for the `tokio1` async version of the API
|
||||
//! * **async-std1-rustls-tls**: TLS support for the `async-std1` async version of the API
|
||||
//!
|
||||
@@ -71,11 +69,10 @@
|
||||
//! _Use [tokio] or [async-std] as an async execution runtime for sending emails_
|
||||
//!
|
||||
//! The correct runtime version must be chosen in order for lettre to work correctly.
|
||||
//! For example, when sending emails from a Tokio 1.3.0 context, the Tokio 1.x executor
|
||||
//! 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.
|
||||
//!
|
||||
//! * **tokio02**: Allow to asynchronously send emails using [Tokio 0.2.x]
|
||||
//! * **tokio1**: Allow to asynchronously send emails using [Tokio 1.x]
|
||||
//! * **async-std1**: Allow to asynchronously send emails using [async-std 1.x]
|
||||
//!
|
||||
@@ -87,19 +84,21 @@
|
||||
//!
|
||||
//! * **serde**: Serialization/Deserialization of entities
|
||||
//! * **tracing**: Logging using the `tracing` crate
|
||||
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
|
||||
//!
|
||||
//! [`SMTP`]: crate::transport::smtp
|
||||
//! [`sendmail`]: crate::transport::sendmail
|
||||
//! [`file`]: crate::transport::file
|
||||
//! [`ContentType`]: crate::message::header::ContentType
|
||||
//! [tokio]: https://docs.rs/tokio/1
|
||||
//! [async-std]: https://docs.rs/async-std/1
|
||||
//! [ring]: https://github.com/briansmith/ring#ring
|
||||
//! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
|
||||
//! [Tokio 0.2.x]: https://docs.rs/tokio/0.2
|
||||
//! [Tokio 1.x]: https://docs.rs/tokio/1
|
||||
//! [async-std 1.x]: https://docs.rs/async-std/1
|
||||
//! [mime 0.3]: https://docs.rs/mime/0.3
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-beta.4")]
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.4")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -113,9 +112,59 @@
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[cfg(not(lettre_ignore_tls_mismatch))]
|
||||
mod compiletime_checks {
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
feature = "native-tls",
|
||||
not(feature = "tokio1-native-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
|
||||
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
|
||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
feature = "rustls-tls",
|
||||
not(feature = "tokio1-rustls-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `rustls-tls` features, but the `tokio1-rustls-tls` feature hasn't been turned on.
|
||||
If you'd like to use native-tls make sure that the `rustls-tls` hasn't been enabled by mistake.
|
||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
||||
|
||||
/*
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
feature = "native-tls",
|
||||
not(feature = "async-std1-native-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the `async-std1-native-tls` feature hasn't been turned on.
|
||||
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
|
||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
||||
*/
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
feature = "native-tls",
|
||||
not(feature = "async-std1-native-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
|
||||
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
|
||||
If you'd like to use rustls make sure that the `native-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
|
||||
If you're building a library which depends on lettre import lettre without default features and enable just the features you need.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
feature = "rustls-tls",
|
||||
not(feature = "async-std1-rustls-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `rustls-tls` features, but the `async-std1-rustls-tls` feature hasn't been turned on.
|
||||
If you'd like to use native-tls make sure that the `rustls-tls` hasn't been enabled by mistake (you may need to import lettre without default features)
|
||||
If you're building a library which depends on lettre import it without default features and enable just the features you need.");
|
||||
}
|
||||
|
||||
pub mod address;
|
||||
pub mod error;
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod executor;
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
@@ -124,13 +173,11 @@ pub mod transport;
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub use self::executor::AsyncStd1Executor;
|
||||
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
|
||||
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
||||
pub use self::executor::Executor;
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub use self::executor::Tokio02Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::executor::Tokio1Executor;
|
||||
#[cfg(all(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))]
|
||||
#[cfg(all(any(feature = "tokio1", feature = "async-std1")))]
|
||||
#[doc(inline)]
|
||||
pub use self::transport::AsyncTransport;
|
||||
pub use crate::address::Address;
|
||||
@@ -139,7 +186,7 @@ pub use crate::address::Address;
|
||||
pub use crate::message::Message;
|
||||
#[cfg(all(
|
||||
feature = "file-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::file::AsyncFileTransport;
|
||||
@@ -148,7 +195,7 @@ pub use crate::transport::file::AsyncFileTransport;
|
||||
pub use crate::transport::file::FileTransport;
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::sendmail::AsyncSendmailTransport;
|
||||
@@ -157,7 +204,7 @@ pub use crate::transport::sendmail::AsyncSendmailTransport;
|
||||
pub use crate::transport::sendmail::SendmailTransport;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1", feature = "async-std1")
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
pub use crate::transport::smtp::AsyncSmtpTransport;
|
||||
#[doc(inline)]
|
||||
|
||||
145
src/message/attachment.rs
Normal file
145
src/message/attachment.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
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 {
|
||||
/// Create a new attachment
|
||||
///
|
||||
/// This attachment will be displayed as a normal attachment,
|
||||
/// with the chosen `filename` appearing as the file name.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::error::Error;
|
||||
/// use std::fs;
|
||||
///
|
||||
/// use lettre::message::{Attachment, header::ContentType};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let filename = String::from("invoice.pdf");
|
||||
/// # if false {
|
||||
/// let filebody = fs::read("invoice.pdf")?;
|
||||
/// # }
|
||||
/// # let filebody = fs::read("docs/lettre.png")?;
|
||||
/// let content_type = ContentType::parse("application/pdf").unwrap();
|
||||
/// let attachment = Attachment::new(filename).body(filebody, content_type);
|
||||
///
|
||||
/// // The document `attachment` will show up as a normal attachment.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(filename: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Attached(filename),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new inline attachment
|
||||
///
|
||||
/// This attachment should be displayed inline into the message
|
||||
/// body:
|
||||
///
|
||||
/// ```html
|
||||
/// <img src="cid:123">
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::error::Error;
|
||||
/// use std::fs;
|
||||
///
|
||||
/// use lettre::message::{Attachment, header::ContentType};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let content_id = String::from("123");
|
||||
/// # if false {
|
||||
/// let filebody = fs::read("image.jpg")?;
|
||||
/// # }
|
||||
/// # let filebody = fs::read("docs/lettre.png")?;
|
||||
/// let content_type = ContentType::parse("image/jpeg").unwrap();
|
||||
/// let attachment = Attachment::new_inline(content_id).body(filebody, content_type);
|
||||
///
|
||||
/// // The image `attachment` will display inline into the email.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new_inline(content_id: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Inline(content_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the attachment into a [`SinglePart`] which can then be used to build the rest of the email
|
||||
///
|
||||
/// Look at the [Complex MIME body example](crate::message#complex-mime-body)
|
||||
/// to see how [`SinglePart`] can be put into the email.
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -183,8 +183,8 @@ impl MaybeString {
|
||||
/// would result into an invalid encoded body.
|
||||
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool {
|
||||
match encoding {
|
||||
ContentTransferEncoding::SevenBit => is_7bit_encoded(&self),
|
||||
ContentTransferEncoding::EightBit => is_8bit_encoded(&self),
|
||||
ContentTransferEncoding::SevenBit => is_7bit_encoded(self),
|
||||
ContentTransferEncoding::EightBit => is_8bit_encoded(self),
|
||||
ContentTransferEncoding::Binary
|
||||
| ContentTransferEncoding::QuotedPrintable
|
||||
| ContentTransferEncoding::Base64 => true,
|
||||
@@ -342,7 +342,7 @@ 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);
|
||||
let indices = find_all_lf_char_indices(string);
|
||||
|
||||
for i in indices {
|
||||
// this relies on `indices` being in reverse order
|
||||
|
||||
@@ -11,7 +11,8 @@ use crate::BoxError;
|
||||
/// The `Message` builder takes care of choosing the most
|
||||
/// efficient encoding based on the chosen body, so in most
|
||||
/// use-caches this header shouldn't be set manually.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum ContentTransferEncoding {
|
||||
/// ASCII
|
||||
SevenBit,
|
||||
|
||||
@@ -11,8 +11,13 @@ use crate::BoxError;
|
||||
|
||||
/// `Content-Type` of the body
|
||||
///
|
||||
/// This struct can represent any valid [mime type], which can be parsed via
|
||||
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
|
||||
///
|
||||
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
///
|
||||
/// [mime type]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContentType(Mime);
|
||||
|
||||
impl ContentType {
|
||||
@@ -62,6 +67,14 @@ impl FromStr for ContentType {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mime03")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "mime03")))]
|
||||
impl From<Mime> for ContentType {
|
||||
fn from(mime: Mime) -> Self {
|
||||
Self::from_mime(mime)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error occurred while trying to [`ContentType::parse`].
|
||||
#[derive(Debug)]
|
||||
pub struct ContentTypeErr(mime::FromStrError);
|
||||
@@ -78,6 +91,60 @@ impl Display for ContentTypeErr {
|
||||
}
|
||||
}
|
||||
|
||||
// -- Serialization and Deserialization --
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde {
|
||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
||||
use serde::ser::{Serialize, Serializer};
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use super::ContentType;
|
||||
|
||||
impl Serialize for ContentType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_newtype_struct("ContentType", &format!("{}", &self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ContentType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ContentTypeVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ContentTypeVisitor {
|
||||
type Value = ContentType;
|
||||
|
||||
// The error message which states what the Visitor expects to
|
||||
// receive
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a ContentType string like `text/plain`")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, mime: &str) -> Result<ContentType, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match ContentType::parse(mime) {
|
||||
Ok(content_type) => Ok(content_type),
|
||||
Err(_) => Err(E::custom(format!(
|
||||
"Couldn't parse the following MIME-Type: {}",
|
||||
mime
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(ContentTypeVisitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ContentType;
|
||||
|
||||
@@ -26,6 +26,9 @@ mod mailbox;
|
||||
mod special;
|
||||
mod 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;
|
||||
|
||||
@@ -160,7 +163,7 @@ impl Display for Headers {
|
||||
for (name, value) in &self.headers {
|
||||
Display::fmt(name, f)?;
|
||||
f.write_str(": ")?;
|
||||
HeaderValueEncoder::encode(&name, &value, f)?;
|
||||
HeaderValueEncoder::encode(name, value, f)?;
|
||||
f.write_str("\r\n")?;
|
||||
}
|
||||
|
||||
@@ -237,7 +240,7 @@ impl HeaderName {
|
||||
|
||||
impl Display for HeaderName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self)
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,13 +411,14 @@ impl HeaderValueEncoder {
|
||||
let mut next_word = next_word;
|
||||
|
||||
while !next_word.is_empty() {
|
||||
if self.remaining_line_len() <= base64_len(1) {
|
||||
let mut len = available_len_to_max_encode_len(self.remaining_line_len())
|
||||
.min(next_word.len());
|
||||
|
||||
if len == 0 {
|
||||
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;
|
||||
@@ -797,4 +801,21 @@ mod tests {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_653() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjgggggggggfっ".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n",
|
||||
" =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ pub struct MimeVersion {
|
||||
minor: u8,
|
||||
}
|
||||
|
||||
/// MIME version 1.0
|
||||
///
|
||||
/// Should be used in all MIME messages.
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
|
||||
|
||||
impl MimeVersion {
|
||||
|
||||
@@ -70,7 +70,7 @@ impl Display for Mailbox {
|
||||
if let Some(ref name) = self.name {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
f.write_str(&name)?;
|
||||
f.write_str(name)?;
|
||||
f.write_str(" <")?;
|
||||
self.email.fmt(f)?;
|
||||
return f.write_char('>');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::io::Write;
|
||||
|
||||
use crate::message::{
|
||||
header::{ContentTransferEncoding, ContentType, Header, Headers},
|
||||
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
|
||||
EmailFormat, IntoBody,
|
||||
};
|
||||
use mime::Mime;
|
||||
@@ -9,7 +9,7 @@ use std::iter::repeat_with;
|
||||
|
||||
/// MIME part variants
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Part {
|
||||
pub(super) enum Part {
|
||||
/// Single part with content
|
||||
Single(SinglePart),
|
||||
|
||||
@@ -26,18 +26,6 @@ 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 {
|
||||
@@ -112,6 +100,20 @@ impl SinglePart {
|
||||
SinglePartBuilder::new()
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &Headers {
|
||||
@@ -260,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)
|
||||
@@ -286,7 +283,7 @@ impl Default for MultiPartBuilder {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPart {
|
||||
headers: Headers,
|
||||
parts: Parts,
|
||||
parts: Vec<Part>,
|
||||
}
|
||||
|
||||
impl MultiPart {
|
||||
@@ -330,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
|
||||
@@ -369,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();
|
||||
@@ -472,12 +460,12 @@ mod test {
|
||||
fn multi_part_mixed() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.part(Part::Single(
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")),
|
||||
))
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
@@ -511,11 +499,11 @@ mod test {
|
||||
fn multi_part_encrypted() {
|
||||
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.part(Part::Single(
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::parse("application/pgp-encrypted").unwrap())
|
||||
.body(String::from("Version: 1")),
|
||||
))
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(
|
||||
@@ -566,11 +554,11 @@ mod test {
|
||||
"pgp-sha256".to_owned(),
|
||||
)
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.part(Part::Single(
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Test email for signature")),
|
||||
))
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(
|
||||
@@ -624,10 +612,10 @@ mod test {
|
||||
fn multi_part_alternative() {
|
||||
let part = MultiPart::alternative()
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.part(Part::Single(SinglePart::builder()
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"))))
|
||||
.body(String::from("Текст письма в уникоде")))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
|
||||
@@ -4,15 +4,8 @@
|
||||
//!
|
||||
//! This section demonstrates how to build messages.
|
||||
//!
|
||||
//! <!--
|
||||
//! style for <details><summary>Blablabla</summary> Lots of stuff</details>
|
||||
//! borrowed from https://docs.rs/time/0.2.23/src/time/lib.rs.html#49-54
|
||||
//! -->
|
||||
//! <style>
|
||||
//! summary, details:not([open]) { cursor: pointer; }
|
||||
//! summary { display: list-item; }
|
||||
//! summary::marker { content: '▶ '; }
|
||||
//! details[open] summary::marker { content: '▼ '; }
|
||||
//! </style>
|
||||
//!
|
||||
//!
|
||||
@@ -63,7 +56,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 +64,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)
|
||||
//! .body(String::from("Hello, world! :)")),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType::TEXT_HTML)
|
||||
//! .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(())
|
||||
//! # }
|
||||
//! ```
|
||||
@@ -124,7 +106,7 @@
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! use lettre::message::{header, Body, Message, MultiPart, Part, SinglePart};
|
||||
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
|
||||
//! use std::fs;
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
@@ -144,35 +126,22 @@
|
||||
//! MultiPart::mixed()
|
||||
//! .multipart(
|
||||
//! MultiPart::alternative()
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType::TEXT_PLAIN)
|
||||
//! .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)
|
||||
//! .body(String::from(
|
||||
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
//! )),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType::parse("image/png")?)
|
||||
//! .header(header::ContentDisposition::inline())
|
||||
//! .header(header::ContentId::from(String::from("<123>")))
|
||||
//! .body(image_body),
|
||||
//! Attachment::new_inline(String::from("123"))
|
||||
//! .body(image_body, "image/png".parse().unwrap()),
|
||||
//! ),
|
||||
//! ),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType::TEXT_PLAIN)
|
||||
//! .header(header::ContentDisposition::attachment("example.rs"))
|
||||
//! .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(())
|
||||
//! # }
|
||||
@@ -228,10 +197,12 @@
|
||||
|
||||
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::*;
|
||||
|
||||
mod attachment;
|
||||
mod body;
|
||||
pub mod header;
|
||||
mod mailbox;
|
||||
@@ -293,7 +264,8 @@ impl MessageBuilder {
|
||||
|
||||
/// 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())
|
||||
}
|
||||
@@ -306,7 +278,7 @@ impl MessageBuilder {
|
||||
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)`.
|
||||
///
|
||||
@@ -422,7 +394,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
|
||||
@@ -445,6 +417,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,
|
||||
@@ -524,7 +500,7 @@ impl EmailFormat for Message {
|
||||
MessageBody::Mime(p) => p.format(out),
|
||||
MessageBody::Raw(r) => {
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend_from_slice(&r)
|
||||
out.extend_from_slice(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -580,6 +556,7 @@ mod test {
|
||||
|
||||
let email = Message::builder()
|
||||
.date(date)
|
||||
.bcc("hidden@example.com".parse().unwrap())
|
||||
.header(header::From(
|
||||
vec![Mailbox::new(
|
||||
Some("Каи".into()),
|
||||
|
||||
@@ -134,11 +134,11 @@
|
||||
|
||||
pub use self::error::Error;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use std::marker::PhantomData;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
@@ -163,11 +163,8 @@ pub struct FileTransport {
|
||||
/// Asynchronously writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
|
||||
)]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[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>,
|
||||
@@ -220,7 +217,7 @@ impl FileTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> AsyncFileTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
@@ -290,7 +287,7 @@ impl Transport for FileTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
#[async_trait]
|
||||
impl<E> AsyncTransport for AsyncFileTransport<E>
|
||||
where
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::Envelope;
|
||||
@@ -136,11 +136,8 @@ pub trait Transport {
|
||||
}
|
||||
|
||||
/// Async Transport method for emails
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
|
||||
)]
|
||||
#[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
|
||||
@@ -155,7 +152,7 @@ pub trait AsyncTransport {
|
||||
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
|
||||
self.send_raw(envelope, &raw).await
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
|
||||
@@ -26,29 +26,6 @@
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 0.2 example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "tokio02", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, Tokio02Executor, AsyncSendmailTransport, SendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 1.x example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
@@ -98,16 +75,14 @@
|
||||
pub use self::error::Error;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use std::marker::PhantomData;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
@@ -117,7 +92,7 @@ use std::{
|
||||
|
||||
mod error;
|
||||
|
||||
const DEFAULT_SENDMAIL: &str = "/usr/sbin/sendmail";
|
||||
const DEFAULT_SENDMAIL: &str = "sendmail";
|
||||
|
||||
/// Sends emails using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -130,18 +105,18 @@ pub struct SendmailTransport {
|
||||
/// Asynchronously sends emails using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
|
||||
)]
|
||||
#[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
|
||||
/// Creates a new transport with the `sendmail` command
|
||||
///
|
||||
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
|
||||
/// use [SendmailTransport::new_with_command].
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: DEFAULT_SENDMAIL.into(),
|
||||
@@ -170,12 +145,15 @@ impl SendmailTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[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
|
||||
/// Creates a new transport with the `sendmail` command
|
||||
///
|
||||
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
|
||||
/// use [AsyncSendmailTransport::new_with_command].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: SendmailTransport::new(),
|
||||
@@ -191,24 +169,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
|
||||
use tokio02_crate::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.inner.command);
|
||||
c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
|
||||
use tokio1_crate::process::Command;
|
||||
@@ -253,7 +213,7 @@ impl Default for SendmailTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> Default for AsyncSendmailTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
@@ -306,7 +266,7 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&email)
|
||||
.write_all(email)
|
||||
.await
|
||||
.map_err(error::client)?;
|
||||
let output = process.output().await.map_err(error::client)?;
|
||||
@@ -320,38 +280,6 @@ impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSendmailTransport<Tokio02Executor> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio02_crate::io::AsyncWriteExt;
|
||||
|
||||
let mut command = self.tokio02_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn().map_err(error::client)?;
|
||||
|
||||
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 {
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
|
||||
@@ -370,7 +298,7 @@ impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&email)
|
||||
.write_all(email)
|
||||
.await
|
||||
.map_err(error::client)?;
|
||||
let output = process.wait_with_output().await.map_err(error::client)?;
|
||||
|
||||
@@ -1,51 +1,36 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
marker::PhantomData,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
use super::pool::async_impl::Pool;
|
||||
#[cfg(feature = "pool")]
|
||||
use super::PoolConfig;
|
||||
use super::{
|
||||
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
|
||||
};
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{Envelope, Executor};
|
||||
|
||||
/// Asynchronously sends emails using the SMTP protocol
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
|
||||
)]
|
||||
pub struct AsyncSmtpTransport<E> {
|
||||
// TODO: pool
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransport<E: Executor> {
|
||||
#[cfg(feature = "pool")]
|
||||
inner: Arc<Pool<E>>,
|
||||
#[cfg(not(feature = "pool"))]
|
||||
inner: AsyncSmtpClient<E>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSmtpTransport<Tokio02Executor> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
|
||||
@@ -58,6 +43,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -93,8 +79,6 @@ 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",
|
||||
@@ -103,8 +87,6 @@ where
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
@@ -132,8 +114,6 @@ 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",
|
||||
@@ -142,8 +122,6 @@ where
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
@@ -172,21 +150,41 @@ where
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
||||
let new = SmtpInfo {
|
||||
let info = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
AsyncSmtpTransportBuilder { info: new }
|
||||
AsyncSmtpTransportBuilder {
|
||||
info,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests the SMTP connection
|
||||
///
|
||||
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
||||
/// The connection is closed afterwards if a connection pool is not used.
|
||||
pub async fn test_connection(&self) -> Result<bool, Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let is_connected = conn.test_connected().await;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(is_connected)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Debug for AsyncSmtpTransport<E> {
|
||||
impl<E: Executor> 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);
|
||||
@@ -208,12 +206,11 @@ where
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1")))
|
||||
)]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `AsyncSmtpTransport`
|
||||
@@ -242,10 +239,14 @@ impl AsyncSmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.info.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
@@ -254,8 +255,6 @@ impl AsyncSmtpTransportBuilder {
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
@@ -266,6 +265,16 @@ impl AsyncSmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a custom configuration for the connection pool
|
||||
///
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
#[cfg(feature = "pool")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
|
||||
self.pool_config = pool_config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport
|
||||
pub fn build<E>(self) -> AsyncSmtpTransport<E>
|
||||
where
|
||||
@@ -275,6 +284,10 @@ impl AsyncSmtpTransportBuilder {
|
||||
info: self.info,
|
||||
marker_: PhantomData,
|
||||
};
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
let client = Pool::new(self.pool_config, client);
|
||||
|
||||
AsyncSmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
@@ -296,13 +309,14 @@ where
|
||||
let mut conn = E::connect(
|
||||
&self.info.server,
|
||||
self.info.port,
|
||||
self.info.timeout,
|
||||
&self.info.hello_name,
|
||||
&self.info.tls,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, &credentials).await?;
|
||||
conn.auth(&self.info.authentication, credentials).await?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
@@ -316,6 +330,8 @@ impl<E> Debug for AsyncSmtpClient<E> {
|
||||
}
|
||||
}
|
||||
|
||||
// `clone` is unused when the `pool` feature is on
|
||||
#[allow(dead_code)]
|
||||
impl<E> AsyncSmtpClient<E>
|
||||
where
|
||||
E: Executor,
|
||||
|
||||
@@ -4,6 +4,7 @@ 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];
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
Envelope,
|
||||
};
|
||||
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use std::fmt::Display;
|
||||
use std::{fmt::Display, time::Duration};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
@@ -44,31 +44,17 @@ 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
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub async fn connect_tokio1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_tokio1(hostname, port, tls_parameters).await?;
|
||||
let stream = AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
@@ -76,13 +62,13 @@ impl AsyncSmtpConnection {
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub async fn connect_asyncstd1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_asyncstd1(hostname, port, tls_parameters).await?;
|
||||
let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
@@ -175,10 +161,7 @@ impl AsyncSmtpConnection {
|
||||
) -> Result<(), Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
try_smtp!(self.command(Starttls).await, self);
|
||||
try_smtp!(
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters).await,
|
||||
self
|
||||
);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters).await?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
@@ -312,7 +295,7 @@ impl AsyncSmtpConnection {
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(error::code(response.code))
|
||||
Err(error::code(response.code()))
|
||||
}
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
|
||||
@@ -1,43 +1,34 @@
|
||||
use std::{
|
||||
mem,
|
||||
io, mem,
|
||||
net::SocketAddr,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use futures_io::{
|
||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError, ErrorKind,
|
||||
Result as IoResult,
|
||||
};
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
use async_std::net::TcpStream as AsyncStd1TcpStream;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
|
||||
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::net::TcpStream as Tokio1TcpStream;
|
||||
use tokio1_crate::net::{TcpStream as Tokio1TcpStream, ToSocketAddrs as Tokio1ToSocketAddrs};
|
||||
|
||||
#[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;
|
||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||
#[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",
|
||||
@@ -58,15 +49,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),
|
||||
@@ -101,14 +83,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")]
|
||||
@@ -133,32 +107,48 @@ 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
|
||||
.map_err(error::connection)?;
|
||||
|
||||
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,
|
||||
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = Tokio1TcpStream::connect((hostname, port))
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
async fn try_connect_timeout<T: Tokio1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
) -> Result<Tokio1TcpStream, Error> {
|
||||
let addrs = tokio1_crate::net::lookup_host(server)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
let connect_future = Tokio1TcpStream::connect(&addr);
|
||||
match tokio1_crate::time::timeout(timeout, connect_future).await {
|
||||
Ok(Ok(stream)) => return Ok(stream),
|
||||
Ok(Err(err)) => last_err = Some(err),
|
||||
Err(_) => {
|
||||
last_err = Some(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"connection timed out",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(match last_err {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
Some(t) => try_connect_timeout(server, t).await?,
|
||||
None => Tokio1TcpStream::connect(server)
|
||||
.await
|
||||
.map_err(error::connection)?,
|
||||
};
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
@@ -168,14 +158,45 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub async fn connect_asyncstd1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port))
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
) -> Result<AsyncStd1TcpStream, Error> {
|
||||
let addrs = server.to_socket_addrs().await.map_err(error::connection)?;
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
let connect_future = AsyncStd1TcpStream::connect(&addr);
|
||||
match async_std::future::timeout(timeout, connect_future).await {
|
||||
Ok(Ok(stream)) => return Ok(stream),
|
||||
Ok(Err(err)) => last_err = Some(err),
|
||||
Err(_) => {
|
||||
last_err = Some(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"connection timed out",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(match last_err {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
Some(t) => try_connect_timeout(server, t).await?,
|
||||
None => AsyncStd1TcpStream::connect(server)
|
||||
.await
|
||||
.map_err(error::connection)?,
|
||||
};
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
@@ -186,29 +207,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 = 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
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))
|
||||
@@ -259,62 +257,13 @@ impl AsyncNetworkStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[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 = 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
|
||||
.map_err(error::connection)?;
|
||||
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).map_err(error::connection)?;
|
||||
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
.connect(domain, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
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,
|
||||
tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = mem::take(&mut tls_parameters.domain);
|
||||
let domain = tls_parameters.domain().to_string();
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
@@ -341,10 +290,13 @@ impl AsyncNetworkStream {
|
||||
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
return {
|
||||
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let domain =
|
||||
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
|
||||
use rustls::ServerName;
|
||||
use tokio1_rustls::TlsConnector;
|
||||
|
||||
let domain = ServerName::try_from(domain.as_str())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
@@ -393,10 +345,13 @@ impl AsyncNetworkStream {
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
return {
|
||||
use async_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let domain =
|
||||
DNSNameRef::try_from_ascii_str(&domain).map_err(error::connection)?;
|
||||
use futures_rustls::TlsConnector;
|
||||
use rustls::ServerName;
|
||||
|
||||
let domain = ServerName::try_from(domain.as_str())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
|
||||
let connector = TlsConnector::from(config);
|
||||
let stream = connector
|
||||
@@ -411,12 +366,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")]
|
||||
@@ -441,12 +390,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);
|
||||
@@ -499,12 +442,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")]
|
||||
@@ -530,12 +467,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")]
|
||||
@@ -557,12 +488,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")]
|
||||
|
||||
@@ -145,7 +145,7 @@ impl SmtpConnection {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
{
|
||||
try_smtp!(self.command(Starttls), self);
|
||||
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
@@ -276,7 +276,7 @@ impl SmtpConnection {
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(error::code(response.code))
|
||||
Err(error::code(response.code()))
|
||||
};
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
#[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"))]
|
||||
@@ -39,9 +39,9 @@ pub use self::{
|
||||
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 net;
|
||||
@@ -78,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 {
|
||||
@@ -111,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);
|
||||
@@ -121,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,3 +1,5 @@
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::convert::TryFrom;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
mem,
|
||||
@@ -9,7 +11,7 @@ use std::{
|
||||
use native_tls::TlsStream;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::{ClientSession, StreamOwned};
|
||||
use rustls::{ClientConnection, ServerName, StreamOwned};
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::InnerTlsParameters;
|
||||
@@ -33,7 +35,7 @@ enum InnerNetworkStream {
|
||||
NativeTls(TlsStream<TcpStream>),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(StreamOwned<ClientSession, TcpStream>),
|
||||
RustlsTls(StreamOwned<ClientConnection, TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
@@ -90,12 +92,20 @@ impl NetworkStream {
|
||||
timeout: Duration,
|
||||
) -> Result<TcpStream, Error> {
|
||||
let addrs = server.to_socket_addrs().map_err(error::connection)?;
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
|
||||
return Ok(result);
|
||||
match TcpStream::connect_timeout(&addr, timeout) {
|
||||
Ok(stream) => return Ok(stream),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
Err(error::connection("Could not connect"))
|
||||
|
||||
Err(match last_err {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
@@ -149,12 +159,11 @@ impl NetworkStream {
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(connector) => {
|
||||
use webpki::DNSNameRef;
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())
|
||||
.map_err(error::connection)?;
|
||||
let stream = StreamOwned::new(ClientSession::new(&connector, domain), tcp_stream);
|
||||
|
||||
let domain = ServerName::try_from(tls_parameters.domain())
|
||||
.map_err(|_| error::connection("domain isn't a valid DNS name"))?;
|
||||
let connection =
|
||||
ClientConnection::new(connector.clone(), domain).map_err(error::connection)?;
|
||||
let stream = StreamOwned::new(connection, tcp_stream);
|
||||
InnerNetworkStream::RustlsTls(stream)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,12 +3,13 @@ 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 rustls::{
|
||||
client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier},
|
||||
ClientConfig, Error as TlsError, OwnedTrustAnchor, RootCertStore, ServerName,
|
||||
};
|
||||
use std::fmt::{self, Debug};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use webpki::DNSNameRef;
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
@@ -163,21 +164,35 @@ impl TlsParametersBuilder {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||
use webpki_roots::TLS_SERVER_ROOTS;
|
||||
let tls = ClientConfig::builder();
|
||||
let tls = tls.with_safe_defaults();
|
||||
|
||||
let mut tls = ClientConfig::new();
|
||||
|
||||
for cert in self.root_certs {
|
||||
for rustls_cert in cert.rustls {
|
||||
tls.root_store.add(&rustls_cert).map_err(error::tls)?;
|
||||
let tls = if self.accept_invalid_certs {
|
||||
tls.with_custom_certificate_verifier(Arc::new(InvalidCertsVerifier {}))
|
||||
} else {
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
for cert in self.root_certs {
|
||||
for rustls_cert in cert.rustls {
|
||||
root_cert_store.add(&rustls_cert).map_err(error::tls)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.accept_invalid_certs {
|
||||
tls.dangerous()
|
||||
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {}));
|
||||
}
|
||||
root_cert_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
|
||||
|ta| {
|
||||
OwnedTrustAnchor::from_subject_spki_name_constraints(
|
||||
ta.subject,
|
||||
ta.spki,
|
||||
ta.name_constraints,
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
tls.with_custom_certificate_verifier(Arc::new(WebPkiVerifier::new(
|
||||
root_cert_store,
|
||||
None,
|
||||
)))
|
||||
};
|
||||
let tls = tls.with_no_client_auth();
|
||||
|
||||
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::RustlsTls(Arc::new(tls)),
|
||||
domain: self.domain,
|
||||
@@ -257,11 +272,14 @@ impl Certificate {
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let rustls_cert = {
|
||||
use rustls::internal::pemfile;
|
||||
use std::io::Cursor;
|
||||
|
||||
let mut pem = Cursor::new(pem);
|
||||
pemfile::certs(&mut pem).map_err(|_| error::tls("invalid certificates"))?
|
||||
rustls_pemfile::certs(&mut pem)
|
||||
.map_err(|_| error::tls("invalid certificates"))?
|
||||
.into_iter()
|
||||
.map(rustls::Certificate)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
@@ -286,11 +304,13 @@ struct InvalidCertsVerifier;
|
||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_roots: &RootCertStore,
|
||||
_presented_certs: &[rustls::Certificate],
|
||||
_dns_name: DNSNameRef<'_>,
|
||||
_end_entity: &rustls::Certificate,
|
||||
_intermediates: &[rustls::Certificate],
|
||||
_server_name: &ServerName,
|
||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||
_ocsp_response: &[u8],
|
||||
) -> Result<ServerCertVerified, TLSError> {
|
||||
_now: SystemTime,
|
||||
) -> Result<ServerCertVerified, TlsError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ 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
|
||||
///
|
||||
@@ -107,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 {
|
||||
@@ -135,7 +136,7 @@ impl ServerInfo {
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
|
||||
for line in response.message.as_slice() {
|
||||
for line in response.message() {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -197,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
|
||||
|
||||
@@ -116,12 +116,10 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg(feature = "pool")]
|
||||
pub use self::pool::PoolConfig;
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub(crate) use self::transport::SmtpClient;
|
||||
pub use self::{
|
||||
error::Error,
|
||||
transport::{SmtpTransport, SmtpTransportBuilder},
|
||||
@@ -137,18 +135,18 @@ 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;
|
||||
pub mod commands;
|
||||
mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg(feature = "pool")]
|
||||
mod pool;
|
||||
pub mod response;
|
||||
mod transport;
|
||||
pub mod util;
|
||||
pub(super) mod util;
|
||||
|
||||
// Registered port numbers:
|
||||
// https://www.iana.
|
||||
@@ -164,7 +162,7 @@ pub const SUBMISSION_PORT: u16 = 587;
|
||||
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);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SmtpInfo {
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::transport::smtp::{client::SmtpConnection, error, error::Error, SmtpClient};
|
||||
|
||||
use r2d2::{CustomizeConnection, ManageConnection, Pool};
|
||||
|
||||
/// Configuration for a connection pool
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
pub struct PoolConfig {
|
||||
min_idle: u32,
|
||||
max_size: u32,
|
||||
connection_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl PoolConfig {
|
||||
/// Create a new pool configuration with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Minimum number of idle connections
|
||||
///
|
||||
/// Defaults to `0`
|
||||
pub fn min_idle(mut self, min_idle: u32) -> Self {
|
||||
self.min_idle = min_idle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of pooled connections
|
||||
///
|
||||
/// Defaults to `10`
|
||||
pub fn max_size(mut self, max_size: u32) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection timeout
|
||||
///
|
||||
/// Defaults to `30 seconds`
|
||||
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
|
||||
self.connection_timeout = connection_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection idle timeout
|
||||
///
|
||||
/// Defaults to `60 seconds`
|
||||
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
|
||||
self.idle_timeout = idle_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build<C: ManageConnection<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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_idle: 0,
|
||||
max_size: 10,
|
||||
connection_timeout: Duration::from_secs(30),
|
||||
idle_timeout: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageConnection for SmtpClient {
|
||||
type Connection = SmtpConnection;
|
||||
type Error = Error;
|
||||
|
||||
fn connect(&self) -> Result<Self::Connection, Error> {
|
||||
self.connection()
|
||||
}
|
||||
|
||||
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
||||
if conn.test_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(error::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();
|
||||
}
|
||||
}
|
||||
}
|
||||
299
src/transport/smtp/pool/async_impl.rs
Normal file
299
src/transport/smtp/pool/async_impl.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use std::fmt::{self, Debug};
|
||||
use std::mem;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use futures_util::lock::Mutex;
|
||||
use futures_util::stream::{self, StreamExt};
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
use crate::executor::SpawnHandle;
|
||||
use crate::transport::smtp::async_transport::AsyncSmtpClient;
|
||||
use crate::Executor;
|
||||
|
||||
use super::super::client::AsyncSmtpConnection;
|
||||
use super::super::Error;
|
||||
use super::PoolConfig;
|
||||
|
||||
pub struct Pool<E: Executor> {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Vec<ParkedConnection>>,
|
||||
client: AsyncSmtpClient<E>,
|
||||
handle: OnceCell<E::Handle>,
|
||||
}
|
||||
|
||||
struct ParkedConnection {
|
||||
conn: AsyncSmtpConnection,
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
pub struct PooledConnection<E: Executor> {
|
||||
conn: Option<AsyncSmtpConnection>,
|
||||
pool: Arc<Pool<E>>,
|
||||
}
|
||||
|
||||
impl<E: Executor> Pool<E> {
|
||||
pub fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
|
||||
let pool = Arc::new(Self {
|
||||
config,
|
||||
connections: Mutex::new(Vec::new()),
|
||||
client,
|
||||
handle: OnceCell::new(),
|
||||
});
|
||||
|
||||
{
|
||||
let pool_ = Arc::clone(&pool);
|
||||
|
||||
let min_idle = pool_.config.min_idle;
|
||||
let idle_timeout = pool_.config.idle_timeout;
|
||||
let pool = Arc::downgrade(&pool_);
|
||||
|
||||
let handle = E::spawn(async move {
|
||||
loop {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("running cleanup tasks");
|
||||
|
||||
match pool.upgrade() {
|
||||
Some(pool) => {
|
||||
#[allow(clippy::needless_collect)]
|
||||
let (count, dropped) = {
|
||||
let mut connections = pool.connections.lock().await;
|
||||
|
||||
let to_drop = connections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
let dropped = to_drop
|
||||
.into_iter()
|
||||
.map(|i| connections.remove(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(connections.len(), dropped)
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mut created = 0;
|
||||
for _ in count..=(min_idle as usize) {
|
||||
let conn = match pool.client.connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("couldn't create idle connection {}", err);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let _ = err;
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut connections = pool.connections.lock().await;
|
||||
connections.push(ParkedConnection::park(conn));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
if created > 0 {
|
||||
tracing::debug!("created {} idle connections", created);
|
||||
}
|
||||
|
||||
if !dropped.is_empty() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropped {} idle connections", dropped.len());
|
||||
|
||||
abort_concurrent(dropped.into_iter().map(|conn| conn.unpark()))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!(
|
||||
"breaking out of task - no more references to Pool are available"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
E::sleep(idle_timeout).await;
|
||||
}
|
||||
});
|
||||
pool_
|
||||
.handle
|
||||
.set(handle)
|
||||
.expect("handle hasn't been set yet");
|
||||
}
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
|
||||
loop {
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock().await;
|
||||
connections.pop()
|
||||
};
|
||||
|
||||
match conn {
|
||||
Some(conn) => {
|
||||
let mut conn = conn.unpark();
|
||||
|
||||
// TODO: handle the client try another connection if this one isn't good
|
||||
if !conn.test_connected().await {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection");
|
||||
|
||||
conn.abort().await;
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("reusing a pooled connection");
|
||||
|
||||
return Ok(PooledConnection::wrap(conn, self.clone()));
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("creating a new connection");
|
||||
|
||||
let conn = self.client.connection().await?;
|
||||
return Ok(PooledConnection::wrap(conn, self.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recycle(&self, mut conn: AsyncSmtpConnection) {
|
||||
if conn.has_broken() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection instead of recycling it");
|
||||
|
||||
conn.abort().await;
|
||||
drop(conn);
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("recycling connection");
|
||||
|
||||
let mut connections = self.connections.lock().await;
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections);
|
||||
conn.abort().await;
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Debug for Pool<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Pool")
|
||||
.field("config", &self.config)
|
||||
.field(
|
||||
"connections",
|
||||
&match self.connections.try_lock() {
|
||||
Some(connections) => format!("{} connections", connections.len()),
|
||||
|
||||
None => "LOCKED".to_string(),
|
||||
},
|
||||
)
|
||||
.field("client", &self.client)
|
||||
.field(
|
||||
"handle",
|
||||
&match self.handle.get() {
|
||||
Some(_) => "Some(JoinHandle)",
|
||||
None => "None",
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Drop for Pool<E> {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping Pool");
|
||||
|
||||
let connections = mem::take(self.connections.get_mut());
|
||||
let handle = self.handle.take();
|
||||
E::spawn(async move {
|
||||
if let Some(handle) = handle {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
|
||||
abort_concurrent(connections.into_iter().map(|conn| conn.unpark())).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ParkedConnection {
|
||||
fn park(conn: AsyncSmtpConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
since: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_duration(&self) -> Duration {
|
||||
self.since.elapsed()
|
||||
}
|
||||
|
||||
fn unpark(self) -> AsyncSmtpConnection {
|
||||
self.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> PooledConnection<E> {
|
||||
fn wrap(conn: AsyncSmtpConnection, pool: Arc<Pool<E>>) -> Self {
|
||||
Self {
|
||||
conn: Some(conn),
|
||||
pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Deref for PooledConnection<E> {
|
||||
type Target = AsyncSmtpConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.conn.as_ref().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> DerefMut for PooledConnection<E> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.conn.as_mut().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Drop for PooledConnection<E> {
|
||||
fn drop(&mut self) {
|
||||
let conn = self
|
||||
.conn
|
||||
.take()
|
||||
.expect("AsyncSmtpConnection hasn't been taken yet");
|
||||
let pool = Arc::clone(&self.pool);
|
||||
|
||||
E::spawn(async move {
|
||||
pool.recycle(conn).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn abort_concurrent<I>(iter: I)
|
||||
where
|
||||
I: Iterator<Item = AsyncSmtpConnection>,
|
||||
{
|
||||
stream::iter(iter)
|
||||
.for_each_concurrent(8, |mut conn| async move {
|
||||
conn.abort().await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
66
src/transport/smtp/pool/mod.rs
Normal file
66
src/transport/smtp/pool/mod.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub mod async_impl;
|
||||
pub mod sync_impl;
|
||||
|
||||
/// Configuration for a connection pool
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub struct PoolConfig {
|
||||
min_idle: u32,
|
||||
max_size: u32,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl PoolConfig {
|
||||
/// Create a new pool configuration with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Minimum number of idle connections
|
||||
///
|
||||
/// Defaults to `0`
|
||||
pub fn min_idle(mut self, min_idle: u32) -> Self {
|
||||
self.min_idle = min_idle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of pooled connections
|
||||
///
|
||||
/// Defaults to `10`
|
||||
pub fn max_size(mut self, max_size: u32) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection timeout
|
||||
///
|
||||
/// Defaults to `30 seconds`
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "The Connection timeout is already configured on the SMTP transport")]
|
||||
pub fn connection_timeout(self, connection_timeout: Duration) -> Self {
|
||||
let _ = connection_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection idle timeout
|
||||
///
|
||||
/// Defaults to `60 seconds`
|
||||
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
|
||||
self.idle_timeout = idle_timeout;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_idle: 0,
|
||||
max_size: 10,
|
||||
idle_timeout: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
256
src/transport/smtp/pool/sync_impl.rs
Normal file
256
src/transport/smtp/pool/sync_impl.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
use std::fmt::{self, Debug};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::{Arc, Mutex, TryLockError};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{mem, thread};
|
||||
|
||||
use crate::transport::smtp::transport::SmtpClient;
|
||||
|
||||
use super::super::client::SmtpConnection;
|
||||
use super::super::Error;
|
||||
use super::PoolConfig;
|
||||
|
||||
pub struct Pool {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Vec<ParkedConnection>>,
|
||||
client: SmtpClient,
|
||||
}
|
||||
|
||||
struct ParkedConnection {
|
||||
conn: SmtpConnection,
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
pub struct PooledConnection {
|
||||
conn: Option<SmtpConnection>,
|
||||
pool: Arc<Pool>,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
pub fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
|
||||
let pool = Arc::new(Self {
|
||||
config,
|
||||
connections: Mutex::new(Vec::new()),
|
||||
client,
|
||||
});
|
||||
|
||||
{
|
||||
let pool_ = Arc::clone(&pool);
|
||||
|
||||
let min_idle = pool_.config.min_idle;
|
||||
let idle_timeout = pool_.config.idle_timeout;
|
||||
let pool = Arc::downgrade(&pool_);
|
||||
|
||||
thread::Builder::new()
|
||||
.name("lettre-connection-pool".into())
|
||||
.spawn(move || {
|
||||
while let Some(pool) = pool.upgrade() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("running cleanup tasks");
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
let (count, dropped) = {
|
||||
let mut connections = pool.connections.lock().unwrap();
|
||||
|
||||
let to_drop = connections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
let dropped = to_drop
|
||||
.into_iter()
|
||||
.map(|i| connections.remove(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(connections.len(), dropped)
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mut created = 0;
|
||||
for _ in count..=(min_idle as usize) {
|
||||
let conn = match pool.client.connection() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("couldn't create idle connection {}", err);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let _ = err;
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut connections = pool.connections.lock().unwrap();
|
||||
connections.push(ParkedConnection::park(conn));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
if created > 0 {
|
||||
tracing::debug!("created {} idle connections", created);
|
||||
}
|
||||
|
||||
if !dropped.is_empty() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropped {} idle connections", dropped.len());
|
||||
|
||||
for conn in dropped {
|
||||
let mut conn = conn.unpark();
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(idle_timeout);
|
||||
}
|
||||
})
|
||||
.expect("couldn't spawn the Pool thread");
|
||||
}
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
|
||||
loop {
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock().unwrap();
|
||||
connections.pop()
|
||||
};
|
||||
|
||||
match conn {
|
||||
Some(conn) => {
|
||||
let mut conn = conn.unpark();
|
||||
|
||||
// TODO: handle the client try another connection if this one isn't good
|
||||
if !conn.test_connected() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection");
|
||||
|
||||
conn.abort();
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("reusing a pooled connection");
|
||||
|
||||
return Ok(PooledConnection::wrap(conn, self.clone()));
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("creating a new connection");
|
||||
|
||||
let conn = self.client.connection()?;
|
||||
return Ok(PooledConnection::wrap(conn, self.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recycle(&self, mut conn: SmtpConnection) {
|
||||
if conn.has_broken() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection instead of recycling it");
|
||||
|
||||
conn.abort();
|
||||
drop(conn);
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("recycling connection");
|
||||
|
||||
let mut connections = self.connections.lock().unwrap();
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections);
|
||||
conn.abort();
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Pool {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Pool")
|
||||
.field("config", &self.config)
|
||||
.field(
|
||||
"connections",
|
||||
&match self.connections.try_lock() {
|
||||
Ok(connections) => format!("{} connections", connections.len()),
|
||||
|
||||
Err(TryLockError::WouldBlock) => "LOCKED".to_string(),
|
||||
Err(TryLockError::Poisoned(_)) => "POISONED".to_string(),
|
||||
},
|
||||
)
|
||||
.field("client", &self.client)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pool {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping Pool");
|
||||
|
||||
let connections = mem::take(&mut *self.connections.get_mut().unwrap());
|
||||
for conn in connections {
|
||||
let mut conn = conn.unpark();
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParkedConnection {
|
||||
fn park(conn: SmtpConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
since: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_duration(&self) -> Duration {
|
||||
self.since.elapsed()
|
||||
}
|
||||
|
||||
fn unpark(self) -> SmtpConnection {
|
||||
self.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl PooledConnection {
|
||||
fn wrap(conn: SmtpConnection, pool: Arc<Pool>) -> Self {
|
||||
Self {
|
||||
conn: Some(conn),
|
||||
pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PooledConnection {
|
||||
type Target = SmtpConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.conn.as_ref().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for PooledConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.conn.as_mut().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PooledConnection {
|
||||
fn drop(&mut self) {
|
||||
let conn = self
|
||||
.conn
|
||||
.take()
|
||||
.expect("SmtpConnection hasn't been taken yet");
|
||||
self.pool.recycle(conn);
|
||||
}
|
||||
}
|
||||
@@ -137,10 +137,10 @@ 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 {
|
||||
@@ -180,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)
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
#[cfg(feature = "pool")]
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
use r2d2::Pool;
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg(feature = "pool")]
|
||||
use super::pool::sync_impl::Pool;
|
||||
#[cfg(feature = "pool")]
|
||||
use super::PoolConfig;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::{error, Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use crate::{address::Envelope, Transport};
|
||||
|
||||
/// Sends emails using the SMTP protocol
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: Pool<SmtpClient>,
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
#[cfg(feature = "pool")]
|
||||
inner: Arc<Pool>,
|
||||
#[cfg(not(feature = "pool"))]
|
||||
inner: SmtpClient,
|
||||
}
|
||||
|
||||
@@ -26,14 +27,11 @@ 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().map_err(error::client)?;
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
let result = conn.send(envelope, email)?;
|
||||
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit()?;
|
||||
|
||||
Ok(result)
|
||||
@@ -105,10 +103,25 @@ impl SmtpTransport {
|
||||
|
||||
SmtpTransportBuilder {
|
||||
info: new,
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests the SMTP connection
|
||||
///
|
||||
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
||||
/// The connection is closed afterwards if a connection pool is not used.
|
||||
pub fn test_connection(&self) -> Result<bool, Error> {
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
let is_connected = conn.test_connected();
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit()?;
|
||||
|
||||
Ok(is_connected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration.
|
||||
@@ -116,7 +129,7 @@ impl SmtpTransport {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
@@ -163,8 +176,8 @@ impl SmtpTransportBuilder {
|
||||
/// Use a custom configuration for the connection pool
|
||||
///
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
#[cfg(feature = "pool")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
|
||||
self.pool_config = pool_config;
|
||||
self
|
||||
@@ -172,16 +185,15 @@ impl SmtpTransportBuilder {
|
||||
|
||||
/// Build the transport
|
||||
///
|
||||
/// If the `r2d2` feature is enabled an `Arc` wrapped pool is be created.
|
||||
/// If the `pool` feature is enabled an `Arc` wrapped pool is be created.
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
let client = SmtpClient { info: self.info };
|
||||
SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: self.pool_config.build(client),
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
inner: client,
|
||||
}
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
let client = Pool::new(self.pool_config, client);
|
||||
|
||||
SmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +237,7 @@ impl SmtpClient {
|
||||
}
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, &credentials)?;
|
||||
conn.auth(&self.info.authentication, credentials)?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use async_trait::async_trait;
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
@@ -80,7 +80,7 @@ impl Transport for StubTransport {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for StubTransport {
|
||||
type Ok = ();
|
||||
|
||||
@@ -101,53 +101,6 @@ mod sync {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio02"))]
|
||||
mod tokio_02 {
|
||||
use crate::default_date;
|
||||
use lettre::{AsyncFileTransport, AsyncTransport, Message, Tokio02Executor};
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{read_to_string, remove_file},
|
||||
};
|
||||
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn file_transport_tokio02() {
|
||||
let sender = AsyncFileTransport::<Tokio02Executor>::new(temp_dir());
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date(default_date())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let id = result.unwrap();
|
||||
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 -0000\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "file-transport", feature = "builder", feature = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
@@ -160,7 +113,6 @@ mod tokio_1 {
|
||||
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn file_transport_tokio1() {
|
||||
let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||
|
||||
@@ -20,33 +20,6 @@ mod sync {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
feature = "builder",
|
||||
feature = "tokio02"
|
||||
))]
|
||||
mod tokio_02 {
|
||||
use lettre::{AsyncSendmailTransport, AsyncTransport, Message, Tokio02Executor};
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sendmail_transport_tokio02() {
|
||||
let sender = AsyncSendmailTransport::<Tokio02Executor>::new();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.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",
|
||||
|
||||
@@ -20,31 +20,6 @@ mod sync {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio02"))]
|
||||
mod tokio_02 {
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio02Executor};
|
||||
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn smtp_transport_simple_tokio02() {
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let sender: AsyncSmtpTransport<Tokio02Executor> =
|
||||
AsyncSmtpTransport::<Tokio02Executor>::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
sender.send(email).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "builder", feature = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "pool"))]
|
||||
mod sync {
|
||||
use lettre::{address::Envelope, SmtpTransport, Transport};
|
||||
use std::{sync::mpsc, thread};
|
||||
|
||||
@@ -20,30 +20,6 @@ mod sync {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "builder", feature = "tokio02"))]
|
||||
mod tokio_02 {
|
||||
use lettre::{transport::stub::StubTransport, AsyncTransport, Message};
|
||||
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn stub_transport_tokio02() {
|
||||
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 = "tokio1"))]
|
||||
mod tokio_1 {
|
||||
|
||||
Reference in New Issue
Block a user