Merge pull request #413 from amousset/refactor-net

Refactor smtp client
This commit is contained in:
Alexis Mousset
2020-05-02 19:56:49 +02:00
committed by GitHub
23 changed files with 526 additions and 749 deletions

View File

@@ -37,6 +37,7 @@ serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
textnonce = { version = "0.7", optional = true } textnonce = { version = "0.7", optional = true }
webpki = { version = "0.21", optional = true } webpki = { version = "0.21", optional = true }
webpki-roots = { version = "0.19", optional = true }
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"
@@ -50,10 +51,9 @@ name = "transport_smtp"
[features] [features]
builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"] builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"]
connection-pool = ["r2d2"] default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "rustls-tls", "builder", "r2d2"]
default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "native-tls", "builder"]
file-transport = ["serde", "serde_json"] file-transport = ["serde", "serde_json"]
rustls-tls = ["webpki", "rustls"] rustls-tls = ["webpki", "webpki-roots", "rustls"]
sendmail-transport = [] sendmail-transport = []
smtp-transport = ["bufstream", "base64", "nom"] smtp-transport = ["bufstream", "base64", "nom"]
unstable = [] unstable = []

View File

@@ -1,12 +1,8 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lettre::{ use lettre::{Message, SmtpTransport, Transport};
transport::smtp::ConnectionReuseParameters, ClientSecurity, Message, SmtpClient, Transport,
};
fn bench_simple_send(c: &mut Criterion) { fn bench_simple_send(c: &mut Criterion) {
let mut sender = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None) let sender = SmtpTransport::new("127.0.0.1").port(2525);
.unwrap()
.transport();
c.bench_function("send email", move |b| { c.bench_function("send email", move |b| {
b.iter(|| { b.iter(|| {
@@ -24,10 +20,7 @@ fn bench_simple_send(c: &mut Criterion) {
} }
fn bench_reuse_send(c: &mut Criterion) { fn bench_reuse_send(c: &mut Criterion) {
let mut sender = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None) let sender = SmtpTransport::new("127.0.0.1").port(2525);
.unwrap()
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.transport();
c.bench_function("send email with connection reuse", move |b| { c.bench_function("send email with connection reuse", move |b| {
b.iter(|| { b.iter(|| {
let email = Message::builder() let email = Message::builder()

View File

@@ -1,7 +1,7 @@
extern crate env_logger; extern crate env_logger;
extern crate lettre; extern crate lettre;
use lettre::{Message, SmtpClient, Transport}; use lettre::{Message, SmtpTransport, Transport};
fn main() { fn main() {
env_logger::init(); env_logger::init();
@@ -14,7 +14,7 @@ fn main() {
.unwrap(); .unwrap();
// Open a local connection on port 25 // Open a local connection on port 25
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport(); let mailer = SmtpTransport::unencrypted_localhost();
// Send the email // Send the email
let result = mailer.send(&email); let result = mailer.send(&email);

View File

@@ -1,6 +1,6 @@
extern crate lettre; extern crate lettre;
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpClient, Transport}; use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
fn main() { fn main() {
let email = Message::builder() let email = Message::builder()
@@ -17,10 +17,9 @@ fn main() {
); );
// Open a remote connection to gmail // Open a remote connection to gmail
let mut mailer = SmtpClient::new_simple("smtp.gmail.com") let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap() .unwrap()
.credentials(creds) .credentials(creds);
.transport();
// Send the email // Send the email
let result = mailer.send(&email); let result = mailer.send(&email);

View File

@@ -270,7 +270,6 @@ pub mod serde {
} }
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?; let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?; let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
// FIXME avoid unwrap here
Ok(Address::new(user, domain).unwrap()) Ok(Address::new(user, domain).unwrap())
} }
} }
@@ -279,5 +278,3 @@ pub mod serde {
} }
} }
} }
// FIXME test serializer deserializer

View File

@@ -31,11 +31,11 @@ pub use crate::transport::file::FileTransport;
#[cfg(feature = "sendmail-transport")] #[cfg(feature = "sendmail-transport")]
pub use crate::transport::sendmail::SendmailTransport; pub use crate::transport::sendmail::SendmailTransport;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::client::net::ClientTlsParameters; pub use crate::transport::smtp::client::net::TlsParameters;
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))] #[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
pub use crate::transport::smtp::r2d2::SmtpConnectionManager; pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport}; pub use crate::transport::smtp::{SmtpTransport, Tls};
#[cfg(feature = "builder")] #[cfg(feature = "builder")]
use std::convert::TryFrom; use std::convert::TryFrom;
@@ -115,14 +115,12 @@ pub trait Transport<'a> {
type Result; type Result;
/// Sends the email /// Sends the email
/// FIXME not mut fn send(&self, message: &Message) -> Self::Result {
fn send(&mut self, message: &Message) -> Self::Result {
let raw = message.formatted(); let raw = message.formatted();
self.send_raw(message.envelope(), &raw) self.send_raw(message.envelope(), &raw)
} }
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result; fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result;
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -41,7 +41,7 @@ struct SerializableEmail<'a> {
impl<'a> Transport<'a> for FileTransport { impl<'a> Transport<'a> for FileTransport {
type Result = FileResult; type Result = FileResult;
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result {
let email_id = Uuid::new_v4(); let email_id = Uuid::new_v4();
let mut file = self.path.clone(); let mut file = self.path.clone();

View File

@@ -39,7 +39,7 @@ impl SendmailTransport {
impl<'a> Transport<'a> for SendmailTransport { impl<'a> Transport<'a> for SendmailTransport {
type Result = SendmailResult; type Result = SendmailResult;
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result { fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result {
let email_id = Uuid::new_v4(); let email_id = Uuid::new_v4();
// Spawn the sendmail command // Spawn the sendmail command

View File

@@ -3,13 +3,9 @@
use crate::transport::smtp::error::Error; use crate::transport::smtp::error::Error;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
/// Accepted authentication mechanisms on an encrypted connection /// Accepted authentication mechanisms
/// Trying LOGIN last as it is deprecated. /// Trying LOGIN last as it is deprecated.
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login]; pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Accepted authentication mechanisms on an unencrypted connection
// FIXME remove
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
/// Convertible to user credentials /// Convertible to user credentials
pub trait IntoCredentials { pub trait IntoCredentials {

View File

@@ -2,18 +2,20 @@
use crate::transport::smtp::{ use crate::transport::smtp::{
authentication::{Credentials, Mechanism}, authentication::{Credentials, Mechanism},
client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout}, client::net::{NetworkStream, TlsParameters},
commands::*, commands::*,
error::{Error, SmtpResult}, error::{Error, SmtpResult},
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
response::Response, response::Response,
}; };
use crate::Envelope;
use bufstream::BufStream; use bufstream::BufStream;
use log::debug; use log::debug;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use std::fmt::Debug; use std::fmt::Debug;
use std::{ use std::{
fmt::Display, fmt::Display,
io::{self, BufRead, Read, Write}, io::{self, BufRead, Write},
net::ToSocketAddrs, net::ToSocketAddrs,
string::String, string::String,
time::Duration, time::Duration,
@@ -77,12 +79,27 @@ fn escape_crlf(string: &str) -> String {
string.replace("\r\n", "<CRLF>") string.replace("\r\n", "<CRLF>")
} }
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
$client.abort();
return Err(From::from(err))
},
}
})
);
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
#[derive(Default)] pub struct SmtpConnection {
pub struct InnerClient<S: Write + Read = NetworkStream> {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: Option<BufStream<S>>, stream: BufStream<NetworkStream>,
/// Panic state
panic: bool,
/// Information about the server
server_info: ServerInfo,
} }
macro_rules! return_err ( macro_rules! return_err (
@@ -91,102 +108,176 @@ macro_rules! return_err (
}) })
); );
impl<S: Write + Read> InnerClient<S> { impl SmtpConnection {
/// Creates a new SMTP client pub fn server_info(&self) -> &ServerInfo {
/// &self.server_info
/// It does not connects to the server, but only creates the `Client`
pub fn new() -> InnerClient<S> {
InnerClient { stream: None }
}
}
impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.command(QuitCommand);
self.stream = None;
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: S) {
self.stream = Some(BufStream::new(stream));
}
/// Upgrades the underlying connection to SSL/TLS
#[cfg(any(feature = "native-tls", feature = "rustls"))]
pub fn upgrade_tls_stream(
&mut self,
tls_parameters: &ClientTlsParameters,
) -> Result<(), Error> {
match self.stream {
Some(ref mut stream) => stream.get_mut().upgrade_tls(tls_parameters),
None => Ok(()),
}
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
self.stream
.as_ref()
.map(|s| s.get_ref().is_encrypted())
.unwrap_or(false)
}
/// Set timeout
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
if let Some(ref mut stream) = self.stream {
stream.get_mut().set_read_timeout(duration)?;
stream.get_mut().set_write_timeout(duration)?;
}
Ok(())
} }
/// Connects to the configured server /// Connects to the configured server
///
/// Sends EHLO and parses server information
pub fn connect<A: ToSocketAddrs>( pub fn connect<A: ToSocketAddrs>(
&mut self, server: A,
addr: &A,
timeout: Option<Duration>, timeout: Option<Duration>,
tls_parameters: Option<&ClientTlsParameters>, hello_name: &ClientId,
) -> Result<(), Error> { tls_parameters: Option<&TlsParameters>,
// Connect should not be called when the client is already connected ) -> Result<SmtpConnection, Error> {
if self.stream.is_some() { let mut addresses = server.to_socket_addrs()?;
return_err!("The connection is already established", self);
}
let mut addresses = addr.to_socket_addrs()?;
// FIXME try all
let server_addr = match addresses.next() { let server_addr = match addresses.next() {
Some(addr) => addr, Some(addr) => addr,
None => return_err!("Could not resolve hostname", self), None => return_err!("Could not resolve hostname", self),
}; };
debug!("connecting to {}", server_addr); debug!("connecting to {}", server_addr);
// Try to connect let stream = BufStream::new(NetworkStream::connect(
self.set_stream(Connector::connect(&server_addr, timeout, tls_parameters)?); &server_addr,
timeout,
tls_parameters,
)?);
let mut conn = SmtpConnection {
stream,
panic: false,
server_info: ServerInfo::default(),
};
conn.set_timeout(timeout)?;
// TODO log
let _response = conn.read_response()?;
conn.ehlo(hello_name)?;
// Print server information
debug!("server {}", conn.server_info);
Ok(conn)
}
pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> SmtpResult {
// Mail
let mut mail_options = vec![];
if self.server_info().supports_feature(Extension::EightBitMime) {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
try_smtp!(
self.command(Mail::new(envelope.from().cloned(), mail_options,)),
self
);
// Recipient
for to_address in envelope.to() {
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
}
// Data
try_smtp!(self.command(Data), self);
// Message content
let result = try_smtp!(self.message(email), self);
Ok(result)
}
pub fn has_broken(&self) -> bool {
self.panic
}
pub fn can_starttls(&self) -> bool {
!self.stream.get_ref().is_encrypted()
&& self.server_info.supports_feature(Extension::StartTls)
}
pub fn starttls(
&mut self,
tls_parameters: &TlsParameters,
hello_name: &ClientId,
) -> Result<(), Error> {
if self.server_info.supports_feature(Extension::StartTls) {
#[cfg(any(feature = "native-tls", feature = "rustls"))]
{
try_smtp!(self.command(Starttls), self);
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
debug!("connection encrypted");
// Send EHLO again
try_smtp!(self.ehlo(hello_name), self);
Ok(())
}
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
// This should never happen as `Tls` can only be created
// when a TLS library is enabled
unreachable!("TLS support required but not supported");
} else {
Err(Error::Client("STARTTLS is not supported on this server"))
}
}
/// Send EHLO and update server info
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
let ehlo_response = try_smtp!(
self.command(Ehlo::new(ClientId::new(hello_name.to_string()))),
self
);
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
Ok(()) Ok(())
} }
pub fn quit(&mut self) -> SmtpResult {
Ok(try_smtp!(self.command(Quit), self))
}
pub fn abort(&mut self) {
// Only try to quit if we are not already broken
if !self.panic {
self.panic = true;
let _ = self.command(Quit);
}
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: NetworkStream) {
self.stream = BufStream::new(stream);
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
self.stream.get_ref().is_encrypted()
}
/// Set timeout
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
self.stream.get_mut().set_read_timeout(duration)?;
self.stream.get_mut().set_write_timeout(duration)
}
/// Checks if the server is connected using the NOOP SMTP command /// Checks if the server is connected using the NOOP SMTP command
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))] pub fn test_connected(&mut self) -> bool {
pub fn is_connected(&mut self) -> bool { self.command(Noop).is_ok()
self.stream.is_some() && self.command(NoopCommand).is_ok()
} }
/// Sends an AUTH command with the given mechanism, and handles challenge if needed /// Sends an AUTH command with the given mechanism, and handles challenge if needed
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult { pub fn auth(&mut self, mechanisms: &[Mechanism], credentials: &Credentials) -> SmtpResult {
let mechanism = match self.server_info.get_auth_mechanism(mechanisms) {
Some(m) => m,
None => {
return Err(Error::Client(
"No compatible authentication mechanism was found",
))
}
};
// Limit challenges to avoid blocking // Limit challenges to avoid blocking
let mut challenges = 10; let mut challenges = 10;
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?; let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = self.command(AuthCommand::new_from_response( response = try_smtp!(
mechanism, self.command(Auth::new_from_response(
credentials.clone(), mechanism,
&response, credentials.clone(),
)?)?; &response,
)?),
self
);
} }
if challenges == 0 { if challenges == 0 {
@@ -214,12 +305,8 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
/// Writes a string to the server /// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> { fn write(&mut self, string: &[u8]) -> Result<(), Error> {
if self.stream.is_none() { self.stream.write_all(string)?;
return Err(From::from("Connection closed")); self.stream.flush()?;
}
self.stream.as_mut().unwrap().write_all(string)?;
self.stream.as_mut().unwrap().flush()?;
debug!( debug!(
"Wrote: {}", "Wrote: {}",
@@ -240,7 +327,7 @@ impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
break; break;
} }
// TODO read more than one line // TODO read more than one line
let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?; let read_count = self.stream.read_line(&mut raw_response)?;
// EOF is reached // EOF is reached
if read_count == 0 { if read_count == 0 {

View File

@@ -18,7 +18,7 @@ use std::{
/// Parameters to use for secure clients /// Parameters to use for secure clients
#[derive(Clone)] #[derive(Clone)]
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct ClientTlsParameters { pub struct TlsParameters {
/// A connector from `native-tls` /// A connector from `native-tls`
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
connector: TlsConnector, connector: TlsConnector,
@@ -30,17 +30,17 @@ pub struct ClientTlsParameters {
domain: String, domain: String,
} }
impl ClientTlsParameters { impl TlsParameters {
/// Creates a `ClientTlsParameters` /// Creates a `TlsParameters`
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
pub fn new(domain: String, connector: TlsConnector) -> Self { pub fn new(domain: String, connector: TlsConnector) -> Self {
ClientTlsParameters { connector, domain } Self { connector, domain }
} }
/// Creates a `ClientTlsParameters` /// Creates a `TlsParameters`
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
pub fn new(domain: String, connector: ClientConfig) -> Self { pub fn new(domain: String, connector: ClientConfig) -> Self {
ClientTlsParameters { Self {
connector: Box::new(connector), connector: Box::new(connector),
domain, domain,
} }
@@ -87,6 +87,85 @@ impl NetworkStream {
NetworkStream::Mock(_) => Ok(()), NetworkStream::Mock(_) => Ok(()),
} }
} }
pub fn connect(
addr: &SocketAddr,
timeout: Option<Duration>,
tls_parameters: Option<&TlsParameters>,
) -> Result<NetworkStream, Error> {
let tcp_stream = match timeout {
Some(t) => TcpStream::connect_timeout(addr, t)?,
None => TcpStream::connect(addr)?,
};
match tls_parameters {
#[cfg(feature = "native-tls")]
Some(context) => context
.connector
.connect(context.domain.as_ref(), tcp_stream)
.map(|tls| NetworkStream::Tls(Box::new(tls)))
.map_err(|e| Error::Io(io::Error::new(ErrorKind::Other, e))),
#[cfg(feature = "rustls")]
Some(context) => {
let domain = webpki::DNSNameRef::try_from_ascii_str(&context.domain)?;
Ok(NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
ClientSession::new(&Arc::new(*context.connector.clone()), domain),
tcp_stream,
))))
}
None => Ok(NetworkStream::Tcp(tcp_stream)),
}
}
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
*self = match *self {
#[cfg(feature = "native-tls")]
NetworkStream::Tcp(ref mut stream) => match tls_parameters
.connector
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
{
Ok(tls_stream) => NetworkStream::Tls(Box::new(tls_stream)),
Err(err) => return Err(Error::Io(io::Error::new(ErrorKind::Other, err))),
},
#[cfg(feature = "rustls")]
NetworkStream::Tcp(ref mut stream) => {
let domain = webpki::DNSNameRef::try_from_ascii_str(&tls_parameters.domain)?;
NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
ClientSession::new(&Arc::new(*tls_parameters.connector.clone()), domain),
stream.try_clone().unwrap(),
)))
}
NetworkStream::Tls(_) | NetworkStream::Mock(_) => return Ok(()),
};
Ok(())
}
pub fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Tcp(_) | NetworkStream::Mock(_) => false,
NetworkStream::Tls(_) => true,
}
}
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
/// Set write timeout for IO calls
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
} }
impl Read for NetworkStream { impl Read for NetworkStream {
@@ -125,108 +204,3 @@ impl Write for NetworkStream {
} }
} }
} }
/// A trait for the concept of opening a stream
pub trait Connector: Sized {
/// Opens a connection to the given IP socket
fn connect(
addr: &SocketAddr,
timeout: Option<Duration>,
tls_parameters: Option<&ClientTlsParameters>,
) -> Result<Self, Error>;
/// Upgrades to TLS connection
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> Result<(), Error>;
/// Is the NetworkStream encrypted
fn is_encrypted(&self) -> bool;
}
impl Connector for NetworkStream {
fn connect(
addr: &SocketAddr,
timeout: Option<Duration>,
tls_parameters: Option<&ClientTlsParameters>,
) -> Result<NetworkStream, Error> {
let tcp_stream = match timeout {
Some(duration) => TcpStream::connect_timeout(addr, duration)?,
None => TcpStream::connect(addr)?,
};
match tls_parameters {
#[cfg(feature = "native-tls")]
Some(context) => context
.connector
.connect(context.domain.as_ref(), tcp_stream)
.map(|tls| NetworkStream::Tls(Box::new(tls)))
.map_err(|e| Error::Io(io::Error::new(ErrorKind::Other, e))),
#[cfg(feature = "rustls")]
Some(context) => {
let domain = webpki::DNSNameRef::try_from_ascii_str(&context.domain)?;
Ok(NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
ClientSession::new(&Arc::new(*context.connector.clone()), domain),
tcp_stream,
))))
}
None => Ok(NetworkStream::Tcp(tcp_stream)),
}
}
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> Result<(), Error> {
*self = match *self {
#[cfg(feature = "native-tls")]
NetworkStream::Tcp(ref mut stream) => match tls_parameters
.connector
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
{
Ok(tls_stream) => NetworkStream::Tls(Box::new(tls_stream)),
Err(err) => return Err(Error::Io(io::Error::new(ErrorKind::Other, err))),
},
#[cfg(feature = "rustls")]
NetworkStream::Tcp(ref mut stream) => {
let domain = webpki::DNSNameRef::try_from_ascii_str(&tls_parameters.domain)?;
NetworkStream::Tls(Box::new(rustls::StreamOwned::new(
ClientSession::new(&Arc::new(*tls_parameters.connector.clone()), domain),
stream.try_clone().unwrap(),
)))
}
NetworkStream::Tls(_) | NetworkStream::Mock(_) => return Ok(()),
};
Ok(())
}
fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Tcp(_) | NetworkStream::Mock(_) => false,
NetworkStream::Tls(_) => true,
}
}
}
/// A trait for read and write timeout support
pub trait Timeout: Sized {
/// Set read timeout for IO calls
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
/// Set write timeout for IO calls
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
}
impl Timeout for NetworkStream {
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
/// Set write timeout for IO calls
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
}

