Builder for the Client

This commit is contained in:
Alexis Mousset
2015-03-02 17:25:02 +01:00
parent d262947848
commit 487f845f85
6 changed files with 129 additions and 141 deletions

View File

@@ -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
-------------

View File

@@ -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<String>, 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::<Port>().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::<u16>().unwrap(),
None => 1,

View File

@@ -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<A: ToSocketAddr>(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<S = TcpStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<S>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
/// Client variable states
state: State,
/// Configuration of the client
configuration: Configuration,
/// Client credentials
credentials: Option<Credentials>,
/// Information about the client
client_info: ClientBuilder,
}
macro_rules! try_smtp (
@@ -127,54 +165,17 @@ impl<S = TcpStream> Client<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new<A: ToSocketAddr>(addr: A) -> Client<S> {
pub fn new(builder: ClientBuilder) -> Client<S> {
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<S> {
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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
@@ -230,15 +231,17 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
}
// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
}
// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
}
// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
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())

View File

@@ -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<Extension> {
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);
}
}

View File

@@ -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<Extension> {
let mut esmtp_features: Vec<Extension> = 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"),

View File

@@ -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;