feat(transport-smtp): Add rustls support

This commit is contained in:
Alexis Mousset
2019-11-17 00:51:53 +01:00
parent d2675fab82
commit 29e4829f69
6 changed files with 118 additions and 36 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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<S: Write + Read = NetworkStream> {
/// TCP stream between client and server
/// Value is None before connection
@@ -95,7 +96,7 @@ impl<S: Write + Read> InnerClient<S> {
}
}
impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
impl<S: Connector + Write + Read + Timeout> InnerClient<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.command(QuitCommand);
@@ -108,6 +109,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
}
/// 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<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
// Try to connect
self.set_stream(Connector::connect(&server_addr, timeout, tls_parameters)?);
Ok(())
}

View File

@@ -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<TlsStream<TcpStream>>),
#[cfg(feature = "rustls")]
Tls(rustls::StreamOwned<ClientSession, TcpStream>),
/// Mock stream
Mock(MockStream),
}
@@ -43,6 +64,9 @@ impl NetworkStream {
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
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<usize> {
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<usize> {
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<Duration>,
tls_parameters: Option<&ClientTlsParameters>,
) -> io::Result<Self>;
) -> Result<Self, Error>;
/// 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<Duration>,
tls_parameters: Option<&ClientTlsParameters>,
) -> io::Result<NetworkStream> {
) -> 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| 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,
}
}
}

View File

@@ -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<io::Error> for Error {
}
}
#[cfg(feature = "native-tls")]
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Tls(err)
@@ -116,6 +124,12 @@ impl From<FromUtf8Error> for Error {
}
}
impl From<webpki::InvalidDNSNameError> for Error {
fn from(err: webpki::InvalidDNSNameError) -> Error {
InvalidDNSName(err)
}
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.code.severity {

View File

@@ -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<SmtpClient, Error> {
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<ServerInfo>,
/// 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() {