feat(transport-smtp): Add rustls support
This commit is contained in:
38
Cargo.toml
38
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user