diff --git a/Cargo.toml b/Cargo.toml index 15e1146..09dee61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,43 +12,47 @@ keywords = ["email", "smtp", "mailer"] edition = "2018" [badges] -maintenance = { status = "actively-developed" } is-it-maintained-issue-resolution = { repository = "lettre/lettre" } is-it-maintained-open-issues = { repository = "lettre/lettre" } +maintenance = { status = "actively-developed" } [dependencies] -log = "^0.4" -nom = { version = "^5.0", optional = true } -bufstream = { version = "^0.1", optional = true } -native-tls = { version = "^0.2", optional = true } base64 = { version = "^0.11", optional = true } +bufstream = { version = "^0.1", optional = true } +email = { version = "^0.0.20", optional = true } +fast_chemail = "^0.9" hostname = { version = "^0.2", optional = true } +log = "^0.4" +mime = { version = "^0.3", optional = true } +native-tls = { version = "^0.2", optional = true } +nom = { version = "^5.0", optional = true } +r2d2 = { version = "^0.8", optional = true } +rustls = { version = "^0.16", optional = true } serde = { version = "^1.0", optional = true, features = ["derive"] } serde_json = { version = "^1.0", optional = true } -fast_chemail = "^0.9" -r2d2 = { version = "^0.8", optional = true } -email = { version = "^0.0.20", optional = true } -mime = { version = "^0.3", optional = true } time = { version = "^0.1", optional = true } uuid = { version = "^0.8", features = ["v4"], optional = true } +webpki = { version = "^0.21", optional = true } [dev-dependencies] +criterion = "^0.3" env_logger = "^0.7" glob = "^0.3" -criterion = "^0.3" [[bench]] -name = "transport_smtp" harness = false +name = "transport_smtp" [features] -default = ["file-transport", "smtp-transport", "sendmail-transport", "builder"] builder = ["email", "mime", "time", "base64", "uuid"] -unstable = [] -file-transport = ["serde", "serde_json"] -smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"] -sendmail-transport = [] connection-pool = ["r2d2"] +default = ["file-transport", "smtp-transport", "sendmail-transport", "ssl-rustls", "builder"] +file-transport = ["serde", "serde_json"] +sendmail-transport = [] +smtp-transport = ["bufstream", "base64", "nom", "hostname"] +ssl-native = ["native-tls"] +ssl-rustls = ["rustls", "webpki"] +unstable = [] [[example]] name = "smtp" @@ -56,7 +60,7 @@ required-features = ["smtp-transport"] [[example]] name = "smtp_gmail" -required-features = ["smtp-transport"] +required-features = ["smtp-transport", "native-tls"] [[example]] name = "builder" diff --git a/src/smtp/authentication.rs b/src/smtp/authentication.rs index 9eabac0..8809d23 100644 --- a/src/smtp/authentication.rs +++ b/src/smtp/authentication.rs @@ -8,6 +8,7 @@ use std::fmt::{self, Display, Formatter}; 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] = &[]; /// Convertible to user credentials diff --git a/src/smtp/client/mod.rs b/src/smtp/client/mod.rs index 663c493..7c9a1d5 100644 --- a/src/smtp/client/mod.rs +++ b/src/smtp/client/mod.rs @@ -1,7 +1,8 @@ //! SMTP client use crate::smtp::authentication::{Credentials, Mechanism}; -use crate::smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout}; +use crate::smtp::client::net::ClientTlsParameters; +use crate::smtp::client::net::{Connector, NetworkStream, Timeout}; use crate::smtp::commands::*; use crate::smtp::error::{Error, SmtpResult}; use crate::smtp::response::Response; @@ -73,7 +74,7 @@ fn escape_crlf(string: &str) -> String { } /// Structure that implements the SMTP client -#[derive(Debug, Default)] +#[derive(Default)] pub struct InnerClient { /// TCP stream between client and server /// Value is None before connection @@ -95,7 +96,7 @@ impl InnerClient { } } -impl InnerClient { +impl InnerClient { /// Closes the SMTP transaction if possible pub fn close(&mut self) { let _ = self.command(QuitCommand); @@ -108,6 +109,7 @@ impl InnerClient { } /// Upgrades the underlying connection to SSL/TLS + #[cfg(feature = "native-tls")] pub fn upgrade_tls_stream(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> { match self.stream { Some(ref mut stream) => stream.get_mut().upgrade_tls(tls_parameters), @@ -155,6 +157,7 @@ impl InnerClient { // Try to connect self.set_stream(Connector::connect(&server_addr, timeout, tls_parameters)?); + Ok(()) } diff --git a/src/smtp/client/net.rs b/src/smtp/client/net.rs index 9b1ad1a..9f3d4c6 100644 --- a/src/smtp/client/net.rs +++ b/src/smtp/client/net.rs @@ -1,9 +1,16 @@ //! A trait to represent a stream use crate::smtp::client::mock::MockStream; +use crate::smtp::error::Error; +#[cfg(feature = "native-tls")] use native_tls::{Protocol, TlsConnector, TlsStream}; -use std::io::{self, ErrorKind, Read, Write}; +#[cfg(feature = "rustls")] +use rustls::{ClientConfig, ClientSession}; +#[cfg(feature = "native-tls")] +use std::io::ErrorKind; +use std::io::{self, Read, Write}; use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream}; +use std::sync::Arc; use std::time::Duration; /// Parameters to use for secure clients @@ -11,29 +18,43 @@ use std::time::Duration; #[allow(missing_debug_implementations)] pub struct ClientTlsParameters { /// A connector from `native-tls` + #[cfg(feature = "native-tls")] pub connector: TlsConnector, + /// A client from `rustls` + #[cfg(feature = "rustls")] + pub connector: ClientConfig, /// The domain name which is expected in the TLS certificate from the server pub domain: String, } impl ClientTlsParameters { /// Creates a `ClientTlsParameters` - pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters { + #[cfg(feature = "native-tls")] + pub fn new(domain: String, connector: TlsConnector) -> Self { + ClientTlsParameters { connector, domain } + } + + /// Creates a `ClientTlsParameters` + #[cfg(feature = "rustls")] + pub fn new(domain: String, connector: ClientConfig) -> Self { ClientTlsParameters { connector, domain } } } /// Accepted protocols by default. /// This removes TLS 1.0 and 1.1 compared to tls-native defaults. +#[cfg(feature = "native-tls")] pub const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12; -#[derive(Debug)] /// Represents the different types of underlying network streams pub enum NetworkStream { /// Plain TCP stream Tcp(TcpStream), /// Encrypted TCP stream + #[cfg(feature = "native-tls")] Tls(Box>), + #[cfg(feature = "rustls")] + Tls(rustls::StreamOwned), /// Mock stream Mock(MockStream), } @@ -43,6 +64,9 @@ impl NetworkStream { pub fn peer_addr(&self) -> io::Result { match *self { NetworkStream::Tcp(ref s) => s.peer_addr(), + #[cfg(feature = "native-tls")] + NetworkStream::Tls(ref s) => s.get_ref().peer_addr(), + #[cfg(feature = "rustls")] NetworkStream::Tls(ref s) => s.get_ref().peer_addr(), NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new( Ipv4Addr::new(127, 0, 0, 1), @@ -55,6 +79,9 @@ impl NetworkStream { pub fn shutdown(&self, how: Shutdown) -> io::Result<()> { match *self { NetworkStream::Tcp(ref s) => s.shutdown(how), + #[cfg(feature = "native-tls")] + NetworkStream::Tls(ref s) => s.get_ref().shutdown(how), + #[cfg(feature = "rustls")] NetworkStream::Tls(ref s) => s.get_ref().shutdown(how), NetworkStream::Mock(_) => Ok(()), } @@ -65,6 +92,9 @@ impl Read for NetworkStream { fn read(&mut self, buf: &mut [u8]) -> io::Result { match *self { NetworkStream::Tcp(ref mut s) => s.read(buf), + #[cfg(feature = "native-tls")] + NetworkStream::Tls(ref mut s) => s.read(buf), + #[cfg(feature = "rustls")] NetworkStream::Tls(ref mut s) => s.read(buf), NetworkStream::Mock(ref mut s) => s.read(buf), } @@ -75,6 +105,9 @@ impl Write for NetworkStream { fn write(&mut self, buf: &[u8]) -> io::Result { match *self { NetworkStream::Tcp(ref mut s) => s.write(buf), + #[cfg(feature = "native-tls")] + NetworkStream::Tls(ref mut s) => s.write(buf), + #[cfg(feature = "rustls")] NetworkStream::Tls(ref mut s) => s.write(buf), NetworkStream::Mock(ref mut s) => s.write(buf), } @@ -83,6 +116,9 @@ impl Write for NetworkStream { fn flush(&mut self) -> io::Result<()> { match *self { NetworkStream::Tcp(ref mut s) => s.flush(), + #[cfg(feature = "native-tls")] + NetworkStream::Tls(ref mut s) => s.flush(), + #[cfg(feature = "rustls")] NetworkStream::Tls(ref mut s) => s.flush(), NetworkStream::Mock(ref mut s) => s.flush(), } @@ -96,9 +132,9 @@ pub trait Connector: Sized { addr: &SocketAddr, timeout: Option, tls_parameters: Option<&ClientTlsParameters>, - ) -> io::Result; + ) -> Result; /// Upgrades to TLS connection - fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()>; + fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> Result<(), Error>; /// Is the NetworkStream encrypted fn is_encrypted(&self) -> bool; } @@ -108,25 +144,35 @@ impl Connector for NetworkStream { addr: &SocketAddr, timeout: Option, tls_parameters: Option<&ClientTlsParameters>, - ) -> io::Result { + ) -> 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| io::Error::new(ErrorKind::Other, e)), + #[cfg(feature = "rustls")] + Some(context) => { + let domain = webpki::DNSNameRef::try_from_ascii_str(&context.domain)?; + + Ok(NetworkStream::Tls(rustls::StreamOwned::new( + ClientSession::new(&Arc::new(context.connector.clone()), domain), + tcp_stream, + ))) + } None => Ok(NetworkStream::Tcp(tcp_stream)), } } - #[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))] - fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> { + 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()) @@ -134,19 +180,25 @@ impl Connector for NetworkStream { Ok(tls_stream) => NetworkStream::Tls(Box::new(tls_stream)), Err(err) => return Err(io::Error::new(ErrorKind::Other, err)), }, - NetworkStream::Tls(_) => return Ok(()), - NetworkStream::Mock(_) => return Ok(()), + #[cfg(feature = "rustls")] + NetworkStream::Tcp(ref mut stream) => { + let domain = webpki::DNSNameRef::try_from_ascii_str(&tls_parameters.domain)?; + + NetworkStream::Tls(rustls::StreamOwned::new( + ClientSession::new(&Arc::new(tls_parameters.connector.clone()), domain), + stream.try_clone().unwrap(), + )) + } + NetworkStream::Tls(_) | NetworkStream::Mock(_) => return Ok(()), }; Ok(()) } - #[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))] fn is_encrypted(&self) -> bool { match *self { - NetworkStream::Tcp(_) => false, + NetworkStream::Tcp(_) | NetworkStream::Mock(_) => false, NetworkStream::Tls(_) => true, - NetworkStream::Mock(_) => false, } } } diff --git a/src/smtp/error.rs b/src/smtp/error.rs index ace85a9..9c47ba7 100644 --- a/src/smtp/error.rs +++ b/src/smtp/error.rs @@ -3,6 +3,7 @@ use self::Error::*; use crate::smtp::response::{Response, Severity}; use base64::DecodeError; +#[cfg(feature = "native-tls")] use native_tls; use nom; use std::error::Error as StdError; @@ -35,9 +36,12 @@ pub enum Error { /// IO error Io(io::Error), /// TLS error + #[cfg(feature = "native-tls")] Tls(native_tls::Error), /// Parsing error Parsing(nom::error::ErrorKind), + /// Invalid hostname + InvalidDNSName(webpki::InvalidDNSNameError), } impl Display for Error { @@ -66,8 +70,10 @@ impl StdError for Error { Resolution => "could not resolve hostname", Client(err) => err, Io(ref err) => err.description(), + #[cfg(feature = "native-tls")] Tls(ref err) => err.description(), Parsing(ref err) => err.description(), + InvalidDNSName(ref err) => err.description(), } } @@ -76,6 +82,7 @@ impl StdError for Error { ChallengeParsing(ref err) => Some(&*err), Utf8Parsing(ref err) => Some(&*err), Io(ref err) => Some(&*err), + #[cfg(feature = "native-tls")] Tls(ref err) => Some(&*err), _ => None, } @@ -88,6 +95,7 @@ impl From for Error { } } +#[cfg(feature = "native-tls")] impl From for Error { fn from(err: native_tls::Error) -> Error { Tls(err) @@ -116,6 +124,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: webpki::InvalidDNSNameError) -> Error { + InvalidDNSName(err) + } +} + impl From for Error { fn from(response: Response) -> Error { match response.code.severity { diff --git a/src/smtp/mod.rs b/src/smtp/mod.rs index 9ebca28..c78ba0a 100644 --- a/src/smtp/mod.rs +++ b/src/smtp/mod.rs @@ -17,6 +17,8 @@ use crate::smtp::authentication::{ Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS, }; use crate::smtp::client::net::ClientTlsParameters; +#[cfg(feature = "native-tls")] +// TODO RUSTLS use crate::smtp::client::net::DEFAULT_TLS_MIN_PROTOCOL; use crate::smtp::client::InnerClient; use crate::smtp::commands::*; @@ -24,6 +26,7 @@ use crate::smtp::error::{Error, SmtpResult}; use crate::smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}; use crate::{SendableEmail, Transport}; use log::{debug, info}; +#[cfg(feature = "native-tls")] use native_tls::TlsConnector; use std::net::{SocketAddr, ToSocketAddrs}; use std::time::Duration; @@ -134,6 +137,7 @@ impl SmtpClient { /// 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 { let mut tls_builder = TlsConnector::builder(); tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL)); @@ -215,7 +219,7 @@ struct State { #[allow(missing_debug_implementations)] pub struct SmtpTransport { /// Information about the server - /// Value is None before HELO/EHLO + /// Value is None before EHLO server_info: Option, /// SmtpTransport variable states state: State, @@ -302,16 +306,20 @@ impl<'a> SmtpTransport { (&ClientSecurity::Opportunistic(_), false) => (), (&ClientSecurity::None, _) => (), (&ClientSecurity::Wrapper(_), _) => (), + #[cfg(feature = "native-tls")] (&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(feature = "native-tls"))] + (&ClientSecurity::Opportunistic(_), true) | (&ClientSecurity::Required(_), true) => { + // FIXME dedicated error variant + return Err(From::from("Encryption required but no TLS support enabled")); + } } if self.client_info.credentials.is_some() {