View File

@@ -18,30 +18,29 @@ use std::{
/// EHLO command /// EHLO command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EhloCommand { pub struct Ehlo {
client_id: ClientId, client_id: ClientId,
} }
impl Display for EhloCommand { impl Display for Ehlo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[allow(clippy::write_with_newline)]
write!(f, "EHLO {}\r\n", self.client_id) write!(f, "EHLO {}\r\n", self.client_id)
} }
} }
impl EhloCommand { impl Ehlo {
/// Creates a EHLO command /// Creates a EHLO command
pub fn new(client_id: ClientId) -> EhloCommand { pub fn new(client_id: ClientId) -> Ehlo {
EhloCommand { client_id } Ehlo { client_id }
} }
} }
/// STARTTLS command /// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StarttlsCommand; pub struct Starttls;
impl Display for StarttlsCommand { impl Display for Starttls {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("STARTTLS\r\n") f.write_str("STARTTLS\r\n")
} }
@@ -50,12 +49,12 @@ impl Display for StarttlsCommand {
/// MAIL command /// MAIL command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MailCommand { pub struct Mail {
sender: Option<Address>, sender: Option<Address>,
parameters: Vec<MailParameter>, parameters: Vec<MailParameter>,
} }
impl Display for MailCommand { impl Display for Mail {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!( write!(
f, f,
@@ -69,22 +68,22 @@ impl Display for MailCommand {
} }
} }
impl MailCommand { impl Mail {
/// Creates a MAIL command /// Creates a MAIL command
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> MailCommand { pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
MailCommand { sender, parameters } Mail { sender, parameters }
} }
} }
/// RCPT command /// RCPT command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RcptCommand { pub struct Rcpt {
recipient: Address, recipient: Address,
parameters: Vec<RcptParameter>, parameters: Vec<RcptParameter>,
} }
impl Display for RcptCommand { impl Display for Rcpt {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?; write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters { for parameter in &self.parameters {
@@ -94,10 +93,10 @@ impl Display for RcptCommand {
} }
} }
impl RcptCommand { impl Rcpt {
/// Creates an RCPT command /// Creates an RCPT command
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> RcptCommand { pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
RcptCommand { Rcpt {
recipient, recipient,
parameters, parameters,
} }
@@ -107,9 +106,9 @@ impl RcptCommand {
/// DATA command /// DATA command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DataCommand; pub struct Data;
impl Display for DataCommand { impl Display for Data {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("DATA\r\n") f.write_str("DATA\r\n")
} }
@@ -118,9 +117,9 @@ impl Display for DataCommand {
/// QUIT command /// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct QuitCommand; pub struct Quit;
impl Display for QuitCommand { impl Display for Quit {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("QUIT\r\n") f.write_str("QUIT\r\n")
} }
@@ -129,9 +128,9 @@ impl Display for QuitCommand {
/// NOOP command /// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NoopCommand; pub struct Noop;
impl Display for NoopCommand { impl Display for Noop {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NOOP\r\n") f.write_str("NOOP\r\n")
} }
@@ -140,11 +139,11 @@ impl Display for NoopCommand {
/// HELP command /// HELP command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HelpCommand { pub struct Help {
argument: Option<String>, argument: Option<String>,
} }
impl Display for HelpCommand { impl Display for Help {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("HELP")?; f.write_str("HELP")?;
if self.argument.is_some() { if self.argument.is_some() {
@@ -154,61 +153,61 @@ impl Display for HelpCommand {
} }
} }
impl HelpCommand { impl Help {
/// Creates an HELP command /// Creates an HELP command
pub fn new(argument: Option<String>) -> HelpCommand { pub fn new(argument: Option<String>) -> Help {
HelpCommand { argument } Help { argument }
} }
} }
/// VRFY command /// VRFY command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VrfyCommand { pub struct Vrfy {
argument: String, argument: String,
} }
impl Display for VrfyCommand { impl Display for Vrfy {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[allow(clippy::write_with_newline)] #[allow(clippy::write_with_newline)]
write!(f, "VRFY {}\r\n", self.argument) write!(f, "VRFY {}\r\n", self.argument)
} }
} }
impl VrfyCommand { impl Vrfy {
/// Creates a VRFY command /// Creates a VRFY command
pub fn new(argument: String) -> VrfyCommand { pub fn new(argument: String) -> Vrfy {
VrfyCommand { argument } Vrfy { argument }
} }
} }
/// EXPN command /// EXPN command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExpnCommand { pub struct Expn {
argument: String, argument: String,
} }
impl Display for ExpnCommand { impl Display for Expn {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[allow(clippy::write_with_newline)] #[allow(clippy::write_with_newline)]
write!(f, "EXPN {}\r\n", self.argument) write!(f, "EXPN {}\r\n", self.argument)
} }
} }
impl ExpnCommand { impl Expn {
/// Creates an EXPN command /// Creates an EXPN command
pub fn new(argument: String) -> ExpnCommand { pub fn new(argument: String) -> Expn {
ExpnCommand { argument } Expn { argument }
} }
} }
/// RSET command /// RSET command
#[derive(PartialEq, Clone, Debug, Copy)] #[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RsetCommand; pub struct Rset;
impl Display for RsetCommand { impl Display for Rset {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("RSET\r\n") f.write_str("RSET\r\n")
} }
@@ -217,14 +216,14 @@ impl Display for RsetCommand {
/// AUTH command /// AUTH command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AuthCommand { pub struct Auth {
mechanism: Mechanism, mechanism: Mechanism,
credentials: Credentials, credentials: Credentials,
challenge: Option<String>, challenge: Option<String>,
response: Option<String>, response: Option<String>,
} }
impl Display for AuthCommand { impl Display for Auth {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = self let encoded_response = self
.response .response
@@ -243,19 +242,19 @@ impl Display for AuthCommand {
} }
} }
impl AuthCommand { impl Auth {
/// Creates an AUTH command (from a challenge if provided) /// Creates an AUTH command (from a challenge if provided)
pub fn new( pub fn new(
mechanism: Mechanism, mechanism: Mechanism,
credentials: Credentials, credentials: Credentials,
challenge: Option<String>, challenge: Option<String>,
) -> Result<AuthCommand, Error> { ) -> Result<Auth, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() { let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_deref())?) Some(mechanism.response(&credentials, challenge.as_deref())?)
} else { } else {
None None
}; };
Ok(AuthCommand { Ok(Auth {
mechanism, mechanism,
credentials, credentials,
challenge, challenge,
@@ -269,7 +268,7 @@ impl AuthCommand {
mechanism: Mechanism, mechanism: Mechanism,
credentials: Credentials, credentials: Credentials,
response: &Response, response: &Response,
) -> Result<AuthCommand, Error> { ) -> Result<Auth, Error> {
if !response.has_code(334) { if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge")); return Err(Error::ResponseParsing("Expecting a challenge"));
} }
@@ -284,7 +283,7 @@ impl AuthCommand {
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?); let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
Ok(AuthCommand { Ok(Auth {
mechanism, mechanism,
credentials, credentials,
challenge: Some(decoded_challenge), challenge: Some(decoded_challenge),
@@ -311,26 +310,23 @@ mod test {
keyword: "TEST".to_string(), keyword: "TEST".to_string(),
value: Some("value".to_string()), value: Some("value".to_string()),
}; };
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n"); assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
assert_eq!( assert_eq!(
format!("{}", MailCommand::new(Some(email.clone()), vec![])), format!("{}", Mail::new(Some(email.clone()), vec![])),
"MAIL FROM:<test@example.com>\r\n" "MAIL FROM:<test@example.com>\r\n"
); );
assert_eq!( assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
format!("{}", MailCommand::new(None, vec![])),
"MAIL FROM:<>\r\n"
);
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)]) Mail::new(Some(email.clone()), vec![MailParameter::Size(42)])
), ),
"MAIL FROM:<test@example.com> SIZE=42\r\n" "MAIL FROM:<test@example.com> SIZE=42\r\n"
); );
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",
MailCommand::new( Mail::new(
Some(email.clone()), Some(email.clone()),
vec![ vec![
MailParameter::Size(42), MailParameter::Size(42),
@@ -342,42 +338,42 @@ mod test {
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n" "MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
); );
assert_eq!( assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![])), format!("{}", Rcpt::new(email.clone(), vec![])),
"RCPT TO:<test@example.com>\r\n" "RCPT TO:<test@example.com>\r\n"
); );
assert_eq!( assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])), format!("{}", Rcpt::new(email.clone(), vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n" "RCPT TO:<test@example.com> TEST=value\r\n"
); );
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n"); assert_eq!(format!("{}", Quit), "QUIT\r\n");
assert_eq!(format!("{}", DataCommand), "DATA\r\n"); assert_eq!(format!("{}", Data), "DATA\r\n");
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n"); assert_eq!(format!("{}", Noop), "NOOP\r\n");
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n"); assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
assert_eq!( assert_eq!(
format!("{}", HelpCommand::new(Some("test".to_string()))), format!("{}", Help::new(Some("test".to_string()))),
"HELP test\r\n" "HELP test\r\n"
); );
assert_eq!( assert_eq!(
format!("{}", VrfyCommand::new("test".to_string())), format!("{}", Vrfy::new("test".to_string())),
"VRFY test\r\n" "VRFY test\r\n"
); );
assert_eq!( assert_eq!(
format!("{}", ExpnCommand::new("test".to_string())), format!("{}", Expn::new("test".to_string())),
"EXPN test\r\n" "EXPN test\r\n"
); );
assert_eq!(format!("{}", RsetCommand), "RSET\r\n"); assert_eq!(format!("{}", Rset), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string()); let credentials = Credentials::new("user".to_string(), "password".to_string());
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap() Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
), ),
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n" "AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
); );
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap() Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
), ),
"AUTH LOGIN\r\n" "AUTH LOGIN\r\n"
); );

