diff --git a/Cargo.toml b/Cargo.toml index a0acd43..a98df95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ serde = { version = "1", optional = true, features = ["derive"] } serde_json = { version = "1", optional = true } textnonce = { version = "0.7", optional = true } webpki = { version = "0.21", optional = true } +webpki-roots = { version = "0.19", optional = true } [dev-dependencies] criterion = "0.3" @@ -50,10 +51,9 @@ name = "transport_smtp" [features] builder = ["mime", "base64", "hyperx", "textnonce", "quoted_printable"] -connection-pool = ["r2d2"] -default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "native-tls", "builder"] +default = ["file-transport", "smtp-transport", "hostname", "sendmail-transport", "rustls-tls", "builder", "r2d2"] file-transport = ["serde", "serde_json"] -rustls-tls = ["webpki", "rustls"] +rustls-tls = ["webpki", "webpki-roots", "rustls"] sendmail-transport = [] smtp-transport = ["bufstream", "base64", "nom"] unstable = [] diff --git a/benches/transport_smtp.rs b/benches/transport_smtp.rs index d2dd2c6..dfa24db 100644 --- a/benches/transport_smtp.rs +++ b/benches/transport_smtp.rs @@ -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() diff --git a/examples/smtp.rs b/examples/smtp.rs index 6aa5abc..a117a1d 100644 --- a/examples/smtp.rs +++ b/examples/smtp.rs @@ -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); diff --git a/examples/smtp_gmail.rs b/examples/smtp_gmail.rs index b52590d..5b3141e 100644 --- a/examples/smtp_gmail.rs +++ b/examples/smtp_gmail.rs @@ -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); diff --git a/src/address.rs b/src/address.rs index 4f7da17..3a2bed5 100644 --- a/src/address.rs +++ b/src/address.rs @@ -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 diff --git a/src/lib.rs b/src/lib.rs index 0663d67..0fa37dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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)] diff --git a/src/transport/file/mod.rs b/src/transport/file/mod.rs index 042c50b..15ce512 100644 --- a/src/transport/file/mod.rs +++ b/src/transport/file/mod.rs @@ -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(); diff --git a/src/transport/sendmail/mod.rs b/src/transport/sendmail/mod.rs index 1b1a6c2..4ba7617 100644 --- a/src/transport/sendmail/mod.rs +++ b/src/transport/sendmail/mod.rs @@ -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 diff --git a/src/transport/smtp/authentication.rs b/src/transport/smtp/authentication.rs index 64738e2..077ffb0 100644 --- a/src/transport/smtp/authentication.rs +++ b/src/transport/smtp/authentication.rs @@ -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 { diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs index f3b7b0f..7cde3f7 100644 --- a/src/transport/smtp/client/mod.rs +++ b/src/transport/smtp/client/mod.rs @@ -2,18 +2,20 @@ use crate::transport::smtp::{ authentication::{Credentials, Mechanism}, - client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout}, + client::net::{NetworkStream, TlsParameters}, commands::*, error::{Error, SmtpResult}, + extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, response::Response, }; +use crate::Envelope; use bufstream::BufStream; use log::debug; #[cfg(feature = "serde")] 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 +79,27 @@ fn escape_crlf(string: &str) -> String { string.replace("\r\n", "") } +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 InnerClient { +pub struct SmtpConnection { /// TCP stream between client and server /// Value is None before connection - stream: Option>, + stream: BufStream, + /// Panic state + panic: bool, + /// Information about the server + server_info: ServerInfo, } macro_rules! return_err ( @@ -91,102 +108,176 @@ macro_rules! return_err ( }) ); -impl InnerClient { - /// Creates a new SMTP client - /// - /// It does not connects to the server, but only creates the `Client` - pub fn new() -> InnerClient { - InnerClient { stream: None } - } -} - -impl InnerClient { - /// 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) -> 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( - &mut self, - addr: &A, + server: A, timeout: Option, - 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 { + 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 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(()) } + 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) -> 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( - mechanism, - credentials.clone(), - &response, - )?)?; + response = try_smtp!( + self.command(Auth::new_from_response( + mechanism, + credentials.clone(), + &response, + )?), + self + ); } if challenges == 0 { @@ -214,12 +305,8 @@ impl InnerClient { /// 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 +327,7 @@ impl InnerClient { 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 { diff --git a/src/transport/smtp/client/net.rs b/src/transport/smtp/client/net.rs index e4dcc9f..f82e9ab 100644 --- a/src/transport/smtp/client/net.rs +++ b/src/transport/smtp/client/net.rs @@ -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, + tls_parameters: Option<&TlsParameters>, + ) -> Result { + 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) -> 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) -> 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, - tls_parameters: Option<&ClientTlsParameters>, - ) -> Result; - /// 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, - tls_parameters: Option<&ClientTlsParameters>, - ) -> Result { - 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) -> io::Result<()>; - /// Set write timeout for IO calls - fn set_write_timeout(&mut self, duration: Option) -> io::Result<()>; -} - -impl Timeout for NetworkStream { - fn set_read_timeout(&mut self, duration: Option) -> 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) -> 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(()), - } - } -} diff --git a/src/transport/smtp/commands.rs b/src/transport/smtp/commands.rs index b61a499..a4eb498 100644 --- a/src/transport/smtp/commands.rs +++ b/src/transport/smtp/commands.rs @@ -18,30 +18,29 @@ 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 +49,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
, parameters: Vec, } -impl Display for MailCommand { +impl Display for Mail { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!( f, @@ -69,22 +68,22 @@ impl Display for MailCommand { } } -impl MailCommand { +impl Mail { /// Creates a MAIL command - pub fn new(sender: Option
, parameters: Vec) -> MailCommand { - MailCommand { sender, parameters } + pub fn new(sender: Option
, parameters: Vec) -> 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, } -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 +93,10 @@ impl Display for RcptCommand { } } -impl RcptCommand { +impl Rcpt { /// Creates an RCPT command - pub fn new(recipient: Address, parameters: Vec) -> RcptCommand { - RcptCommand { + pub fn new(recipient: Address, parameters: Vec) -> Rcpt { + Rcpt { recipient, parameters, } @@ -107,9 +106,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 +117,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 +128,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 +139,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, } -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 +153,61 @@ impl Display for HelpCommand { } } -impl HelpCommand { +impl Help { /// Creates an HELP command - pub fn new(argument: Option) -> HelpCommand { - HelpCommand { argument } + pub fn new(argument: Option) -> 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 +216,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, response: Option, } -impl Display for AuthCommand { +impl Display for Auth { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let encoded_response = self .response @@ -243,19 +242,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, - ) -> Result { + ) -> Result { 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 +268,7 @@ impl AuthCommand { mechanism: Mechanism, credentials: Credentials, response: &Response, - ) -> Result { + ) -> Result { if !response.has_code(334) { return Err(Error::ResponseParsing("Expecting a challenge")); } @@ -284,7 +283,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 +310,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:\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: SIZE=42\r\n" ); assert_eq!( format!( "{}", - MailCommand::new( + Mail::new( Some(email.clone()), vec![ MailParameter::Size(42), @@ -342,42 +338,42 @@ mod test { "MAIL FROM: SIZE=42 BODY=8BITMIME TEST=value\r\n" ); assert_eq!( - format!("{}", RcptCommand::new(email.clone(), vec![])), + format!("{}", Rcpt::new(email.clone(), vec![])), "RCPT TO:\r\n" ); assert_eq!( - format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])), + format!("{}", Rcpt::new(email.clone(), vec![rcpt_parameter])), "RCPT TO: 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" ); diff --git a/src/transport/smtp/error.rs b/src/transport/smtp/error.rs index f025873..4fad19a 100644 --- a/src/transport/smtp/error.rs +++ b/src/transport/smtp/error.rs @@ -3,11 +3,9 @@ use self::Error::*; use crate::transport::smtp::response::{Response, Severity}; use base64::DecodeError; -#[cfg(feature = "native-tls")] use std::{ error::Error as StdError, - fmt, - fmt::{Display, Formatter}, + fmt::{self, Display, Formatter}, io, string::FromUtf8Error, }; @@ -43,10 +41,11 @@ pub enum Error { /// Invalid hostname #[cfg(feature = "rustls-tls")] InvalidDNSName(webpki::InvalidDNSNameError), + #[cfg(feature = "r2d2")] + Pool(r2d2::Error), } impl Display for Error { - #[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))] fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { match *self { // 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()), #[cfg(feature = "rustls-tls")] InvalidDNSName(ref err) => err.fmt(fmt), + Pool(ref err) => err.fmt(fmt), } } } @@ -129,6 +129,13 @@ impl From for Error { } } +#[cfg(feature = "r2d2")] +impl From for Error { + fn from(err: r2d2::Error) -> Error { + Pool(err) + } +} + impl From for Error { fn from(response: Response) -> Error { match response.code.severity { diff --git a/src/transport/smtp/extension.rs b/src/transport/smtp/extension.rs index 18cc4b2..bb72222 100644 --- a/src/transport/smtp/extension.rs +++ b/src/transport/smtp/extension.rs @@ -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 { + for mechanism in mechanisms { + if self.supports_auth_mechanism(*mechanism) { + return Some(*mechanism); + } + } + None + } } /// A `MAIL FROM` extension parameter diff --git a/src/transport/smtp/mod.rs b/src/transport/smtp/mod.rs index b6adfc2..70d7c98 100644 --- a/src/transport/smtp/mod.rs +++ b/src/transport/smtp/mod.rs @@ -10,32 +10,25 @@ //! * 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)) //! -use crate::Envelope; use crate::{ transport::smtp::{ - authentication::{ - Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS, - }, - client::{net::ClientTlsParameters, InnerClient}, - commands::*, + authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS}, + client::{net::TlsParameters, SmtpConnection}, error::{Error, SmtpResult}, - extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}, + extension::ClientId, }, - Transport, + Envelope, 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::ops::DerefMut; +use std::time::Duration; #[cfg(feature = "rustls")] use webpki_roots::TLS_SERVER_ROOTS; @@ -44,8 +37,8 @@ pub mod client; pub mod commands; pub mod error; pub mod extension; -#[cfg(feature = "connection-pool")] -pub mod r2d2; +#[cfg(feature = "r2d2")] +pub mod pool; pub mod response; pub mod util; @@ -62,378 +55,192 @@ 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, /// Credentials credentials: Option, - /// 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, - /// 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, + /// Connection pool + #[cfg(feature = "r2d2")] + pool: Option>, } /// Builder for the SMTP `SmtpTransport` -impl SmtpClient { +impl SmtpTransport { /// Creates a new SMTP client /// /// Defaults are: /// - /// * No connection reuse /// * No authentication - /// * No SMTPUTF8 support /// * A 60 seconds timeout for smtp commands + /// * Port 587 /// - /// Consider using [`SmtpClient::new_simple`] instead, if possible. - pub fn new(addr: A, security: ClientSecurity) -> Result { - 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), + /// Consider using [`SmtpTransport::new`] instead, if possible. + pub fn new>(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, } } /// 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 { + pub fn relay(relay: &str) -> Result { + #[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()); - 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 { + #[cfg(feature = "rustls")] let mut tls = ClientConfig::new(); - tls.config - .root_store - .add_server_trust_anchors(&TLS_SERVER_ROOTS); + #[cfg(feature = "rustls")] + tls.root_store.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( - (domain, SUBMISSIONS_PORT), - ClientSecurity::Wrapper(tls_parameters), - ) + #[cfg(feature = "r2d2")] + // Pool with default configuration + // 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 - pub fn new_unencrypted_localhost() -> Result { - 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 + /// + /// 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) -> SmtpClient { + pub fn hello_name(mut self, name: ClientId) -> Self { 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>(mut self, credentials: S) -> SmtpClient { - self.credentials = Some(credentials.into()); + /// 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_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; + pub fn authentication(mut self, mechanisms: Vec) -> Self { + self.authentication = mechanisms; self } /// Set the timeout duration - pub fn timeout(mut self, timeout: Option) -> SmtpClient { + pub fn timeout(mut self, timeout: Option) -> Self { 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, - /// 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, - }, - } + /// Set the port to use + pub fn port(mut self, port: u16) -> Self { + self.port = port; + self } - 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(); - } + /// Set the TLS settings to use + pub fn tls(mut self, tls: Tls) -> Self { + self.tls = tls; + self + } - if self.state.connection_reuse_count > 0 { - info!( - "connection already established to {}", - self.client_info.server_addr - ); - return Ok(()); - } + /// Set the TLS settings to use + #[cfg(feature = "r2d2")] + pub fn pool(mut self, pool: Pool) -> Self { + self.pool = Some(pool); + self + } - 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), + /// Creates a new connection directly usable to send emails + /// + /// Handles encryption and authentication + fn connection(&self) -> Result { + 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() { + conn.starttls(tls_parameters, &self.hello_name)?; } - } 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) => { + conn.starttls(tls_parameters, &self.hello_name)?; } + _ => (), } - 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) => { + conn.auth(self.authentication.as_slice(), &credentials)?; + } + 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 +248,26 @@ 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; + fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Self::Result { + #[cfg(feature = "r2d2")] + let mut conn: Box> = match self.pool { + Some(ref p) => Box::new(p.get()?), + None => Box::new(Box::new(self.connection()?)), + }; + #[cfg(not(feature = "r2d2"))] + let mut conn = self.connection()?; - if !self.client.is_connected() { - self.connect()?; - } + let result = conn.send(envelope, email)?; - // Mail - let mut mail_options = vec![]; - - if self - .server_info - .as_ref() - .unwrap() - .supports_feature(Extension::EightBitMime) + #[cfg(feature = "r2d2")] { - 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(), + if self.pool.is_none() { + conn.quit()?; } - ); - - // 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 - 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 + Ok(result) } } diff --git a/src/transport/smtp/pool.rs b/src/transport/smtp/pool.rs new file mode 100644 index 0000000..b373bd6 --- /dev/null +++ b/src/transport/smtp/pool.rs @@ -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() + } + + 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() + } +} diff --git a/src/transport/smtp/r2d2.rs b/src/transport/smtp/r2d2.rs deleted file mode 100644 index 7610a09..0000000 --- a/src/transport/smtp/r2d2.rs +++ /dev/null @@ -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 { - Ok(SmtpConnectionManager { - transport_builder: transport_builder - .connection_reuse(ConnectionReuseParameters::ReuseUnlimited), - }) - } -} - -impl ManageConnection for SmtpConnectionManager { - type Connection = SmtpTransport; - type Error = Error; - - fn connect(&self) -> Result { - 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 - } -} diff --git a/src/transport/stub/mod.rs b/src/transport/stub/mod.rs index 21c13a1..ac41d9a 100644 --- a/src/transport/stub/mod.rs +++ b/src/transport/stub/mod.rs @@ -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() { diff --git a/tests/r2d2_smtp.rs b/tests/r2d2_smtp.rs index 7787868..39b55fa 100644 --- a/tests/r2d2_smtp.rs +++ b/tests/r2d2_smtp.rs @@ -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(); diff --git a/tests/transport_file.rs b/tests/transport_file.rs index 0c9eeaf..a7086dc 100644 --- a/tests/transport_file.rs +++ b/tests/transport_file.rs @@ -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 ".parse().unwrap()) .reply_to("Yuin ".parse().unwrap()) diff --git a/tests/transport_sendmail.rs b/tests/transport_sendmail.rs index c3d626a..30205fc 100644 --- a/tests/transport_sendmail.rs +++ b/tests/transport_sendmail.rs @@ -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 ".parse().unwrap()) .reply_to("Yuin ".parse().unwrap()) diff --git a/tests/transport_smtp.rs b/tests/transport_smtp.rs index d0d6ac7..6d9fde7 100644 --- a/tests/transport_smtp.rs +++ b/tests/transport_smtp.rs @@ -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(); } diff --git a/tests/transport_stub.rs b/tests/transport_stub.rs index 0cfce42..980c421 100644 --- a/tests/transport_stub.rs +++ b/tests/transport_stub.rs @@ -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 ".parse().unwrap()) .reply_to("Yuin ".parse().unwrap())