From fefb5f7978384ce3cb147d4e1221615ba7a965b7 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Sun, 11 Oct 2015 19:56:02 +0200 Subject: [PATCH] smtps with rust-openssl --- Cargo.toml | 3 +- examples/client.rs | 8 ++--- src/client/mod.rs | 42 ++++++++++++++------------ src/client/net.rs | 73 +++++++++++++++++++++++++++++++++++++++++----- src/lib.rs | 8 ++--- src/sender.rs | 26 +++++++++++++---- 6 files changed, 120 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6ee41ba..7cb61f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "smtp" -version = "0.2.1" +version = "0.3.0" description = "Simple SMTP client" readme = "README.md" documentation = "http://amousset.me/rust-smtp/smtp/" @@ -18,6 +18,7 @@ rustc-serialize = "0.3" rust-crypto = "0.2" bufstream = "0.1" email = "0.0" +openssl = "0.6" [dev-dependencies] env_logger = "0.3" diff --git a/examples/client.rs b/examples/client.rs index 5b36e7c..755108a 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -17,7 +17,7 @@ fn main() { let mut threads = Vec::new(); for _ in 1..5 { - + let th_sender = sender.clone(); threads.push(thread::spawn(move || { @@ -27,15 +27,15 @@ fn main() { .body("Hello World!") .subject("Hello") .build(); - + let _ = th_sender.lock().unwrap().send(email); })); } - + for thread in threads { let _ = thread.join(); } - + let email = EmailBuilder::new() .to("user@localhost") .from("user@localhost") diff --git a/src/client/mod.rs b/src/client/mod.rs index 6910630..6a8ff78 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,15 +1,16 @@ //! SMTP client use std::string::String; -use std::net::{SocketAddr, ToSocketAddrs}; +use std::net::ToSocketAddrs; use std::io::{BufRead, Read, Write}; use bufstream::BufStream; +use openssl::ssl::SslContext; use response::ResponseParser; use authentication::Mecanism; use error::{Error, SmtpResult}; -use client::net::{Connector, SmtpStream}; +use client::net::{Connector, NetworkStream}; use {CRLF, MESSAGE_ENDING}; pub mod net; @@ -41,12 +42,10 @@ fn remove_crlf(string: &str) -> String { } /// Structure that implements the SMTP client -pub struct Client { +pub struct Client { /// TCP stream between client and server /// Value is None before connection stream: Option>, - /// Socket we are connecting to - server_addr: SocketAddr, } macro_rules! return_err ( @@ -55,24 +54,18 @@ macro_rules! return_err ( }) ); -impl Client { +impl Client { /// Creates a new SMTP client /// /// It does not connects to the server, but only creates the `Client` - pub fn new(addr: A) -> Result, Error> { - let mut addresses = try!(addr.to_socket_addrs()); - - match addresses.next() { - Some(addr) => Ok(Client { - stream: None, - server_addr: addr, - }), - None => Err(From::from("Could nor resolve hostname")), + pub fn new() -> Client { + Client { + stream: None, } } } -impl Client { +impl Client { /// Closes the SMTP transaction if possible pub fn close(&mut self) { let _ = self.quit(); @@ -80,14 +73,21 @@ impl Client { } /// Connects to the configured server - pub fn connect(&mut self) -> SmtpResult { + pub fn connect(&mut self, addr: &A, ssl_context: Option<&SslContext>) -> 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); } + let mut addresses = try!(addr.to_socket_addrs()); + + let server_addr = match addresses.next() { + Some(addr) => addr, + None => return_err!("Could not resolve hostname", self), + }; + // Try to connect - self.stream = Some(BufStream::new(try!(Connector::connect(&self.server_addr)))); + self.stream = Some(BufStream::new(try!(Connector::connect(&server_addr, ssl_context)))); self.get_reply() } @@ -176,11 +176,13 @@ impl Client { None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")), }; + debug!("CRAM challenge: {}", encoded_challenge); + let cram_response = try!(mecanism.response(username, password, Some(&encoded_challenge))); - self.command(&format!("AUTH CRAM-MD5 {}", cram_response)) + self.command(&format!("{}", cram_response)) } } @@ -211,6 +213,8 @@ impl Client { let mut line = String::new(); try!(self.stream.as_mut().unwrap().read_line(&mut line)); + debug!("Read: {}", escape_crlf(line.as_ref())); + while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) { line.clear(); try!(self.stream.as_mut().unwrap().read_line(&mut line)); diff --git a/src/client/net.rs b/src/client/net.rs index dbd7677..c224fbd 100644 --- a/src/client/net.rs +++ b/src/client/net.rs @@ -1,21 +1,80 @@ //! A trait to represent a stream use std::io; +use std::io::{Read, Write, ErrorKind}; use std::net::SocketAddr; use std::net::TcpStream; +use std::fmt; +use std::fmt::{Debug, Formatter}; +use openssl::ssl::{SslContext, SslStream}; /// A trait for the concept of opening a stream pub trait Connector { /// Opens a connection to the given IP socket - fn connect(addr: &SocketAddr) -> io::Result; + fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result; } -impl Connector for SmtpStream { - fn connect(addr: &SocketAddr) -> io::Result { - TcpStream::connect(addr) +impl Connector for NetworkStream { + fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result { + let tcp_stream = try!(TcpStream::connect(addr)); + + match ssl_context { + Some(context) => match SslStream::new(&context, tcp_stream) { + Ok(stream) => Ok(NetworkStream::Ssl(stream)), + Err(err) => Err(io::Error::new(ErrorKind::Other, err)), + }, + None => Ok(NetworkStream::Plain(tcp_stream)), + } } } -/// Represents an atual SMTP network stream -//Used later for ssl -pub type SmtpStream = TcpStream; +/// TODO +pub enum NetworkStream { + /// TODO + Plain(TcpStream), + /// TODO + Ssl(SslStream), +} + +impl Clone for NetworkStream { + #[inline] + fn clone(&self) -> NetworkStream { + match self { + &NetworkStream::Plain(ref stream) => NetworkStream::Plain(stream.try_clone().unwrap()), + &NetworkStream::Ssl(ref stream) => NetworkStream::Ssl(stream.try_clone().unwrap()), + } + } +} + +impl Debug for NetworkStream { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("NetworkStream(_)") + } +} + +impl Read for NetworkStream { + #[inline] + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match *self { + NetworkStream::Plain(ref mut stream) => stream.read(buf), + NetworkStream::Ssl(ref mut stream) => stream.read(buf), + } + } +} + +impl Write for NetworkStream { + #[inline] + fn write(&mut self, msg: &[u8]) -> io::Result { + match *self { + NetworkStream::Plain(ref mut stream) => stream.write(msg), + NetworkStream::Ssl(ref mut stream) => stream.write(msg), + } + } + #[inline] + fn flush(&mut self) -> io::Result<()> { + match *self { + NetworkStream::Plain(ref mut stream) => stream.flush(), + NetworkStream::Ssl(ref mut stream) => stream.flush(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 33496a3..3c2b7ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,12 +119,11 @@ //! //! ```rust,no_run //! use smtp::client::Client; -//! use smtp::client::net::SmtpStream; //! use smtp::SMTP_PORT; -//! use std::net::TcpStream; +//! use smtp::client::net::NetworkStream; //! -//! let mut email_client: Client = Client::new(("localhost", SMTP_PORT)).unwrap(); -//! let _ = email_client.connect(); +//! let mut email_client: Client = Client::new(); +//! 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"); @@ -143,6 +142,7 @@ extern crate time; extern crate uuid; extern crate email as email_format; extern crate bufstream; +extern crate openssl; mod extension; pub mod client; diff --git a/src/sender.rs b/src/sender.rs index c99b7be..0750b08 100644 --- a/src/sender.rs +++ b/src/sender.rs @@ -3,12 +3,13 @@ use std::string::String; use std::net::{SocketAddr, ToSocketAddrs}; +use openssl::ssl::{SslMethod, SslContext}; + use SMTP_PORT; use extension::{Extension, ServerInfo}; use error::{SmtpResult, Error}; use email::SendableEmail; use client::Client; -use client::net::SmtpStream; use authentication::Mecanism; /// Contains client configuration @@ -25,6 +26,8 @@ pub struct SenderBuilder { credentials: Option<(String, String)>, /// Socket we are connecting to server_addr: SocketAddr, + /// SSL contexyt to use + ssl_context: Option, /// List of authentication mecanism, sorted by priority authentication_mecanisms: Vec, } @@ -35,10 +38,10 @@ impl SenderBuilder { pub fn new(addr: A) -> Result { let mut addresses = try!(addr.to_socket_addrs()); - match addresses.next() { Some(addr) => Ok(SenderBuilder { server_addr: addr, + ssl_context: None, credentials: None, connection_reuse_count_limit: 100, enable_connection_reuse: false, @@ -54,6 +57,17 @@ impl SenderBuilder { SenderBuilder::new(("localhost", SMTP_PORT)) } + /// Add an SSL context and try to use SSL connection + 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()) + } + /// Set the name used during HELO or EHLO pub fn hello_name(mut self, name: &str) -> SenderBuilder { self.hello_name = name.to_string(); @@ -111,7 +125,7 @@ pub struct Sender { /// Information about the client client_info: SenderBuilder, /// Low level client - client: Client, + client: Client, } macro_rules! try_smtp ( @@ -134,7 +148,9 @@ impl Sender { /// /// It does not connects to the server, but only creates the `Sender` pub fn new(builder: SenderBuilder) -> Sender { - let client: Client = Client::new(builder.server_addr).unwrap(); + + let client = Client::new(); + Sender { client: client, server_info: None, @@ -173,7 +189,7 @@ 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()); + try!(self.client.connect(&self.client_info.server_addr, self.client_info.ssl_context.as_ref())); // Log the connection info!("connection established to {}", self.client_info.server_addr);