View File

@@ -3,11 +3,9 @@
use self::Error::*; use self::Error::*;
use crate::transport::smtp::response::{Response, Severity}; use crate::transport::smtp::response::{Response, Severity};
use base64::DecodeError; use base64::DecodeError;
#[cfg(feature = "native-tls")]
use std::{ use std::{
error::Error as StdError, error::Error as StdError,
fmt, fmt::{self, Display, Formatter},
fmt::{Display, Formatter},
io, io,
string::FromUtf8Error, string::FromUtf8Error,
}; };
@@ -43,10 +41,11 @@ pub enum Error {
/// Invalid hostname /// Invalid hostname
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InvalidDNSName(webpki::InvalidDNSNameError), InvalidDNSName(webpki::InvalidDNSNameError),
#[cfg(feature = "r2d2")]
Pool(r2d2::Error),
} }
impl Display for Error { impl Display for Error {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
match *self { match *self {
// Try to display the first line of the server's response that usually // Try to display the first line of the server's response that usually
@@ -70,6 +69,7 @@ impl Display for Error {
Parsing(ref err) => fmt.write_str(err.description()), Parsing(ref err) => fmt.write_str(err.description()),
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
InvalidDNSName(ref err) => err.fmt(fmt), InvalidDNSName(ref err) => err.fmt(fmt),
Pool(ref err) => err.fmt(fmt),
} }
} }
} }
@@ -129,6 +129,13 @@ impl From<webpki::InvalidDNSNameError> for Error {
} }
} }
#[cfg(feature = "r2d2")]
impl From<r2d2::Error> for Error {
fn from(err: r2d2::Error) -> Error {
Pool(err)
}
}
impl From<Response> for Error { impl From<Response> for Error {
fn from(response: Response) -> Error { fn from(response: Response) -> Error {
match response.code.severity { match response.code.severity {

View File

@@ -52,6 +52,10 @@ impl ClientId {
.unwrap_or_else(|_| DEFAULT_DOMAIN_CLIENT_ID.to_string()), .unwrap_or_else(|_| DEFAULT_DOMAIN_CLIENT_ID.to_string()),
) )
} }
#[cfg(not(feature = "hostname"))]
pub fn hostname() -> ClientId {
ClientId::Domain(DEFAULT_DOMAIN_CLIENT_ID.to_string())
}
} }
/// Supported ESMTP keywords /// Supported ESMTP keywords
@@ -86,7 +90,7 @@ impl Display for Extension {
} }
/// Contains information about an SMTP server /// Contains information about an SMTP server
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ServerInfo { pub struct ServerInfo {
/// Server name /// Server name
@@ -176,6 +180,16 @@ impl ServerInfo {
self.features self.features
.contains(&Extension::Authentication(mechanism)) .contains(&Extension::Authentication(mechanism))
} }
/// Gets a compatible mechanism from list
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
for mechanism in mechanisms {
if self.supports_auth_mechanism(*mechanism) {
return Some(*mechanism);
}
}
None
}
} }
/// A `MAIL FROM` extension parameter /// A `MAIL FROM` extension parameter

