From 9f177047f8ebb97c23b599c7d6e665e7399fda39 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Tue, 27 Oct 2015 22:38:45 +0100 Subject: [PATCH] Add SMTPS support --- src/lib.rs | 2 +- src/transport/smtp/authentication.rs | 13 +++++---- src/transport/smtp/client/mod.rs | 11 +++++--- src/transport/smtp/mod.rs | 40 +++++++++++++++++++++++----- 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a7e6612..0a03be8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,7 +138,7 @@ //! use lettre::transport::smtp::client::net::NetworkStream; //! //! let mut email_client: Client = Client::new(); -//! let _ = email_client.connect(&("localhost", SMTP_PORT)); +//! let _ = email_client.connect(&("localhost", SMTP_PORT), None); //! let _ = email_client.ehlo("my_hostname"); //! let _ = email_client.mail("user@example.com", None); //! let _ = email_client.rcpt("user@example.org"); diff --git a/src/transport/smtp/authentication.rs b/src/transport/smtp/authentication.rs index a59eeaf..b77ab52 100644 --- a/src/transport/smtp/authentication.rs +++ b/src/transport/smtp/authentication.rs @@ -53,7 +53,8 @@ impl Mechanism { match *self { Mechanism::Plain => { match challenge { - Some(_) => Err(Error::ClientError("This mechanism does not expect a challenge")), + Some(_) => + Err(Error::ClientError("This mechanism does not expect a challenge")), None => Ok(format!("{}{}{}{}", NUL, username, NUL, password) .as_bytes() .to_base64(base64::STANDARD)), @@ -62,7 +63,8 @@ impl Mechanism { Mechanism::CramMd5 => { let encoded_challenge = match challenge { Some(challenge) => challenge, - None => return Err(Error::ClientError("This mechanism does expect a challenge")), + None => + return Err(Error::ClientError("This mechanism does expect a challenge")), }; let decoded_challenge = match encoded_challenge.from_base64() { @@ -99,9 +101,10 @@ mod test { let mechanism = Mechanism::CramMd5; assert_eq!(mechanism.response("alice", - "wonderland", - Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")) - .unwrap(), + "wonderland", + Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=\ + =")) + .unwrap(), "YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA="); assert!(mechanism.response("alice", "wonderland", Some("tést")).is_err()); assert!(mechanism.response("alice", "wonderland", None).is_err()); diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs index febbb9d..a7712c4 100644 --- a/src/transport/smtp/client/mod.rs +++ b/src/transport/smtp/client/mod.rs @@ -88,7 +88,10 @@ impl Client { } /// Connects to the configured server - pub fn connect(&mut self, addr: &A) -> EmailResult { + pub fn connect(&mut self, + addr: &A, + ssl_context: Option<&SslContext>) + -> EmailResult { // Connect should not be called when the client is already connected if self.stream.is_some() { return_err!("The connection is already established", self); @@ -102,7 +105,7 @@ impl Client { }; // Try to connect - self.set_stream(try!(Connector::connect(&server_addr, None))); + self.set_stream(try!(Connector::connect(&server_addr, ssl_context))); self.get_reply() } @@ -189,8 +192,8 @@ impl Client { debug!("CRAM challenge: {}", encoded_challenge); let cram_response = try!(mechanism.response(username, - password, - Some(&encoded_challenge))); + password, + Some(&encoded_challenge))); self.command(&format!("{}", cram_response)) } diff --git a/src/transport/smtp/mod.rs b/src/transport/smtp/mod.rs index c658511..ade37b5 100644 --- a/src/transport/smtp/mod.rs +++ b/src/transport/smtp/mod.rs @@ -47,9 +47,17 @@ pub static NUL: &'static str = "\0"; /// TLS security level #[derive(Debug)] pub enum SecurityLevel { - /// Only send an email on encrypted connection + /// Use a TLS wrapped connection + /// + /// Non RFC-compliant, should only be used if the server does not support STARTTLS. + EncryptedWrapper, + /// Only send an email on encrypted connection (with STARTTLS) + /// + /// Recommended mode, prevents MITM when used with verified certificates. AlwaysEncrypt, - /// Use TLS when available + /// Use TLS when available (with STARTTLS) + /// + /// Default mode. Opportunistic, /// Never use TLS NeverEncrypt, @@ -112,13 +120,29 @@ impl SmtpTransportBuilder { self } - /// Require SSL/TLS using STARTTLS + /// Set the security level for SSL/TLS pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder { self.security_level = level; self } /// Require SSL/TLS using STARTTLS + /// + /// Incompatible with `ssl_wrapper()`` + pub fn encrypt(mut self) -> SmtpTransportBuilder { + self.security_level = SecurityLevel::AlwaysEncrypt; + self + } + + /// Require SSL/TLS using STARTTLS + /// + /// Incompatible with `encrypt()` + pub fn ssl_wrapper(mut self) -> SmtpTransportBuilder { + self.security_level = SecurityLevel::EncryptedWrapper; + self + } + + /// Enable SMTPUTF8 if the server supports it pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder { self.smtp_utf8 = enabled; self @@ -260,10 +284,13 @@ impl EmailTransport for SmtpTransport { } } - // If there is a usable connection, test if the server answers and hello has - // been sent if self.state.connection_reuse_count == 0 { - try!(self.client.connect(&self.client_info.server_addr)); + try!(self.client.connect(&self.client_info.server_addr, + match &self.client_info.security_level { + &SecurityLevel::EncryptedWrapper => + Some(&self.client_info.ssl_context), + _ => None, + })); // Log the connection info!("connection established to {}", self.client_info.server_addr); @@ -276,6 +303,7 @@ impl EmailTransport for SmtpTransport { return Err(From::from("Could not encrypt connection, aborting")), (&SecurityLevel::Opportunistic, false) => (), (&SecurityLevel::NeverEncrypt, _) => (), + (&SecurityLevel::EncryptedWrapper, _) => (), (_, true) => { try_smtp!(self.client.starttls(), self); try_smtp!(self.client.upgrade_tls_stream(&self.client_info.ssl_context),