Builder for the Client
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
rust-smtp [](https://travis-ci.org/amousset/rust-smtp) [](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
|
||||
-------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
24
src/lib.rs
24
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user