diff --git a/README.md b/README.md index 52cac66..53dd309 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ rust-smtp [![Build Status](https://travis-ci.org/amousset/rust-smtp.svg?branch=master)](https://travis-ci.org/amousset/rust-smtp) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/amousset/rust-smtp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ========= -This library implements an SMTP library and a simple SMTP client. +This library implements a simple SMTP client. See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information. Rust versions @@ -12,7 +12,7 @@ This library is designed for Rust 1.0.0-nightly (master). Install ------- -If you're using the library in a program, just add these lines to your `Cargo.toml`: +To use this library, add the following to your `Cargo.toml`: ```toml [dependencies] @@ -40,7 +40,7 @@ Run `cargo run --example client -- -h` to get a list of available options. Tests ----- -You can build and run the tests with `cargo test`. The client does not have tests for now. +You can build and run the tests with `cargo test`. Documentation ------------- diff --git a/examples/client.rs b/examples/client.rs index d2a7a8b..5fc55f7 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -21,25 +21,24 @@ use std::string::String; use std::env; use getopts::{optopt, optflag, getopts, OptGroup, usage}; -use smtp::client::Client; +use smtp::client::ClientBuilder; use smtp::error::SmtpResult; use smtp::mailer::Email; -fn sendmail(source_address: &str, recipient_addresses: &[&str], message: &str, subject: &str, - server: &str, port: Port, my_hostname: &str, number: u16) -> SmtpResult { +fn sendmail(source_address: String, recipient_addresses: Vec, message: String, subject: String, + server: String, port: Port, my_hostname: String, number: u16) -> SmtpResult { let mut email = Email::new(); for destination in recipient_addresses.iter() { - email.to(*destination); + email.to(destination.as_slice()); } - email.from(source_address); - email.body(message); - email.subject(subject); + email.from(source_address.as_slice()); + email.body(message.as_slice()); + email.subject(subject.as_slice()); email.date_now(); - let mut client: Client = Client::new((server, port)); - client.set_hello_name(my_hostname); - client.set_enable_connection_reuse(true); + let mut client = ClientBuilder::new((server.as_slice(), port)).hello_name(my_hostname) + .enable_connection_reuse(true).build(); for _ in range(1, number) { let _ = client.send(email.clone()); @@ -106,7 +105,7 @@ fn main() { let mut recipients = Vec::new(); for recipient in recipients_str.split(' ') { - recipients.push(recipient); + recipients.push(recipient.to_string()); } let mut message = String::new(); @@ -119,32 +118,32 @@ fn main() { match sendmail( // sender - matches.opt_str("r").unwrap().as_slice(), + matches.opt_str("r").unwrap().clone(), // recipients - recipients.as_slice(), + recipients, // message content - message.as_slice(), + message, // subject match matches.opt_str("s") { - Some(ref subject) => subject.as_slice(), - None => "(empty subject)" + Some(ref subject) => subject.clone(), + None => "(empty subject)".to_string(), }, // server match matches.opt_str("a") { - Some(ref server) => server.as_slice(), - None => "localhost" + Some(ref server) => server.clone(), + None => "localhost".to_string(), }, // port match matches.opt_str("p") { Some(port) => port.as_slice().parse::().unwrap(), - None => 25 + None => 25, }, // my hostname match matches.opt_str("m") { - Some(ref my_hostname) => my_hostname.as_slice(), - None => "localhost" + Some(ref my_hostname) => my_hostname.clone(), + None => "localhost".to_string(), }, - // subject + // number of copies match matches.opt_str("n") { Some(ref n) => n.as_slice().parse::().unwrap(), None => 1, diff --git a/src/client/mod.rs b/src/client/mod.rs index 0b194e5..d325f66 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -37,19 +37,70 @@ mod server_info; mod connecter; mod stream; -/// Represents the configuration of a client -#[derive(Debug)] -struct Configuration { +/// Contains client configuration +pub struct ClientBuilder { /// Maximum connection reuse /// /// Zero means no limitation - pub connection_reuse_count_limit: u16, + connection_reuse_count_limit: u16, /// Enable connection reuse - pub enable_connection_reuse: bool, - /// Maximum line length - pub line_length_limit: u16, + enable_connection_reuse: bool, /// Name sent during HELO or EHLO - pub hello_name: String, + hello_name: String, + /// Credentials + credentials: Option<(String, String)>, + /// Socket we are connecting to + server_addr: SocketAddr, +} + +/// Builder for the SMTP Client +impl ClientBuilder { + /// Creates a new local SMTP client + pub fn new(addr: A) -> ClientBuilder { + ClientBuilder { + server_addr: addr.to_socket_addr().ok().expect("could not parse server address"), + credentials: None, + connection_reuse_count_limit: 100, + enable_connection_reuse: false, + hello_name: "localhost".to_string(), + } + } + + /// Creates a new local SMTP client to port 25 + pub fn localhost() -> ClientBuilder { + ClientBuilder::new(("localhost", SMTP_PORT)) + } + + /// Set the name used during HELO or EHLO + pub fn hello_name(mut self, name: String) -> ClientBuilder { + self.hello_name = name; + self + } + + /// Enable connection reuse + pub fn enable_connection_reuse(mut self, enable: bool) -> ClientBuilder { + self.enable_connection_reuse = enable; + self + } + + /// Set the maximum number of emails sent using one connection + pub fn connection_reuse_count_limit(mut self, limit: u16) -> ClientBuilder { + self.connection_reuse_count_limit = limit; + self + } + + /// Set the client credentials + pub fn credentials(mut self, username: String, password: String) -> ClientBuilder { + self.credentials = Some((username, password)); + self + } + + /// Build the SMTP client + /// + /// It does not connects to the server, but only creates the `Client` + pub fn build(self) -> Client { + Client::new(self) + } } /// Represents the state of a client @@ -61,31 +112,18 @@ struct State { pub connection_reuse_count: u16, } -/// Represents the credentials -#[derive(Debug, Clone)] -struct Credentials { - /// Username - pub username: String, - /// Password - pub password: String, -} - /// Structure that implements the SMTP 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, /// Information about the server /// Value is None before HELO/EHLO server_info: Option, /// Client variable states state: State, - /// Configuration of the client - configuration: Configuration, - /// Client credentials - credentials: Option, + /// Information about the client + client_info: ClientBuilder, } macro_rules! try_smtp ( @@ -127,54 +165,17 @@ impl Client { /// Creates a new SMTP client /// /// It does not connects to the server, but only creates the `Client` - pub fn new(addr: A) -> Client { + pub fn new(builder: ClientBuilder) -> Client { Client{ stream: None, - server_addr: addr.to_socket_addr().ok().expect("could not parse server address"), server_info: None, - configuration: Configuration { - connection_reuse_count_limit: 100, - enable_connection_reuse: false, - line_length_limit: 998, - hello_name: "localhost".to_string(), - }, + client_info: builder, state: State { panic: false, connection_reuse_count: 0, }, - credentials: None, } } - - /// Creates a new local SMTP client to port 25 - /// - /// It does not connects to the server, but only creates the `Client` - pub fn localhost() -> Client { - Client::new(("localhost", SMTP_PORT)) - } - - /// Set the name used during HELO or EHLO - pub fn set_hello_name(&mut self, name: &str) { - self.configuration.hello_name = name.to_string() - } - - /// Set the maximum number of emails sent using one connection - pub fn set_enable_connection_reuse(&mut self, enable: bool) { - self.configuration.enable_connection_reuse = enable - } - - /// Set the maximum number of emails sent using one connection - pub fn set_connection_reuse_count_limit(&mut self, count: u16) { - self.configuration.connection_reuse_count_limit = count - } - - /// Set the client credentials - pub fn set_credentials(&mut self, username: &str, password: &str) { - self.credentials = Some(Credentials { - username: username.to_string(), - password: password.to_string(), - }) - } } impl Client { @@ -230,15 +231,17 @@ impl Client { } // TODO: Use PLAIN AUTH in encrypted connections, CRAM-MD5 otherwise - if self.credentials.is_some() && self.state.connection_reuse_count == 0 { - let credentials = self.credentials.clone().unwrap(); - if self.server_info.as_ref().unwrap().supports_feature(Extension::CramMd5Authentication).is_some() { - let result = self.auth_cram_md5(credentials.username.as_slice(), - credentials.password.as_slice()); + if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 { + + let (username, password) = self.client_info.credentials.clone().unwrap(); + + if self.server_info.as_ref().unwrap().supports_feature(Extension::CramMd5Authentication) { + let result = self.auth_cram_md5(username.as_slice(), + password.as_slice()); try_smtp!(result, self); - } else if self.server_info.as_ref().unwrap().supports_feature(Extension::PlainAuthentication).is_some() { - let result = self.auth_plain(credentials.username.as_slice(), - credentials.password.as_slice()); + } else if self.server_info.as_ref().unwrap().supports_feature(Extension::PlainAuthentication) { + let result = self.auth_plain(username.as_slice(), + password.as_slice()); try_smtp!(result, self); } else { debug!("No supported authentication mecanisms available"); @@ -247,7 +250,7 @@ impl Client { let current_message = Uuid::new_v4(); email.set_message_id(format!("<{}@{}>", current_message, - self.configuration.hello_name.clone())); + self.client_info.hello_name.clone())); let from_address = email.from_address(); let to_addresses = email.to_addresses(); @@ -282,8 +285,8 @@ impl Client { } // Test if we can reuse the existing connection - if (!self.configuration.enable_connection_reuse) || - (self.state.connection_reuse_count >= self.configuration.connection_reuse_count_limit) { + if (!self.client_info.enable_connection_reuse) || + (self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) { self.reset(); } @@ -298,7 +301,7 @@ impl Client { } // Try to connect - self.stream = Some(try!(Connecter::connect(self.server_addr))); + self.stream = Some(try!(Connecter::connect(self.client_info.server_addr))); let result = self.stream.as_mut().unwrap().get_reply(); with_code!(result, [220].iter()) @@ -322,7 +325,7 @@ impl Client { /// Send a HELO command and fills `server_info` fn helo(&mut self) -> SmtpResult { - let hostname = self.configuration.hello_name.clone(); + let hostname = self.client_info.hello_name.clone(); let result = try!(self.command(format!("HELO {}", hostname).as_slice(), [250].iter())); self.server_info = Some( ServerInfo{ @@ -335,7 +338,7 @@ impl Client { /// Sends a EHLO command and fills `server_info` fn ehlo(&mut self) -> SmtpResult { - let hostname = self.configuration.hello_name.clone(); + let hostname = self.client_info.hello_name.clone(); let result = try!(self.command(format!("EHLO {}", hostname).as_slice(), [250].iter())); self.server_info = Some( ServerInfo{ @@ -352,8 +355,8 @@ impl Client { fn mail(&mut self, address: &str) -> SmtpResult { // Checks message encoding according to the server's capability let options = match self.server_info.as_ref().unwrap().supports_feature(Extension::EightBitMime) { - Some(_) => "BODY=8BITMIME", - None => "", + true => "BODY=8BITMIME", + false => "", }; self.command(format!("MAIL FROM:<{}> {}", address, options).as_slice(), [250].iter()) diff --git a/src/client/server_info.rs b/src/client/server_info.rs index ee8a6d4..9d7ebf6 100644 --- a/src/client/server_info.rs +++ b/src/client/server_info.rs @@ -41,13 +41,8 @@ impl Display for ServerInfo { impl ServerInfo { /// Checks if the server supports an ESMTP feature - pub fn supports_feature(&self, keyword: Extension) -> Option { - for feature in self.esmtp_features.iter() { - if keyword.same_extension_as(feature) { - return Some(*feature); - } - } - None + pub fn supports_feature(&self, keyword: Extension) -> bool { + self.esmtp_features.contains(&keyword) } } @@ -74,17 +69,17 @@ mod test { #[test] fn test_supports_feature() { - assert_eq!(ServerInfo{ - name: "name".to_string(), - esmtp_features: vec![Extension::EightBitMime] - }.supports_feature(Extension::EightBitMime), Some(Extension::EightBitMime)); - assert_eq!(ServerInfo{ - name: "name".to_string(), - esmtp_features: vec![Extension::PlainAuthentication, Extension::EightBitMime] - }.supports_feature(Extension::EightBitMime), Some(Extension::EightBitMime)); assert!(ServerInfo{ name: "name".to_string(), esmtp_features: vec![Extension::EightBitMime] - }.supports_feature(Extension::PlainAuthentication).is_none()); + }.supports_feature(Extension::EightBitMime)); + assert!(ServerInfo{ + name: "name".to_string(), + esmtp_features: vec![Extension::PlainAuthentication, Extension::EightBitMime] + }.supports_feature(Extension::EightBitMime)); + assert_eq!(ServerInfo{ + name: "name".to_string(), + esmtp_features: vec![Extension::EightBitMime] + }.supports_feature(Extension::PlainAuthentication), false); } } diff --git a/src/extension.rs b/src/extension.rs index 009dea4..dcd9d28 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -63,11 +63,6 @@ impl Extension { } } - /// Checks if the ESMTP keyword is the same - pub fn same_extension_as(&self, other: &Extension) -> bool { - self == other - } - /// Parses supported ESMTP features pub fn parse_esmtp_response(message: &str) -> Vec { let mut esmtp_features: Vec = Vec::new(); @@ -95,12 +90,6 @@ mod test { assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec!(Extension::PlainAuthentication, Extension::CramMd5Authentication))); } - #[test] - fn test_same_extension_as() { - assert_eq!(Extension::EightBitMime.same_extension_as(&Extension::EightBitMime), true); - assert_eq!(Extension::EightBitMime.same_extension_as(&Extension::SmtpUtfEight), false); - } - #[test] fn test_parse_esmtp_response() { assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 SIZE 42"), diff --git a/src/lib.rs b/src/lib.rs index f3e7d9f..9e94f91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ //! This is the most basic example of usage: //! //! ```rust,no_run -//! use smtp::client::Client; +//! use smtp::client::ClientBuilder; //! use smtp::mailer::Email; //! //! // Create an email @@ -40,7 +40,7 @@ //! email.date_now(); //! //! // Open a local connection on port 25 -//! let mut client: Client = Client::localhost(); +//! let mut client = ClientBuilder::localhost().build(); //! // Send the email //! let result = client.send(email); //! @@ -50,7 +50,7 @@ //! ### Complete example //! //! ```rust,no_run -//! use smtp::client::Client; +//! use smtp::client::ClientBuilder; //! use smtp::mailer::Email; //! //! let mut email = Email::new(); @@ -66,12 +66,11 @@ //! email.date_now(); //! //! // Connect to a remote server on a custom port -//! let mut client: Client = Client::new(("server.tld", 10025)); -//! // Set the name sent during EHLO/HELO, default is `localhost` -//! client.set_hello_name("my.hostname.tld"); -//! // Enable connection reuse -//! client.set_enable_connection_reuse(true); -//! +//! let mut client = ClientBuilder::new(("server.tld", 10025)) +//! // Set the name sent during EHLO/HELO, default is `localhost` +//! .hello_name("my.hostname.tld".to_string()) +//! // Enable connection reuse +//! .enable_connection_reuse(true).build(); //! let result_1 = client.send(email.clone()); //! assert!(result_1.is_ok()); //! // The second email will use the same connection @@ -87,7 +86,7 @@ //! If you just want to send an email without using `Email` to provide headers: //! //! ```rust,no_run -//! use smtp::client::Client; +//! use smtp::client::ClientBuilder; //! use smtp::sendable_email::SimpleSendableEmail; //! //! // Create a minimal email @@ -97,7 +96,7 @@ //! "Hello world !" //! ); //! -//! let mut client: Client = Client::localhost(); +//! let mut client = ClientBuilder::localhost().build(); //! let result = client.send(email); //! assert!(result.is_ok()); //! ``` @@ -121,6 +120,9 @@ pub mod mailer; use std::old_io::net::ip::Port; +// Registrated port numbers: +// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + /// Default smtp port pub static SMTP_PORT: Port = 25;