feat(transport-smtp): Refactor connection pooling

This commit is contained in:
Alexis Mousset
2020-05-08 16:04:58 +02:00
parent 33b0a9e27d
commit c43e205212
7 changed files with 154 additions and 131 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "lettre"
version = "0.10.0-pre" # remember to update html_root_url
version = "0.10.0-alpha.0" # remember to update html_root_url
description = "Email client"
readme = "README.md"
homepage = "https://lettre.at"
@@ -51,7 +51,7 @@ name = "transport_smtp"
[features]
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
default = ["file-transport", "smtp-transport", "rustls-tls", "hostname", "sendmail-transport", "builder"]
default = ["file-transport", "smtp-transport", "rustls-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
file-transport = ["serde", "serde_json"]
rustls-tls = ["webpki", "webpki-roots", "rustls"]
sendmail-transport = []

View File

@@ -1,6 +1,3 @@
extern crate env_logger;
extern crate lettre;
use lettre::{Message, SmtpTransport, Transport};
fn main() {

View File

@@ -1,5 +1,3 @@
extern crate lettre;
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
fn main() {
@@ -19,7 +17,8 @@ fn main() {
// Open a remote connection to gmail
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds);
.credentials(creds)
.build();
// Send the email
let result = mailer.send(&email);

View File

@@ -191,11 +191,9 @@ use crate::{
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "r2d2")]
use r2d2::Pool;
use r2d2::{Builder, Pool};
#[cfg(feature = "rustls-tls")]
use rustls::ClientConfig;
#[cfg(feature = "r2d2")]
use std::ops::DerefMut;
use std::time::Duration;
#[cfg(feature = "rustls-tls")]
use webpki_roots::TLS_SERVER_ROOTS;
@@ -219,6 +217,8 @@ pub const SMTP_PORT: u16 = 25;
/// Default submission port
pub const SUBMISSION_PORT: u16 = 587;
/// Default submission over TLS port
///
/// https://tools.ietf.org/html/rfc8314
pub const SUBMISSIONS_PORT: u16 = 465;
/// Default timeout
@@ -247,10 +247,86 @@ pub enum Tls {
Wrapper(TlsParameters),
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool<SmtpClient>,
#[cfg(not(feature = "r2d2"))]
inner: SmtpClient,
}
impl Transport for SmtpTransport {
type Ok = Response;
type Error = Error;
/// 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()?;
#[cfg(not(feature = "r2d2"))]
let mut conn = self.inner.connection()?;
let result = conn.send(envelope, email)?;
#[cfg(not(feature = "r2d2"))]
conn.quit()?;
Ok(result)
}
}
impl SmtpTransport {
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No authentication
/// * A 60 seconds timeout for smtp commands
/// * Port 587
///
/// Consider using [`SmtpTransport::new`] instead, if possible.
pub fn builder<T: Into<String>>(server: T) -> SmtpTransportBuilder {
let mut new = SmtpInfo::default();
new.server = server.into();
SmtpTransportBuilder { info: new }
}
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
#[cfg(feature = "native-tls")]
let mut tls_builder = TlsConnector::builder();
#[cfg(feature = "native-tls")]
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
#[cfg(feature = "native-tls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls_builder.build()?);
#[cfg(feature = "rustls-tls")]
let mut tls = ClientConfig::new();
#[cfg(feature = "rustls-tls")]
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
#[cfg(feature = "rustls-tls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls);
Ok(Self::builder(relay)
.port(SUBMISSIONS_PORT)
.tls(Tls::Wrapper(tls_parameters)))
}
/// Creates a new local SMTP client to port 25
///
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
pub fn unencrypted_localhost() -> SmtpTransport {
Self::builder("localhost").port(SMTP_PORT).build()
}
}
#[allow(missing_debug_implementations)]
#[derive(Clone)]
struct SmtpInfo {
/// Name sent during EHLO
hello_name: ClientId,
/// Server we are connecting to
@@ -266,131 +342,109 @@ pub struct SmtpTransport {
/// Define network timeout
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
timeout: Option<Duration>,
/// Connection pool
#[cfg(feature = "r2d2")]
pool: Option<Pool<SmtpTransport>>,
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpTransport {
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No authentication
/// * A 60 seconds timeout for smtp commands
/// * Port 587
///
/// Consider using [`SmtpTransport::new`] instead, if possible.
pub fn new<T: Into<String>>(server: T) -> Self {
impl Default for SmtpInfo {
fn default() -> Self {
Self {
server: server.into(),
server: "localhost".to_string(),
port: SUBMISSION_PORT,
hello_name: ClientId::hostname(),
credentials: None,
authentication: DEFAULT_MECHANISMS.into(),
timeout: Some(DEFAULT_TIMEOUT),
tls: Tls::None,
#[cfg(feature = "r2d2")]
pool: None,
}
}
}
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn relay(relay: &str) -> Result<Self, Error> {
#[cfg(feature = "native-tls")]
let mut tls_builder = TlsConnector::builder();
#[cfg(feature = "native-tls")]
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
#[cfg(feature = "native-tls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls_builder.build().unwrap());
#[cfg(feature = "rustls-tls")]
let mut tls = ClientConfig::new();
#[cfg(feature = "rustls-tls")]
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
#[cfg(feature = "rustls-tls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls);
#[allow(unused_mut)]
let mut new = Self::new(relay)
.port(SUBMISSIONS_PORT)
.tls(Tls::Wrapper(tls_parameters));
#[cfg(feature = "r2d2")]
{
// Pool with default configuration
// FIXME avoid clone
let tpool = new.clone();
new = new.pool(Pool::new(tpool)?);
}
Ok(new)
}
/// Creates a new local SMTP client to port 25
///
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
pub fn unencrypted_localhost() -> Self {
Self::new("localhost").port(SMTP_PORT)
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpTransportBuilder {
info: SmtpInfo,
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder {
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self {
self.hello_name = name;
self.info.hello_name = name;
self
}
/// Set the authentication mechanism to use
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.credentials = Some(credentials);
self.info.credentials = Some(credentials);
self
}
/// Set the authentication mechanism to use
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
self.authentication = mechanisms;
self.info.authentication = mechanisms;
self
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
self.timeout = timeout;
self.info.timeout = timeout;
self
}
/// Set the port to use
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self.info.port = port;
self
}
/// Set the TLS settings to use
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
pub fn tls(mut self, tls: Tls) -> Self {
self.tls = tls;
self.info.tls = tls;
self
}
/// Set the TLS settings to use
/// Build the client
fn build_client(self) -> SmtpClient {
SmtpClient { info: self.info }
}
/// Build the transport with custom pool settings
#[cfg(feature = "r2d2")]
pub fn pool(mut self, pool: Pool<SmtpTransport>) -> Self {
self.pool = Some(pool);
self
pub fn build_with_pool(self, pool: Builder<SmtpClient>) -> SmtpTransport {
let pool = pool.build_unchecked(self.build_client());
SmtpTransport { inner: pool }
}
/// Build the transport (with default pool if enabled)
pub fn build(self) -> SmtpTransport {
let client = self.build_client();
SmtpTransport {
#[cfg(feature = "r2d2")]
inner: Pool::builder().max_size(5).build_unchecked(client),
#[cfg(not(feature = "r2d2"))]
inner: client,
}
}
}
/// Build client
#[derive(Clone)]
pub struct SmtpClient {
info: SmtpInfo,
}
impl SmtpClient {
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
fn connection(&self) -> Result<SmtpConnection, Error> {
pub fn connection(&self) -> Result<SmtpConnection, Error> {
let mut conn = SmtpConnection::connect::<(&str, u16)>(
(self.server.as_ref(), self.port),
self.timeout,
&self.hello_name,
(self.info.server.as_ref(), self.info.port),
self.info.timeout,
&self.info.hello_name,
#[allow(clippy::match_single_binding)]
match self.tls {
match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
@@ -398,23 +452,23 @@ impl SmtpTransport {
)?;
#[allow(clippy::match_single_binding)]
match self.tls {
match self.info.tls {
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
conn.starttls(tls_parameters, &self.hello_name)?;
conn.starttls(tls_parameters, &self.info.hello_name)?;
}
}
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
Tls::Required(ref tls_parameters) => {
conn.starttls(tls_parameters, &self.hello_name)?;
conn.starttls(tls_parameters, &self.info.hello_name)?;
}
_ => (),
}
match &self.credentials {
match &self.info.credentials {
Some(credentials) => {
conn.auth(self.authentication.as_slice(), &credentials)?;
conn.auth(self.info.authentication.as_slice(), &credentials)?;
}
None => (),
}
@@ -422,32 +476,3 @@ impl SmtpTransport {
Ok(conn)
}
}
impl Transport for SmtpTransport {
type Ok = Response;
type Error = Error;
/// Sends an email
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
#[cfg(feature = "r2d2")]
let mut conn: Box<dyn DerefMut<Target = SmtpConnection>> = match self.pool {
Some(ref p) => Box::new(p.get()?),
None => Box::new(Box::new(self.connection()?)),
};
#[cfg(not(feature = "r2d2"))]
let mut conn = self.connection()?;
let result = conn.send(envelope, email)?;
#[cfg(feature = "r2d2")]
{
if self.pool.is_none() {
conn.quit()?;
}
}
#[cfg(not(feature = "r2d2"))]
conn.quit()?;
Ok(result)
}
}

View File

@@ -1,7 +1,7 @@
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpTransport};
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
use r2d2::ManageConnection;
impl ManageConnection for SmtpTransport {
impl ManageConnection for SmtpClient {
type Connection = SmtpConnection;
type Error = Error;

View File

@@ -12,8 +12,9 @@ mod test {
.subject("Happy new year")
.body("Be happy!")
.unwrap();
SmtpTransport::new("127.0.0.1")
SmtpTransport::builder("127.0.0.1")
.port(2525)
.build()
.send(&email)
.unwrap();
}

View File

@@ -1,6 +1,6 @@
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
mod test {
use lettre::{Envelope, SmtpTransport, Tls, Transport};
use lettre::{Envelope, SmtpTransport, Transport};
use r2d2::Pool;
use std::{sync::mpsc, thread};
@@ -14,10 +14,10 @@ mod test {
#[test]
fn send_one() {
let client = SmtpTransport::new("127.0.0.1").port(2525);
let c = client.clone();
let pool = Pool::builder().max_size(1).build(c).unwrap();
let mailer = client.pool(pool);
let pool = Pool::builder().max_size(1);
let mailer = SmtpTransport::builder("127.0.0.1")
.port(2525)
.build_with_pool(pool);
let result = mailer.send_raw(&envelope(), b"test");
assert!(result.is_ok());
@@ -25,10 +25,11 @@ mod test {
#[test]
fn send_from_thread() {
let client = SmtpTransport::new("127.0.0.1").port(2525);
let c = client.clone();
let pool = Pool::builder().max_size(1).build(c).unwrap();
let mailer = client.pool(pool);
let pool = Pool::builder().max_size(1);
let mailer = SmtpTransport::builder("127.0.0.1")
.port(2525)
.build_with_pool(pool);
let (s1, r1) = mpsc::channel();
let (s2, r2) = mpsc::channel();