Remove SmtpClient and make transport immutable

in Transport methods. Also make proper use of
connection pools.
This commit is contained in:
Alexis Mousset
2020-05-01 20:51:07 +02:00
parent dfbe6e9ba2
commit 0604030b91
20 changed files with 507 additions and 713 deletions

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
extern crate lettre;
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpClient, Transport};
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
fn main() {
let email = Message::builder()
@@ -17,10 +17,9 @@ fn main() {
);
// Open a remote connection to gmail
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
let mailer = SmtpTransport::relay("smtp.gmail.com")
.unwrap()
.credentials(creds)
.transport();
.credentials(creds);
// Send the 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 domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
// FIXME avoid unwrap here
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")]
pub use crate::transport::sendmail::SendmailTransport;
#[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"))]
pub use crate::transport::smtp::r2d2::SmtpConnectionManager;
#[cfg(feature = "smtp-transport")]
pub use crate::transport::smtp::{ClientSecurity, SmtpClient, SmtpTransport};
pub use crate::transport::smtp::{SmtpTransport, Tls};
#[cfg(feature = "builder")]
use std::convert::TryFrom;
@@ -115,14 +115,12 @@ pub trait Transport<'a> {
type Result;
/// Sends the email
/// FIXME not mut
fn send(&mut self, message: &Message) -> Self::Result {
fn send(&self, message: &Message) -> Self::Result {
let raw = message.formatted();
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)]

View File

@@ -41,7 +41,7 @@ struct SerializableEmail<'a> {
impl<'a> Transport<'a> for FileTransport {
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 mut file = self.path.clone();

View File

@@ -39,7 +39,7 @@ impl SendmailTransport {
impl<'a> Transport<'a> for SendmailTransport {
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();
// Spawn the sendmail command

View File

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

View File

@@ -2,9 +2,10 @@
use crate::transport::smtp::{
authentication::{Credentials, Mechanism},
client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout},
client::net::{NetworkStream, TlsParameters},
commands::*,
error::{Error, SmtpResult},
extension::{ClientId, Extension, ServerInfo},
response::Response,
};
use bufstream::BufStream;
@@ -13,7 +14,7 @@ use log::debug;
use std::fmt::Debug;
use std::{
fmt::Display,
io::{self, BufRead, Read, Write},
io::{self, BufRead, Write},
net::ToSocketAddrs,
string::String,
time::Duration,
@@ -77,12 +78,27 @@ fn escape_crlf(string: &str) -> String {
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
#[derive(Default)]
pub struct SmtpConnection<S: Write + Read = NetworkStream> {
pub struct SmtpConnection {
/// TCP stream between client and server
/// 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 (
@@ -91,98 +107,139 @@ macro_rules! return_err (
})
);
impl<S: Write + Read> SmtpConnection<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new() -> SmtpConnection<S> {
SmtpConnection { stream: None }
}
}
impl<S: Connector + Write + Read + Timeout> SmtpConnection<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(())
impl SmtpConnection {
pub fn server_info(&self) -> &ServerInfo {
&self.server_info
}
/// Connects to the configured server
///
/// Sends EHLO and parses server information
pub fn connect<A: ToSocketAddrs>(
&mut self,
addr: &A,
server: A,
timeout: Option<Duration>,
tls_parameters: Option<&ClientTlsParameters>,
) -> Result<(), Error> {
// Connect should not be called when the client is already connected
if self.stream.is_some() {
return_err!("The connection is already established", self);
}
let mut addresses = addr.to_socket_addrs()?;
hello_name: &ClientId,
tls_parameters: Option<&TlsParameters>,
) -> Result<SmtpConnection, Error> {
let mut addresses = server.to_socket_addrs()?;
// FIXME try all
let server_addr = match addresses.next() {
Some(addr) => addr,
None => return_err!("Could not resolve hostname", self),
};
debug!("connecting to {}", server_addr);
// Try to connect
self.set_stream(Connector::connect(&server_addr, timeout, tls_parameters)?);
let stream = BufStream::new(NetworkStream::connect(
&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 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
self.ehlo(hello_name)
}
#[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(())
}
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
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
pub fn is_connected(&mut self) -> bool {
self.stream.is_some() && self.command(NoopCommand).is_ok()
pub fn test_connected(&mut self) -> bool {
self.command(Noop).is_ok()
}
/// 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
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) {
challenges -= 1;
response = self.command(AuthCommand::new_from_response(
response = self.command(Auth::new_from_response(
mechanism,
credentials.clone(),
&response,
@@ -214,12 +271,8 @@ impl<S: Connector + Write + Read + Timeout> SmtpConnection<S> {
/// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
if self.stream.is_none() {
return Err(From::from("Connection closed"));
}
self.stream.as_mut().unwrap().write_all(string)?;
self.stream.as_mut().unwrap().flush()?;
self.stream.write_all(string)?;
self.stream.flush()?;
debug!(
"Wrote: {}",
@@ -240,7 +293,7 @@ impl<S: Connector + Write + Read + Timeout> SmtpConnection<S> {
break;
}
// 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
if read_count == 0 {

View File

@@ -18,7 +18,7 @@ use std::{
/// Parameters to use for secure clients
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct ClientTlsParameters {
pub struct TlsParameters {
/// A connector from `native-tls`
#[cfg(feature = "native-tls")]
connector: TlsConnector,
@@ -30,17 +30,17 @@ pub struct ClientTlsParameters {
domain: String,
}
impl ClientTlsParameters {
/// Creates a `ClientTlsParameters`
impl TlsParameters {
/// Creates a `TlsParameters`
#[cfg(feature = "native-tls")]
pub fn new(domain: String, connector: TlsConnector) -> Self {
ClientTlsParameters { connector, domain }
Self { connector, domain }
}
/// Creates a `ClientTlsParameters`
/// Creates a `TlsParameters`
#[cfg(feature = "rustls")]
pub fn new(domain: String, connector: ClientConfig) -> Self {
ClientTlsParameters {
Self {
connector: Box::new(connector),
domain,
}
@@ -87,6 +87,85 @@ impl NetworkStream {
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 {
@@ -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,30 @@ use std::{
/// EHLO command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EhloCommand {
pub struct Ehlo {
client_id: ClientId,
}
impl Display for EhloCommand {
impl Display for Ehlo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[allow(clippy::write_with_newline)]
write!(f, "EHLO {}\r\n", self.client_id)
}
}
impl EhloCommand {
impl Ehlo {
/// Creates a EHLO command
pub fn new(client_id: ClientId) -> EhloCommand {
EhloCommand { client_id }
pub fn new(client_id: ClientId) -> Ehlo {
Ehlo { client_id }
}
}
/// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)]
#[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 {
f.write_str("STARTTLS\r\n")
}
@@ -50,12 +50,12 @@ impl Display for StarttlsCommand {
/// MAIL command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MailCommand {
pub struct Mail {
sender: Option<Address>,
parameters: Vec<MailParameter>,
}
impl Display for MailCommand {
impl Display for Mail {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
@@ -69,22 +69,22 @@ impl Display for MailCommand {
}
}
impl MailCommand {
impl Mail {
/// Creates a MAIL command
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> MailCommand {
MailCommand { sender, parameters }
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
Mail { sender, parameters }
}
}
/// RCPT command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RcptCommand {
pub struct Rcpt {
recipient: Address,
parameters: Vec<RcptParameter>,
}
impl Display for RcptCommand {
impl Display for Rcpt {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters {
@@ -94,10 +94,10 @@ impl Display for RcptCommand {
}
}
impl RcptCommand {
impl Rcpt {
/// Creates an RCPT command
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> RcptCommand {
RcptCommand {
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
Rcpt {
recipient,
parameters,
}
@@ -107,9 +107,9 @@ impl RcptCommand {
/// DATA command
#[derive(PartialEq, Clone, Debug, Copy)]
#[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 {
f.write_str("DATA\r\n")
}
@@ -118,9 +118,9 @@ impl Display for DataCommand {
/// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)]
#[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 {
f.write_str("QUIT\r\n")
}
@@ -129,9 +129,9 @@ impl Display for QuitCommand {
/// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)]
#[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 {
f.write_str("NOOP\r\n")
}
@@ -140,11 +140,11 @@ impl Display for NoopCommand {
/// HELP command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HelpCommand {
pub struct Help {
argument: Option<String>,
}
impl Display for HelpCommand {
impl Display for Help {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("HELP")?;
if self.argument.is_some() {
@@ -154,61 +154,61 @@ impl Display for HelpCommand {
}
}
impl HelpCommand {
impl Help {
/// Creates an HELP command
pub fn new(argument: Option<String>) -> HelpCommand {
HelpCommand { argument }
pub fn new(argument: Option<String>) -> Help {
Help { argument }
}
}
/// VRFY command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VrfyCommand {
pub struct Vrfy {
argument: String,
}
impl Display for VrfyCommand {
impl Display for Vrfy {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[allow(clippy::write_with_newline)]
write!(f, "VRFY {}\r\n", self.argument)
}
}
impl VrfyCommand {
impl Vrfy {
/// Creates a VRFY command
pub fn new(argument: String) -> VrfyCommand {
VrfyCommand { argument }
pub fn new(argument: String) -> Vrfy {
Vrfy { argument }
}
}
/// EXPN command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExpnCommand {
pub struct Expn {
argument: String,
}
impl Display for ExpnCommand {
impl Display for Expn {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[allow(clippy::write_with_newline)]
write!(f, "EXPN {}\r\n", self.argument)
}
}
impl ExpnCommand {
impl Expn {
/// Creates an EXPN command
pub fn new(argument: String) -> ExpnCommand {
ExpnCommand { argument }
pub fn new(argument: String) -> Expn {
Expn { argument }
}
}
/// RSET command
#[derive(PartialEq, Clone, Debug, Copy)]
#[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 {
f.write_str("RSET\r\n")
}
@@ -217,14 +217,14 @@ impl Display for RsetCommand {
/// AUTH command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AuthCommand {
pub struct Auth {
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
response: Option<String>,
}
impl Display for AuthCommand {
impl Display for Auth {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = self
.response
@@ -243,19 +243,19 @@ impl Display for AuthCommand {
}
}
impl AuthCommand {
impl Auth {
/// Creates an AUTH command (from a challenge if provided)
pub fn new(
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
) -> Result<AuthCommand, Error> {
) -> Result<Auth, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_deref())?)
} else {
None
};
Ok(AuthCommand {
Ok(Auth {
mechanism,
credentials,
challenge,
@@ -269,7 +269,7 @@ impl AuthCommand {
mechanism: Mechanism,
credentials: Credentials,
response: &Response,
) -> Result<AuthCommand, Error> {
) -> Result<Auth, Error> {
if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge"));
}
@@ -284,7 +284,7 @@ impl AuthCommand {
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
Ok(AuthCommand {
Ok(Auth {
mechanism,
credentials,
challenge: Some(decoded_challenge),
@@ -311,26 +311,23 @@ mod test {
keyword: "TEST".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!(
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
format!("{}", Mail::new(Some(email.clone()), vec![])),
"MAIL FROM:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", MailCommand::new(None, vec![])),
"MAIL FROM:<>\r\n"
);
assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
assert_eq!(
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"
);
assert_eq!(
format!(
"{}",
MailCommand::new(
Mail::new(
Some(email.clone()),
vec![
MailParameter::Size(42),
@@ -342,42 +339,42 @@ mod test {
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
);
assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![])),
format!("{}", Rcpt::new(email.clone(), vec![])),
"RCPT TO:<test@example.com>\r\n"
);
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"
);
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
assert_eq!(format!("{}", Quit), "QUIT\r\n");
assert_eq!(format!("{}", Data), "DATA\r\n");
assert_eq!(format!("{}", Noop), "NOOP\r\n");
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
assert_eq!(
format!("{}", HelpCommand::new(Some("test".to_string()))),
format!("{}", Help::new(Some("test".to_string()))),
"HELP test\r\n"
);
assert_eq!(
format!("{}", VrfyCommand::new("test".to_string())),
format!("{}", Vrfy::new("test".to_string())),
"VRFY test\r\n"
);
assert_eq!(
format!("{}", ExpnCommand::new("test".to_string())),
format!("{}", Expn::new("test".to_string())),
"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());
assert_eq!(
format!(
"{}",
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
),
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
);
assert_eq!(
format!(
"{}",
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
Auth::new(Mechanism::Login, credentials.clone(), None).unwrap()
),
"AUTH LOGIN\r\n"
);

View File

@@ -52,6 +52,10 @@ impl ClientId {
.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
@@ -86,7 +90,7 @@ impl Display for Extension {
}
/// 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))]
pub struct ServerInfo {
/// Server name
@@ -176,6 +180,16 @@ impl ServerInfo {
self.features
.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

View File

@@ -10,32 +10,29 @@
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
#[cfg(feature = "r2d2")]
use crate::transport::smtp::r2d2::SmtpConnectionManager;
use crate::Envelope;
use crate::{
transport::smtp::{
authentication::{
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
},
client::{net::ClientTlsParameters, SmtpConnection},
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
client::{net::TlsParameters, SmtpConnection},
commands::*,
error::{Error, SmtpResult},
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
extension::{ClientId, Extension, MailBodyParameter, MailParameter},
},
Transport,
};
use log::{debug, info};
#[cfg(feature = "native-tls")]
use native_tls::{Protocol, TlsConnector};
#[cfg(feature = "r2d2")]
use r2d2::Pool;
#[cfg(feature = "rustls")]
use rustls::ClientConfig;
use std::{
net::{SocketAddr, ToSocketAddrs},
time::Duration,
};
use uuid::Uuid;
use std::time::Duration;
#[cfg(feature = "rustls")]
use webpki_roots::TLS_SERVER_ROOTS;
@@ -62,203 +59,49 @@ pub const SUBMISSIONS_PORT: u16 = 465;
/// Accepted protocols by default.
/// 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")]
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub enum ClientSecurity {
pub enum Tls {
/// Insecure connection only (for testing purposes)
None,
/// 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`
Required(ClientTlsParameters),
#[cfg(any(feature = "native-tls", feature = "rustls"))]
Required(TlsParameters),
/// Use TLS wrapped connection
Wrapper(ClientTlsParameters),
}
/// 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,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
Wrapper(TlsParameters),
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpClient {
/// Enable connection reuse
connection_reuse: ConnectionReuseParameters,
pub struct SmtpTransport {
/// Name sent during EHLO
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: 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
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
timeout: Option<Duration>,
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpClient {
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No connection reuse
/// * No authentication
/// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands
///
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
let mut addresses = addr.to_socket_addrs()?;
match addresses.next() {
Some(addr) => Ok(SmtpClient {
server_addr: addr,
security,
smtp_utf8: false,
credentials: None,
connection_reuse: ConnectionReuseParameters::NoReuse,
#[cfg(feature = "hostname")]
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.
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
#[cfg(feature = "native-tls")]
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
let tls_parameters =
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();
tls.config
.root_store
.add_server_trust_anchors(&TLS_SERVER_ROOTS);
let tls_parameters = ClientTlsParameters::new(domain.to_string(), tls);
SmtpClient::new(
(domain, SUBMISSIONS_PORT),
ClientSecurity::Wrapper(tls_parameters),
)
}
/// Creates a new local SMTP client to port 25
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
}
/// 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
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
self.hello_name = name;
self
}
/// Enable connection reuse
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
self.connection_reuse = parameters;
self
}
/// Set the client credentials
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
self.credentials = Some(credentials.into());
self
}
/// Set the authentication mechanism to use
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
self.authentication_mechanism = Some(mechanism);
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
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
self.timeout = timeout;
self
}
/// Build the SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn transport(self) -> SmtpTransport {
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: SmtpConnection,
/// Connection pool
#[cfg(feature = "r2d2")]
pool: Option<Pool>,
}
macro_rules! try_smtp (
@@ -266,174 +109,158 @@ macro_rules! try_smtp (
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.close();
}
$client.abort();
return Err(From::from(err))
},
}
})
);
impl<'a> SmtpTransport {
/// Builder for the SMTP `SmtpTransport`
impl 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 = SmtpConnection::new();
SmtpTransport {
client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
/// 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 {
Self {
server: server.into(),
port: SUBMISSION_PORT,
hello_name: ClientId::hostname(),
credentials: None,
authentication: DEFAULT_MECHANISMS.into(),
timeout: Some(Duration::new(60, 0)),
tls: Tls::None,
#[cfg(feature = "r2d2")]
pool: None,
}
}
fn connect(&mut self) -> Result<(), Error> {
// Check if the connection is still available
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
self.close();
}
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
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());
if self.state.connection_reuse_count > 0 {
info!(
"connection already established to {}",
self.client_info.server_addr
);
return Ok(());
}
#[cfg(feature = "rustls")]
let mut tls = ClientConfig::new();
#[cfg(feature = "rustls")]
tls.config
.root_store
.add_server_trust_anchors(&TLS_SERVER_ROOTS);
#[cfg(feature = "rustls")]
let tls_parameters = TlsParameters::new(relay.to_string(), tls);
self.client.connect(
&self.client_info.server_addr,
self.client_info.timeout,
match self.client_info.security {
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
let new = Self::new(relay)
.port(SUBMISSIONS_PORT)
.tls(Tls::Wrapper(tls_parameters));
#[cfg(feature = "r2d2")]
// Pool with default configuration
let new = new.pool(Pool::new(SmtpConnectionManager))?;
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)
}
/// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> Self {
self.hello_name = name;
self
}
/// Set the authentication mechanism to use
pub fn credentials(mut self, credentials: Credentials) -> Self {
self.credentials = Some(credentials);
self
}
/// Set the authentication mechanism to use
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
self.authentication = mechanisms;
self
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
self.timeout = timeout;
self
}
/// Set the port to use
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
/// Set the TLS settings to use
pub fn tls(mut self, tls: Tls) -> Self {
self.tls = tls;
self
}
/// Set the TLS settings to use
#[cfg(feature = "r2d2")]
pub fn pool(mut self, pool: Pool) -> Self {
self.pool = pool;
self
}
/// Creates a new connection directly usable to send emails
///
/// Handles encryption and authentication
fn connection(&self) -> Result<SmtpConnection, Error> {
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,
},
)?;
self.client.set_timeout(self.client_info.timeout)?;
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(_), _) => (),
match self.tls {
#[cfg(any(feature = "native-tls", feature = "rustls"))]
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
| (&ClientSecurity::Required(ref tls_parameters), true) => {
try_smtp!(self.client.command(StarttlsCommand), self);
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;
}
Tls::Opportunistic(ref tls_parameters) => {
if conn.can_starttls() {
try_smtp!(conn.starttls(tls_parameters, &self.hello_name), conn);
}
} 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;
}
if !found {
info!("No supported authentication mechanisms available");
#[cfg(any(feature = "native-tls", feature = "rustls"))]
Tls::Required(ref tls_parameters) => {
try_smtp!(conn.starttls(tls_parameters, &self.hello_name), conn);
}
_ => (),
}
Ok(())
}
/// Gets the EHLO response and updates server information
fn ehlo(&mut self) -> SmtpResult {
// Extended Hello
let ehlo_response = try_smtp!(
self.client.command(EhloCommand::new(ClientId::new(
self.client_info.hello_name.to_string()
),)),
self
);
match &self.credentials {
Some(credentials) => {
try_smtp!(
conn.auth(self.authentication.as_slice(), &credentials),
conn
);
}
None => (),
}
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
// 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;
Ok(conn)
}
}
@@ -441,101 +268,46 @@ impl<'a> Transport<'a> for SmtpTransport {
type Result = SmtpResult;
/// Sends an email
#[cfg_attr(
feature = "cargo-clippy",
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
)]
fn send_raw(&mut self, envelope: &Envelope, email: &[u8]) -> Self::Result {
let email_id = Uuid::new_v4();
let envelope = envelope;
if !self.client.is_connected() {
self.connect()?;
}
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result {
#[cfg(feature = "r2d2")]
let mut conn = match self.pool {
Some(p) => p.get()?,
None => self.connection()?,
};
#[cfg(not(feature = "r2d2"))]
let mut conn = self.connection()?;
// Mail
let mut mail_options = vec![];
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::EightBitMime)
{
if conn.server_info().supports_feature(Extension::EightBitMime) {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
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(),
}
conn.command(Mail::new(envelope.from().cloned(), mail_options,)),
conn
);
// 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);
try_smtp!(conn.command(Rcpt::new(to_address.clone(), vec![])), conn);
}
// Data
try_smtp!(self.client.command(DataCommand), self);
try_smtp!(conn.command(Data), conn);
// Message content
let result = self.client.message(email);
let result = try_smtp!(conn.message(email), conn);
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()
#[cfg(feature = "r2d2")]
{
if self.pool.is_none() {
try_smtp!(conn.command(Quit), conn);
}
ConnectionReuseParameters::NoReuse => self.close(),
_ => (),
}
#[cfg(not(feature = "r2d2"))]
try_smtp!(conn.command(Quit), conn);
result
Ok(result)
}
}

View File

@@ -1,12 +1,14 @@
use crate::transport::smtp::{error::Error, ConnectionReuseParameters, SmtpClient, SmtpTransport};
use crate::transport::smtp::{
error::Error, ConnectionReuseParameters, SmtpTransport, SmtpTransport,
};
use r2d2::ManageConnection;
pub struct SmtpConnectionManager {
transport_builder: SmtpClient,
transport_builder: SmtpTransport,
}
impl SmtpConnectionManager {
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
pub fn new(transport_builder: SmtpTransport) -> Result<SmtpConnectionManager, Error> {
Ok(SmtpConnectionManager {
transport_builder: transport_builder
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
@@ -25,7 +27,7 @@ impl ManageConnection for SmtpConnectionManager {
}
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
if conn.client.is_connected() {
if conn.client.test_connected() {
return Ok(());
}
Err(Error::Client("is not connected anymore"))

View File

@@ -30,7 +30,7 @@ pub type StubResult = Result<(), ()>;
impl<'a> Transport<'a> for StubTransport {
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!(
"from=<{}> to=<{:?}>",
match envelope.from() {

View File

@@ -1,7 +1,7 @@
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
mod test {
use lettre::{
ClientSecurity, Email, EmailAddress, Envelope, SmtpClient, SmtpConnectionManager, Transport,
Email, EmailAddress, Envelope, SmtpConnectionManager, SmtpTransport, Tls, Transport,
};
use r2d2::Pool;
use std::{sync::mpsc, thread};
@@ -20,7 +20,7 @@ mod test {
#[test]
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 pool = Pool::builder().max_size(1).build(manager).unwrap();
@@ -31,7 +31,7 @@ mod test {
#[test]
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 pool = Pool::builder().max_size(2).build(manager).unwrap();

View File

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

View File

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

View File

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

View File

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