From 47d6870d93fa1ce3868dc4a93019bd362ff0d569 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Mon, 12 Oct 2015 00:59:39 +0200 Subject: [PATCH] Add STARTTLS support instead of SMTPS --- src/client/mod.rs | 35 ++++++++++++++++------ src/client/net.rs | 14 +++++++++ src/lib.rs | 11 +++---- src/sender.rs | 76 ++++++++++++++++++++++++----------------------- 4 files changed, 85 insertions(+), 51 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 6a8ff78..729d64b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,6 +3,8 @@ use std::string::String; use std::net::ToSocketAddrs; use std::io::{BufRead, Read, Write}; +use std::io; +use std::fmt::Debug; use bufstream::BufStream; use openssl::ssl::SslContext; @@ -65,15 +67,30 @@ impl Client { } } -impl Client { +impl Client { /// Closes the SMTP transaction if possible pub fn close(&mut self) { let _ = self.quit(); self.stream = None; } + /// Sets the underlying stream + pub fn set_stream(&mut self, stream: S) { + self.stream = Some(BufStream::new(stream)); + } + + /// Upgrades the underlying connection to SSL/TLS + pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> { + //let current_stream = self.stream.clone(); + if self.stream.is_some() { + self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context) + } else { + Ok(()) + } + } + /// Connects to the configured server - pub fn connect(&mut self, addr: &A, ssl_context: Option<&SslContext>) -> SmtpResult { + pub fn connect(&mut self, addr: &A) -> SmtpResult { // Connect should not be called when the client is already connected if self.stream.is_some() { return_err!("The connection is already established", self); @@ -87,7 +104,7 @@ impl Client { }; // Try to connect - self.stream = Some(BufStream::new(try!(Connector::connect(&server_addr, ssl_context)))); + self.set_stream(try!(Connector::connect(&server_addr, None))); self.get_reply() } @@ -102,12 +119,7 @@ impl Client { self.send_server(command, CRLF) } - /// Send a HELO command and fills `server_info` - pub fn helo(&mut self, hostname: &str) -> SmtpResult { - self.command(&format!("HELO {}", hostname)) - } - - /// Sends a EHLO command and fills `server_info` + /// Sends a EHLO command pub fn ehlo(&mut self, hostname: &str) -> SmtpResult { self.command(&format!("EHLO {}", hostname)) } @@ -186,6 +198,11 @@ impl Client { } } + /// Sends a STARTTLS command + pub fn starttls(&mut self) -> SmtpResult { + self.command("STARTTLS") + } + /// Sends the message content pub fn message(&mut self, message_content: &str) -> SmtpResult { self.send_server(&escape_dot(message_content), MESSAGE_ENDING) diff --git a/src/client/net.rs b/src/client/net.rs index cc3a150..b0f1c2b 100644 --- a/src/client/net.rs +++ b/src/client/net.rs @@ -12,6 +12,8 @@ use openssl::ssl::{SslContext, SslStream}; pub trait Connector { /// Opens a connection to the given IP socket fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result; + /// Upgrades to TLS connection + fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()>; } impl Connector for NetworkStream { @@ -26,8 +28,20 @@ impl Connector for NetworkStream { None => Ok(NetworkStream::Plain(tcp_stream)), } } + + fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> { + *self = match self.clone() { + NetworkStream::Plain(stream) => match SslStream::new(ssl_context, stream) { + Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream), + Err(err) => return Err(io::Error::new(ErrorKind::Other, err)), + }, + NetworkStream::Ssl(stream) => NetworkStream::Ssl(stream), + }; + Ok(()) + } } + /// Represents the different types of underlying network streams pub enum NetworkStream { /// Plain TCP diff --git a/src/lib.rs b/src/lib.rs index 72c27e0..13e5a13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,10 +9,10 @@ //! //! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152)) //! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms +//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487)) //! //! It will eventually implement the following extensions: //! -//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487)) //! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531)) //! //! ## Architecture @@ -57,6 +57,7 @@ //! use smtp::sender::{Sender, SenderBuilder}; //! use smtp::email::EmailBuilder; //! use smtp::authentication::Mecanism; +//! use smtp::SUBMISSION_PORT; //! //! let mut builder = EmailBuilder::new(); //! builder = builder.to(("user@example.org", "Alias name")); @@ -72,13 +73,13 @@ //! let email = builder.build(); //! //! // Connect to a remote server on a custom port -//! let mut sender = SenderBuilder::new(("server.tld", 10025)).unwrap() +//! let mut sender = SenderBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap() //! // Set the name sent during EHLO/HELO, default is `localhost` //! .hello_name("my.hostname.tld") //! // Add credentials for authentication //! .credentials("username", "password") -//! // Use smtps, you can also specify a specific SSL context with `.ssl_context(context)` -//! .ssl() +//! // Use TLS with STARTTLS, you can also specify a specific SSL context with `.ssl_context(context)` +//! .starttls() //! // Configure accepted authetication mecanisms //! .authentication_mecanisms(vec![Mecanism::CramMd5]) //! // Enable connection reuse @@ -125,7 +126,7 @@ //! use smtp::client::net::NetworkStream; //! //! let mut email_client: Client = Client::new(); -//! let _ = email_client.connect(&("localhost", SMTP_PORT), None); +//! let _ = email_client.connect(&("localhost", SMTP_PORT)); //! 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/sender.rs b/src/sender.rs index 0750b08..49dfc9c 100644 --- a/src/sender.rs +++ b/src/sender.rs @@ -57,15 +57,15 @@ impl SenderBuilder { SenderBuilder::new(("localhost", SMTP_PORT)) } - /// Add an SSL context and try to use SSL connection + /// Use STARTTLS with a specific context pub fn ssl_context(mut self, ssl_context: SslContext) -> SenderBuilder { self.ssl_context = Some(ssl_context); self } - /// Add an SSL context and try to use SSL connection - pub fn ssl(self) -> SenderBuilder { - self.ssl_context(SslContext::new(SslMethod::Sslv23).unwrap()) + /// Require SSL/TLS using STARTTLS + pub fn starttls(self) -> SenderBuilder { + self.ssl_context(SslContext::new(SslMethod::Tlsv1).unwrap()) } /// Set the name used during HELO or EHLO @@ -178,6 +178,19 @@ impl Sender { self.client.close(); } + /// Gets the EHLO response and updates server information + pub fn get_ehlo(&mut self) -> SmtpResult { + // Extended Hello + let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self); + + self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self)); + + // Print server information + debug!("server {}", self.server_info.as_ref().unwrap()); + + Ok(ehlo_response) + } + /// Sends an email pub fn send(&mut self, email: T) -> SmtpResult { // Check if the connection is still available @@ -189,48 +202,37 @@ impl Sender { // 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, self.client_info.ssl_context.as_ref())); + try!(self.client.connect(&self.client_info.server_addr)); // Log the connection info!("connection established to {}", self.client_info.server_addr); - // Extended Hello or Hello if needed - let hello_response = match self.client.ehlo(&self.client_info.hello_name) { - Ok(response) => response, - Err(error) => match error { - Error::PermanentError(ref response) if response.has_code(550) => { - match self.client.helo(&self.client_info.hello_name) { - Ok(response) => response, - Err(error) => try_smtp!(Err(error), self), - } - } - _ => { - try_smtp!(Err(error), self) - } - }, - }; + try!(self.get_ehlo()); - self.server_info = Some(try_smtp!(ServerInfo::from_response(&hello_response), self)); + if self.client_info.ssl_context.is_some() { + try_smtp!(self.client.starttls(), self); - // Print server information - debug!("server {}", self.server_info.as_ref().unwrap()); - } + try!(self.client.upgrade_tls_stream(self.client_info.ssl_context.as_ref().unwrap())); - if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 { - let (username, password) = self.client_info.credentials.clone().unwrap(); - - let mut found = false; - - for mecanism in self.client_info.authentication_mecanisms.clone() { - if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) { - found = true; - let result = self.client.auth(mecanism, &username, &password); - try_smtp!(result, self); - } + try!(self.get_ehlo()); } - if !found { - debug!("No supported authentication mecanisms available"); + if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 { + let (username, password) = self.client_info.credentials.clone().unwrap(); + + let mut found = false; + + for mecanism in self.client_info.authentication_mecanisms.clone() { + if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) { + found = true; + let result = self.client.auth(mecanism, &username, &password); + try_smtp!(result, self); + } + } + + if !found { + debug!("No supported authentication mecanisms available"); + } } }