View File

@@ -10,32 +10,25 @@
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152)) //! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms //! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487)) //! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//! //!
use crate::Envelope;
use crate::{ use crate::{
transport::smtp::{ transport::smtp::{
authentication::{ authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS, client::{net::TlsParameters, SmtpConnection},
},
client::{net::ClientTlsParameters, InnerClient},
commands::*,
error::{Error, SmtpResult}, error::{Error, SmtpResult},
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, extension::ClientId,
}, },
Transport, Envelope, Transport,
}; };
use log::{debug, info};
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "r2d2")]
use r2d2::Pool;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use rustls::ClientConfig; use rustls::ClientConfig;
use std::{ use std::ops::DerefMut;
net::{SocketAddr, ToSocketAddrs}, use std::time::Duration;
time::Duration,
};
use uuid::Uuid;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use webpki_roots::TLS_SERVER_ROOTS; use webpki_roots::TLS_SERVER_ROOTS;
@@ -44,8 +37,8 @@ pub mod client;
pub mod commands; pub mod commands;
pub mod error; pub mod error;
pub mod extension; pub mod extension;
#[cfg(feature = "connection-pool")] #[cfg(feature = "r2d2")]
pub mod r2d2; pub mod pool;
pub mod response; pub mod response;
pub mod util; pub mod util;
@@ -62,378 +55,192 @@ pub const SUBMISSIONS_PORT: u16 = 465;
/// Accepted protocols by default. /// Accepted protocols by default.
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults. /// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
/// This is also rustls' default behavior // This is also rustls' default behavior
#[cfg(feature = "native-tls")] #[cfg(feature = "native-tls")]
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12; const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
/// How to apply TLS to a client connection /// How to apply TLS to a client connection
#[derive(Clone)] #[derive(Clone)]
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub enum ClientSecurity { pub enum Tls {
/// Insecure connection only (for testing purposes) /// Insecure connection only (for testing purposes)
None, None,
/// Start with insecure connection and use `STARTTLS` when available /// Start with insecure connection and use `STARTTLS` when available
Opportunistic(ClientTlsParameters), #[cfg(any(feature = "native-tls", feature = "rustls"))]
Opportunistic(TlsParameters),
/// Start with insecure connection and require `STARTTLS` /// Start with insecure connection and require `STARTTLS`
Required(ClientTlsParameters), #[cfg(any(feature = "native-tls", feature = "rustls"))]
Required(TlsParameters),
/// Use TLS wrapped connection /// Use TLS wrapped connection
Wrapper(ClientTlsParameters), #[cfg(any(feature = "native-tls", feature = "rustls"))]
} Wrapper(TlsParameters),
/// Configures connection reuse behavior
#[derive(Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ConnectionReuseParameters {
/// Unlimited connection reuse
ReuseUnlimited,
/// Maximum number of connection reuse
ReuseLimited(u16),
/// Disable connection reuse, close connection after each transaction
NoReuse,
} }
/// Contains client configuration /// Contains client configuration
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
#[derive(Clone)] #[derive(Clone)]
pub struct SmtpClient { pub struct SmtpTransport {
/// Enable connection reuse
connection_reuse: ConnectionReuseParameters,
/// Name sent during EHLO /// Name sent during EHLO
hello_name: ClientId, hello_name: ClientId,
/// Server we are connecting to
server: String,
/// Port to connect to
port: u16,
/// TLS security configuration
tls: Tls,
/// Optional enforced authentication mechanism
authentication: Vec<Mechanism>,
/// Credentials /// Credentials
credentials: Option<Credentials>, credentials: Option<Credentials>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// TLS security configuration
security: ClientSecurity,
/// Enable UTF8 mailboxes in envelope or headers
smtp_utf8: bool,
/// Optional enforced authentication mechanism
authentication_mechanism: Option<Mechanism>,
/// Force use of the set authentication mechanism even if server does not report to support it
force_set_auth: bool,
/// Define network timeout /// Define network timeout
/// It can be changed later for specific needs (like a different timeout for each SMTP command) /// It can be changed later for specific needs (like a different timeout for each SMTP command)
timeout: Option<Duration>, timeout: Option<Duration>,
/// Connection pool
#[cfg(feature = "r2d2")]
pool: Option<Pool<SmtpTransport>>,
} }
/// Builder for the SMTP `SmtpTransport` /// Builder for the SMTP `SmtpTransport`
impl SmtpClient { impl SmtpTransport {
/// Creates a new SMTP client /// Creates a new SMTP client
/// ///
/// Defaults are: /// Defaults are:
/// ///
/// * No connection reuse
/// * No authentication /// * No authentication
/// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands /// * A 60 seconds timeout for smtp commands
/// * Port 587
/// ///
/// Consider using [`SmtpClient::new_simple`] instead, if possible. /// Consider using [`SmtpTransport::new`] instead, if possible.
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> { pub fn new<T: Into<String>>(server: T) -> Self {
let mut addresses = addr.to_socket_addrs()?; Self {
server: server.into(),
match addresses.next() { port: SUBMISSION_PORT,
Some(addr) => Ok(SmtpClient { hello_name: ClientId::hostname(),
server_addr: addr, credentials: None,
security, authentication: DEFAULT_MECHANISMS.into(),
smtp_utf8: false, timeout: Some(Duration::new(60, 0)),
credentials: None, tls: Tls::None,
connection_reuse: ConnectionReuseParameters::NoReuse, #[cfg(feature = "r2d2")]
#[cfg(feature = "hostname")] pool: None,
hello_name: ClientId::hostname(),
#[cfg(not(feature = "hostname"))]
hello_name: ClientId::new("localhost".to_string()),
authentication_mechanism: None,
force_set_auth: false,
timeout: Some(Duration::new(60, 0)),
}),
None => Err(Error::Resolution),
} }
} }
/// Simple and secure transport, should be used when possible. /// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submissions port, using the provided domain /// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates. /// to validate TLS certificates.
#[cfg(feature = "native-tls")] pub fn relay(relay: &str) -> Result<Self, Error> {
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> { #[cfg(feature = "native-tls")]
let mut tls_builder = TlsConnector::builder(); let mut tls_builder = TlsConnector::builder();
#[cfg(feature = "native-tls")]
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL)); 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());
let tls_parameters = #[cfg(feature = "rustls")]
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new(
(domain, SUBMISSIONS_PORT),
ClientSecurity::Wrapper(tls_parameters),
)
}
#[cfg(feature = "rustls")]
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
let mut tls = ClientConfig::new(); let mut tls = ClientConfig::new();
tls.config #[cfg(feature = "rustls")]
.root_store tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
.add_server_trust_anchors(&TLS_SERVER_ROOTS); #[cfg(feature = "rustls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls);
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls); let new = Self::new(relay)
.port(SUBMISSIONS_PORT)
.tls(Tls::Wrapper(tls_parameters));
SmtpClient::new( #[cfg(feature = "r2d2")]
(domain, SUBMISSIONS_PORT), // Pool with default configuration
ClientSecurity::Wrapper(tls_parameters), // FIXME avoid clone
) let tpool = new.clone();
let new = new.pool(Pool::new(tpool)?);
Ok(new)
} }
/// Creates a new local SMTP client to port 25 /// Creates a new local SMTP client to port 25
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> { ///
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None) /// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
} pub fn unencrypted_localhost() -> Self {
Self::new("localhost").port(SMTP_PORT)
/// Enable SMTPUTF8 if the server supports it
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
self.smtp_utf8 = enabled;
self
} }
/// Set the name used during EHLO /// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> SmtpClient { pub fn hello_name(mut self, name: ClientId) -> Self {
self.hello_name = name; self.hello_name = name;
self self
} }
/// Enable connection reuse /// Set the authentication mechanism to use
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient { pub fn credentials(mut self, credentials: Credentials) -> Self {
self.connection_reuse = parameters; self.credentials = Some(credentials);
self
}
/// Set the client credentials
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
self.credentials = Some(credentials.into());
self self
} }
/// Set the authentication mechanism to use /// Set the authentication mechanism to use
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient { pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
self.authentication_mechanism = Some(mechanism); self.authentication = mechanisms;
self
}
/// Set if the set authentication mechanism should be force
pub fn force_set_auth(mut self, force: bool) -> SmtpClient {
self.force_set_auth = force;
self self
} }
/// Set the timeout duration /// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient { pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
self.timeout = timeout; self.timeout = timeout;
self self
} }
/// Build the SMTP client /// Set the port to use
/// pub fn port(mut self, port: u16) -> Self {
/// It does not connect to the server, but only creates the `SmtpTransport` self.port = port;
pub fn transport(self) -> SmtpTransport { self
SmtpTransport::new(self)
}
}
/// Represents the state of a client
#[derive(Debug)]
struct State {
/// Panic state
pub panic: bool,
/// Connection reuse counter
pub connection_reuse_count: u16,
}
/// Structure that implements the high level SMTP client
#[allow(missing_debug_implementations)]
pub struct SmtpTransport {
/// Information about the server
/// Value is None before EHLO
server_info: Option<ServerInfo>,
/// SmtpTransport variable states
state: State,
/// Information about the client
client_info: SmtpClient,
/// Low level client
client: InnerClient,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.close();
}
return Err(From::from(err))
},
}
})
);
impl<'a> SmtpTransport {
/// Creates a new SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpClient) -> SmtpTransport {
let client = InnerClient::new();
SmtpTransport {
client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
} }
fn connect(&mut self) -> Result<(), Error> { /// Set the TLS settings to use
// Check if the connection is still available pub fn tls(mut self, tls: Tls) -> Self {
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) { self.tls = tls;
self.close(); self
} }
if self.state.connection_reuse_count > 0 { /// Set the TLS settings to use
info!( #[cfg(feature = "r2d2")]
"connection already established to {}", pub fn pool(mut self, pool: Pool<SmtpTransport>) -> Self {
self.client_info.server_addr self.pool = Some(pool);
); self
return Ok(()); }
}
self.client.connect( /// Creates a new connection directly usable to send emails
&self.client_info.server_addr, ///
self.client_info.timeout, /// Handles encryption and authentication
match self.client_info.security { fn connection(&self) -> Result<SmtpConnection, Error> {
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters), let mut conn = SmtpConnection::connect::<(&str, u16)>(
(self.server.as_ref(), self.port),
self.timeout,
&self.hello_name,
match self.tls {
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None, _ => None,
}, },
)?; )?;
self.client.set_timeout(self.client_info.timeout)?; match self.tls {
let _response = self.client.read_response()?;
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
self.ehlo()?;
match (
&self.client_info.security.clone(),
self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::StartTls),
) {
(&ClientSecurity::Required(_), false) => {
return Err(From::from("Could not encrypt connection, aborting"));
}
(&ClientSecurity::Opportunistic(_), false) => (),
(&ClientSecurity::None, _) => (),
(&ClientSecurity::Wrapper(_), _) => (),
#[cfg(any(feature = "native-tls", feature = "rustls"))] #[cfg(any(feature = "native-tls", feature = "rustls"))]
(&ClientSecurity::Opportunistic(ref tls_parameters), true) Tls::Opportunistic(ref tls_parameters) => {
| (&ClientSecurity::Required(ref tls_parameters), true) => { if conn.can_starttls() {
try_smtp!(self.client.command(StarttlsCommand), self); conn.starttls(tls_parameters, &self.hello_name)?;
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
debug!("connection encrypted");
// Send EHLO again
self.ehlo()?;
}
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
(&ClientSecurity::Opportunistic(_), true) | (&ClientSecurity::Required(_), true) => {
// This should never happen as `ClientSecurity` can only be created
// when a TLS library is enabled
unreachable!("TLS support required but not supported");
}
}
if self.client_info.credentials.is_some() {
let mut found = false;
if !self.client_info.force_set_auth {
// Compute accepted mechanism
let accepted_mechanisms = match self.client_info.authentication_mechanism {
Some(mechanism) => vec![mechanism],
None => {
if self.client.is_encrypted() {
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
} else {
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
}
}
};
for mechanism in accepted_mechanisms {
if self
.server_info
.as_ref()
.unwrap()
.supports_auth_mechanism(mechanism)
{
found = true;
try_smtp!(
self.client
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
self
);
break;
}
} }
} else {
try_smtp!(
self.client.auth(
self.client_info.authentication_mechanism.expect(
"force_set_auth set to true, but no authentication mechanism set"
),
self.client_info.credentials.as_ref().unwrap(),
),
self
);
found = true;
} }
#[cfg(any(feature = "native-tls", feature = "rustls"))]
if !found { Tls::Required(ref tls_parameters) => {
info!("No supported authentication mechanisms available"); conn.starttls(tls_parameters, &self.hello_name)?;
} }
_ => (),
} }
Ok(())
}
/// Gets the EHLO response and updates server information match &self.credentials {
fn ehlo(&mut self) -> SmtpResult { Some(credentials) => {
// Extended Hello conn.auth(self.authentication.as_slice(), &credentials)?;
let ehlo_response = try_smtp!( }
self.client.command(EhloCommand::new(ClientId::new( None => (),
self.client_info.hello_name.to_string() }
),)),
self
);
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self)); Ok(conn)
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
Ok(ehlo_response)
}
/// Reset the client state
pub fn close(&mut self) {
// Close the SMTP transaction if needed
self.client.close();
// Reset the client state
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
} }
} }
@@ -441,101 +248,26 @@ impl<'a> Transport<'a> for SmtpTransport {
type Result = SmtpResult; type Result = SmtpResult;
/// Sends an email /// Sends an email
#[cfg_attr( fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result {
feature = "cargo-clippy", #[cfg(feature = "r2d2")]
allow(clippy::match_same_arms, clippy::cyclomatic_complexity) let mut conn: Box<dyn DerefMut<Target = SmtpConnection>> = match self.pool {
)] Some(ref p) => Box::new(p.get()?),
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result { None => Box::new(Box::new(self.connection()?)),
let email_id = Uuid::new_v4(); };
let envelope = envelope; #[cfg(not(feature = "r2d2"))]
let mut conn = self.connection()?;
if !self.client.is_connected() { let result = conn.send(envelope, email)?;
self.connect()?;
}
// Mail #[cfg(feature = "r2d2")]
let mut mail_options = vec![];
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::EightBitMime)
{ {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); if self.pool.is_none() {
} conn.quit()?;
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::SmtpUtfEight)
&& self.client_info.smtp_utf8
{
mail_options.push(MailParameter::SmtpUtfEight);
}
try_smtp!(
self.client
.command(MailCommand::new(envelope.from().cloned(), mail_options,)),
self
);
// Log the mail command
info!(
"{}: from=<{}>",
email_id,
match envelope.from() {
Some(address) => address.to_string(),
None => "".to_string(),
} }
);
// Recipient
for to_address in envelope.to() {
try_smtp!(
self.client
.command(RcptCommand::new(to_address.clone(), vec![])),
self
);
// Log the rcpt command
info!("{}: to=<{}>", email_id, to_address);
} }
#[cfg(not(feature = "r2d2"))]
conn.quit()?;
// Data Ok(result)
try_smtp!(self.client.command(DataCommand), self);
// Message content
let result = self.client.message(email);
if let Ok(ref result) = result {
// Increment the connection reuse counter
self.state.connection_reuse_count += 1;
// Log the message
info!(
"{}: conn_use={}, status=sent ({})",
email_id,
self.state.connection_reuse_count,
result
.message
.iter()
.next()
.unwrap_or(&"no response".to_string())
);
}
// Test if we can reuse the existing connection
match self.client_info.connection_reuse {
ConnectionReuseParameters::ReuseLimited(limit)
if self.state.connection_reuse_count >= limit =>
{
self.close()
}
ConnectionReuseParameters::NoReuse => self.close(),
_ => (),
}
result
} }
} }

