Compare commits

..

26 Commits

Author SHA1 Message Date
Paolo Barbolini
94cc0149d1 Prepare 0.10.0-rc.4 (#691) 2021-10-29 09:22:53 +02:00
Filip Gospodinov
a89383cdb6 Re-enable pool tests (#684)
The pool tests have been implicitly disabled by testing
for a feature that has been removed.
2021-10-20 19:31:56 +02:00
Filip Gospodinov
592593f4b8 Expose test_connected via transport (#677)
It is useful for application developers to validate SMTP
settings by testing the connection.

Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-10-20 16:42:49 +02:00
Paolo Barbolini
97d3c760c0 Update rustls to 0.20 (#648) 2021-10-20 14:37:16 +02:00
Alexis Mousset
8f28b0c341 fix(transport): Use the sendmail command in PATH by default (#682)
This will allow the transport to work with default settings on more systems,
while preserving the ability to use a specific binary.
2021-10-16 09:49:45 +02:00
Paolo Barbolini
dc9c5df210 Sync pool impl (#644)
Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-10-16 09:39:06 +02:00
Gaëtan Duchaussois
c9b3fa0baa docs(all): Add instruction to launch fake smtp server (#681) 2021-10-11 16:41:03 +02:00
Paolo Barbolini
addf8754dd smtp: don't send QUIT to connections that failed the STARTTLS handshake (#679) 2021-10-06 17:25:50 +00:00
Jacob Mischka
af157c5f26 Add From<Mime> under mime03 flag (#676)
Closes #615
2021-10-05 11:22:25 +02:00
TornaxO7
3e8988ae55 ContentTransferEncoding derives changes (#652) 2021-09-29 06:42:54 +02:00
Paolo Barbolini
941a00bcaa Bump MSRV to 1.52.1 (#671) 2021-09-12 18:40:26 +00:00
Paolo Barbolini
14079bff8c Give a compiletime error when using an incorrect combination of TLS features (#666) 2021-09-07 20:43:07 +02:00
Paolo Barbolini
696c06e8d7 Bump nom to v7 (#663) 2021-08-22 14:54:09 +02:00
Paolo Barbolini
d4f7618898 Better document how Attachments can be used (#658) 2021-08-20 08:17:36 +00:00
Christopher Vittal
e0a0a2e624 feat(address): Add TryFrom<String> for Address (#660)
Refactor the validation part of from_str into its own function as the
behavior for try_from is identical.
2021-08-20 09:09:49 +02:00
Paolo Barbolini
9ab6bb56d3 Fix broken <summary> style (#659) 2021-08-20 04:48:30 +02:00
Paolo Barbolini
e1d3778329 Revert "Allow a Message to be decomposed into a MessageBuilder (#633)" (#649)
This reverts commit aadcc0f83c.

Co-authored-by: Alexis Mousset <contact@amousset.me>
2021-08-02 12:20:02 +00:00
Paolo Barbolini
623d69c553 Fix #653 (#654) 2021-08-02 09:42:56 +00:00
Paolo Barbolini
55c2618201 Fix latest clippy warnings (#655) 2021-08-02 09:26:34 +00:00
Paolo Barbolini
9f550bce86 Bump MSRV to 1.49 (#656) 2021-08-02 09:26:25 +00:00
TornaxO7
e875d9ff64 ContentType Documentation (#642)
Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2021-07-06 11:46:42 +02:00
Alex Feldman-Crough
aadcc0f83c Allow a Message to be decomposed into a MessageBuilder (#633) 2021-06-30 18:21:55 +00:00
Paolo Barbolini
b534a18017 Async pool implementation (#637) 2021-06-29 15:23:47 +00:00
TornaxO7
0684bccd47 Implement Serialize and Deserialize for ContentType (#643) 2021-06-29 10:58:39 +02:00
Paolo Barbolini
4471759221 Implement connection timeouts for AsyncSmtpTransport (#635) 2021-06-17 22:39:48 +00:00
Paolo Barbolini
ed454819ee Refactor pool module (#636)
* Move pool to it's own module

* pool: deprecate configuring the connection timeout
2021-06-13 14:36:47 +02:00
29 changed files with 1277 additions and 285 deletions

View File

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

View File

@@ -5,7 +5,7 @@
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
* MSRV is now 1.46
* 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,
@@ -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

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre"
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
version = "0.10.0-rc.3"
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,12 +53,12 @@ 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
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"
@@ -76,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"]
@@ -85,19 +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"]
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"

View File

@@ -28,8 +28,8 @@
</div>
<div align="center">
<a href="https://deps.rs/crate/lettre/0.10.0-rc.3">
<img src="https://deps.rs/crate/lettre/0.10.0-rc.3/status.svg"
<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>
@@ -65,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-rc.3"
lettre = "0.10.0-rc.4"
```
```rust,no_run
@@ -103,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`.

View File

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

View File

@@ -1,10 +1,16 @@
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",
@@ -37,12 +43,29 @@ use crate::transport::smtp::Error;
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
#[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>;
@@ -56,6 +79,13 @@ pub trait Executor: Debug + Send + Sync + private::Sealed {
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
}
#[doc(hidden)]
#[cfg(feature = "smtp-transport")]
#[async_trait]
pub trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
async fn shutdown(self);
}
/// Async [`Executor`] using `tokio` `1.x`
///
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
@@ -74,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> {
@@ -89,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 {
@@ -121,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`]
@@ -139,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> {
@@ -154,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 {
@@ -187,6 +279,14 @@ 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::*;
@@ -197,4 +297,10 @@ mod private {
#[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<()> {}
}

View File

@@ -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
@@ -84,18 +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 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-rc.3")]
#![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)]
@@ -109,6 +112,56 @@
)]
#![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 = "tokio1", feature = "async-std1"))]

View File

@@ -20,21 +20,75 @@ enum Disposition {
}
impl Attachment {
/// Creates a new 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),
}
}
/// Creates a new inline attachment
/// 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 part
/// 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 {

View File

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

View File

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

View File

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

View File

@@ -163,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")?;
}
@@ -240,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)
}
}
@@ -411,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;
@@ -800,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;;;;;;;;;;;;;;;;fffeinmjggggggggg".to_string(),
);
assert_eq!(
headers.to_string(),
concat!(
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n",
" =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n"
)
);
}
}

View File

@@ -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('>');

View File

@@ -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>
//!
//!
@@ -507,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)
}
}
}

View File

@@ -152,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>;

View File

@@ -92,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)]
@@ -113,7 +113,10 @@ pub struct AsyncSendmailTransport<E: Executor> {
}
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(),
@@ -147,7 +150,10 @@ 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(),
@@ -260,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)?;
@@ -292,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)?;

View File

@@ -1,10 +1,16 @@
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,
};
@@ -18,8 +24,10 @@ use crate::{Envelope, Executor};
/// Asynchronously sends emails using the SMTP protocol
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
pub struct AsyncSmtpTransport<E> {
// TODO: pool
pub struct AsyncSmtpTransport<E: Executor> {
#[cfg(feature = "pool")]
inner: Arc<Pool<E>>,
#[cfg(not(feature = "pool"))]
inner: AsyncSmtpClient<E>,
}
@@ -35,6 +43,7 @@ impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
let result = conn.send(envelope, email).await?;
#[cfg(not(feature = "pool"))]
conn.quit().await?;
Ok(result)
@@ -141,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);
@@ -180,6 +209,8 @@ where
#[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`
@@ -208,6 +239,12 @@ 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 = "tokio1-native-tls",
@@ -228,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
@@ -237,6 +284,10 @@ impl AsyncSmtpTransportBuilder {
info: self.info,
marker_: PhantomData,
};
#[cfg(feature = "pool")]
let client = Pool::new(self.pool_config, client);
AsyncSmtpTransport { inner: client }
}
}
@@ -258,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)
}
@@ -278,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,

View File

@@ -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;
@@ -48,13 +48,13 @@ impl AsyncSmtpConnection {
///
/// 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
}
@@ -62,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
}
@@ -161,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

View File

@@ -1,8 +1,9 @@
use std::{
mem,
io, mem,
net::SocketAddr,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use futures_io::{
@@ -13,9 +14,9 @@ use futures_io::{
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
#[cfg(feature = "async-std1")]
use async_std::net::TcpStream as AsyncStd1TcpStream;
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;
@@ -23,7 +24,7 @@ use async_native_tls::TlsStream as AsyncStd1TlsStream;
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
#[cfg(feature = "async-std1-rustls-tls")]
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
use futures_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
#[cfg(feature = "tokio1-rustls-tls")]
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
@@ -107,14 +108,47 @@ impl AsyncNetworkStream {
}
#[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 {
@@ -124,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 {
@@ -196,9 +261,9 @@ impl AsyncNetworkStream {
#[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")]
@@ -225,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
@@ -277,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

View File

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

View File

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

View File

@@ -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())
}
}

View File

@@ -118,10 +118,8 @@
#[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},
@@ -144,7 +142,7 @@ pub mod client;
pub mod commands;
mod error;
pub mod extension;
#[cfg(feature = "r2d2")]
#[cfg(feature = "pool")]
mod pool;
pub mod response;
mod transport;

View File

@@ -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();
}
}
}

View 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;
}

View 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),
}
}
}

View 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);
}
}

View File

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

View File

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