diff --git a/Cargo.toml b/Cargo.toml index d0da2e0..1dd16e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,10 @@ required-features = ["smtp-transport", "native-tls"] name = "smtp_starttls" required-features = ["smtp-transport", "native-tls"] +[[example]] +name = "smtp_selfsigned" +required-features = ["smtp-transport", "native-tls"] + [[example]] name = "tokio02_smtp_tls" required-features = ["smtp-transport", "tokio02", "tokio02-native-tls"] diff --git a/examples/smtp_selfsigned.rs b/examples/smtp_selfsigned.rs new file mode 100644 index 0000000..9a59ac6 --- /dev/null +++ b/examples/smtp_selfsigned.rs @@ -0,0 +1,40 @@ +use std::fs; + +use lettre::{ + transport::smtp::authentication::Credentials, + transport::smtp::client::{Certificate, Tls, TlsParameters}, + Message, SmtpTransport, Transport, +}; + +fn main() { + tracing_subscriber::fmt::init(); + + let email = Message::builder() + .from("NoBody ".parse().unwrap()) + .reply_to("Yuin ".parse().unwrap()) + .to("Hei ".parse().unwrap()) + .subject("Happy new year") + .body("Be happy!") + .unwrap(); + + let pem_cert = fs::read("certificate.pem").unwrap(); + let cert = Certificate::from_pem(&pem_cert).unwrap(); + let mut tls = TlsParameters::builder("smtp.server.com".into()); + tls.add_root_certificate(cert); + let tls = tls.build().unwrap(); + + let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string()); + + // Open a remote connection to gmail + let mailer = SmtpTransport::builder_dangerous("smtp.server.com") + .port(465) + .tls(Tls::Wrapper(tls)) + .credentials(creds) + .build(); + + // Send the email + match mailer.send(&email) { + Ok(_) => println!("Email sent successfully!"), + Err(e) => panic!("Could not send email: {:?}", e), + } +} diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs index 54c403f..61c493f 100644 --- a/src/transport/smtp/client/mod.rs +++ b/src/transport/smtp/client/mod.rs @@ -34,7 +34,7 @@ pub(super) use self::tls::InnerTlsParameters; pub use self::{ connection::SmtpConnection, mock::MockStream, - tls::{Tls, TlsParameters, TlsParametersBuilder}, + tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder}, }; #[cfg(feature = "tokio02")] diff --git a/src/transport/smtp/client/tls.rs b/src/transport/smtp/client/tls.rs index 8e90087..0b61b66 100644 --- a/src/transport/smtp/client/tls.rs +++ b/src/transport/smtp/client/tls.rs @@ -1,18 +1,15 @@ #[cfg(feature = "rustls-tls")] use std::sync::Arc; -#[cfg(any(feature = "native-tls", feature = "rustls-tls"))] -use crate::transport::smtp::error::Error; - #[cfg(feature = "native-tls")] use native_tls::{Protocol, TlsConnector}; #[cfg(feature = "rustls-tls")] -use rustls::{ - Certificate, ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError, -}; +use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError}; #[cfg(feature = "rustls-tls")] use webpki::DNSNameRef; +use crate::transport::smtp::error::Error; + /// Accepted protocols by default. /// This removes TLS 1.0 and 1.1 compared to tls-native defaults. // This is also rustls' default behavior @@ -46,9 +43,10 @@ pub struct TlsParameters { } /// Builder for `TlsParameters` -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct TlsParametersBuilder { domain: String, + root_certs: Vec, accept_invalid_hostnames: bool, accept_invalid_certs: bool, } @@ -58,11 +56,20 @@ impl TlsParametersBuilder { pub fn new(domain: String) -> Self { Self { domain, + root_certs: Vec::new(), accept_invalid_hostnames: false, accept_invalid_certs: false, } } + /// Add a custom root certificate + /// + /// Can be used to safely connect to a server using a self signed certificate, for example. + pub fn add_root_certificate(&mut self, cert: Certificate) -> &mut Self { + self.root_certs.push(cert); + self + } + /// Controls whether certificates with an invalid hostname are accepted /// /// Defaults to `false`. @@ -130,8 +137,13 @@ impl TlsParametersBuilder { #[cfg(feature = "native-tls")] pub fn build_native(self) -> Result { let mut tls_builder = TlsConnector::builder(); + + for cert in self.root_certs { + tls_builder.add_root_certificate(cert.native_tls); + } tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames); tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs); + tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL)); let connector = tls_builder.build()?; Ok(TlsParameters { @@ -146,10 +158,19 @@ impl TlsParametersBuilder { use webpki_roots::TLS_SERVER_ROOTS; let mut tls = ClientConfig::new(); + + for cert in self.root_certs { + for rustls_cert in cert.rustls { + tls.root_store + .add(&rustls_cert) + .map_err(|_| Error::InvalidCertificate)?; + } + } if self.accept_invalid_certs { tls.dangerous() .set_certificate_verifier(Arc::new(InvalidCertsVerifier {})); } + tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS); Ok(TlsParameters { connector: InnerTlsParameters::RustlsTls(tls), @@ -195,6 +216,55 @@ impl TlsParameters { } } +/// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`] +#[derive(Clone)] +#[allow(missing_copy_implementations)] +pub struct Certificate { + #[cfg(feature = "native-tls")] + native_tls: native_tls::Certificate, + #[cfg(feature = "rustls-tls")] + rustls: Vec, +} + +impl Certificate { + /// Create a `Certificate` from a DER encoded certificate + pub fn from_der(der: Vec) -> Result { + #[cfg(feature = "native-tls")] + let native_tls_cert = + native_tls::Certificate::from_der(&der).map_err(|_| Error::InvalidCertificate)?; + + Ok(Self { + #[cfg(feature = "native-tls")] + native_tls: native_tls_cert, + #[cfg(feature = "rustls-tls")] + rustls: vec![rustls::Certificate(der)], + }) + } + + /// Create a `Certificate` from a PEM encoded certificate + pub fn from_pem(pem: &[u8]) -> Result { + #[cfg(feature = "native-tls")] + let native_tls_cert = + native_tls::Certificate::from_pem(pem).map_err(|_| Error::InvalidCertificate)?; + + #[cfg(feature = "rustls-tls")] + let rustls_cert = { + use rustls::internal::pemfile; + use std::io::Cursor; + + let mut pem = Cursor::new(pem); + pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)? + }; + + Ok(Self { + #[cfg(feature = "native-tls")] + native_tls: native_tls_cert, + #[cfg(feature = "rustls-tls")] + rustls: rustls_cert, + }) + } +} + #[cfg(feature = "rustls-tls")] struct InvalidCertsVerifier; @@ -203,7 +273,7 @@ impl ServerCertVerifier for InvalidCertsVerifier { fn verify_server_cert( &self, _roots: &RootCertStore, - _presented_certs: &[Certificate], + _presented_certs: &[rustls::Certificate], _dns_name: DNSNameRef<'_>, _ocsp_response: &[u8], ) -> Result { diff --git a/src/transport/smtp/error.rs b/src/transport/smtp/error.rs index edb311c..fa6bd4a 100644 --- a/src/transport/smtp/error.rs +++ b/src/transport/smtp/error.rs @@ -41,6 +41,8 @@ pub enum Error { /// Invalid hostname #[cfg(feature = "rustls-tls")] InvalidDNSName(webpki::InvalidDNSNameError), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + InvalidCertificate, #[cfg(feature = "r2d2")] Pool(r2d2::Error), } @@ -69,6 +71,8 @@ impl Display for Error { Parsing(ref err) => fmt.write_str(err.description()), #[cfg(feature = "rustls-tls")] InvalidDNSName(ref err) => err.fmt(fmt), + #[cfg(any(feature = "native-tls", feature = "rustls-tls"))] + InvalidCertificate => fmt.write_str("invalid certificate"), #[cfg(feature = "r2d2")] Pool(ref err) => err.fmt(fmt), }