View File

@@ -0,0 +1,22 @@
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpTransport};
use r2d2::ManageConnection;
impl ManageConnection for SmtpTransport {
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::Client("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
conn.has_broken()
}
}

View File

@@ -1,37 +0,0 @@
use crate::transport::smtp::{error::Error, ConnectionReuseParameters, SmtpClient, SmtpTransport};
use r2d2::ManageConnection;
pub struct SmtpConnectionManager {
transport_builder: SmtpClient,
}
impl SmtpConnectionManager {
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
Ok(SmtpConnectionManager {
transport_builder: transport_builder
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
})
}
}
impl ManageConnection for SmtpConnectionManager {
type Connection = SmtpTransport;
type Error = Error;
fn connect(&self) -> Result<Self::Connection, Error> {
let mut transport = SmtpTransport::new(self.transport_builder.clone());
transport.connect()?;
Ok(transport)
}
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
if conn.client.is_connected() {
return Ok(());
}
Err(Error::Client("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
conn.state.panic
}
}

View File

@@ -30,7 +30,7 @@ pub type StubResult = Result<(), ()>;
impl<'a> Transport<'a> for StubTransport { impl<'a> Transport<'a> for StubTransport {
type Result = StubResult; type Result = StubResult;
fn send_raw(&mut self, envelope: &Envelope, _email: &[u8]) -> Self::Result { fn send_raw(&self, envelope: &Envelope, _email: &[u8]) -> Self::Result {
info!( info!(
"from=<{}> to=<{:?}>", "from=<{}> to=<{:?}>",
match envelope.from() { match envelope.from() {

View File

@@ -1,7 +1,7 @@
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))] #[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
mod test { mod test {
use lettre::{ use lettre::{
ClientSecurity, Email, EmailAddress, Envelope, SmtpClient, SmtpConnectionManager, Transport, Email, EmailAddress, Envelope, SmtpConnectionManager, SmtpTransport, Tls, Transport,
}; };
use r2d2::Pool; use r2d2::Pool;
use std::{sync::mpsc, thread}; use std::{sync::mpsc, thread};
@@ -20,7 +20,7 @@ mod test {
#[test] #[test]
fn send_one() { fn send_one() {
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap(); let client = SmtpTransport::new("127.0.0.1:2525", Tls::None).unwrap();
let manager = SmtpConnectionManager::new(client).unwrap(); let manager = SmtpConnectionManager::new(client).unwrap();
let pool = Pool::builder().max_size(1).build(manager).unwrap(); let pool = Pool::builder().max_size(1).build(manager).unwrap();
@@ -31,7 +31,7 @@ mod test {
#[test] #[test]
fn send_from_thread() { fn send_from_thread() {
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap(); let client = SmtpTransport::new("127.0.0.1:2525", Tls::None).unwrap();
let manager = SmtpConnectionManager::new(client).unwrap(); let manager = SmtpConnectionManager::new(client).unwrap();
let pool = Pool::builder().max_size(2).build(manager).unwrap(); let pool = Pool::builder().max_size(2).build(manager).unwrap();

View File

@@ -10,7 +10,7 @@ mod test {
#[test] #[test]
fn file_transport() { fn file_transport() {
let mut sender = FileTransport::new(temp_dir()); let sender = FileTransport::new(temp_dir());
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())

View File

@@ -5,7 +5,7 @@ mod test {
#[test] #[test]
fn sendmail_transport_simple() { fn sendmail_transport_simple() {
let mut sender = SendmailTransport::new(); let sender = SendmailTransport::new();
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())

View File

@@ -1,7 +1,7 @@
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
mod test { mod test {
use lettre::{ClientSecurity, Message, SmtpClient, Transport}; use lettre::{Message, SmtpTransport, Transport};
#[test] #[test]
fn smtp_transport_simple() { fn smtp_transport_simple() {
@@ -12,9 +12,8 @@ mod test {
.subject("Happy new year") .subject("Happy new year")
.body("Be happy!") .body("Be happy!")
.unwrap(); .unwrap();
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None) SmtpTransport::new("127.0.0.1")
.unwrap() .port(2525)
.transport()
.send(&email) .send(&email)
.unwrap(); .unwrap();
} }

View File

@@ -2,8 +2,8 @@ use lettre::{transport::stub::StubTransport, Message, Transport};
#[test] #[test]
fn stub_transport() { fn stub_transport() {
let mut sender_ok = StubTransport::new_positive(); let sender_ok = StubTransport::new_positive();
let mut sender_ko = StubTransport::new(Err(())); let sender_ko = StubTransport::new(Err(()));
let email = Message::builder() let email = Message::builder()
.from("NoBody <nobody@domain.tld>".parse().unwrap()) .from("NoBody <nobody@domain.tld>".parse().unwrap())
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap()) .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())