Email builder

This commit is contained in:
Alexis Mousset
2015-03-04 11:04:07 +01:00
parent 487f845f85
commit 2fa499629d
7 changed files with 167 additions and 115 deletions

View File

@@ -7,7 +7,7 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
#![feature(core, old_io, rustc_private, env, collections)]
#![feature(core, old_io, rustc_private, collections)]
#[macro_use]
extern crate log;
@@ -23,19 +23,19 @@ use getopts::{optopt, optflag, getopts, OptGroup, usage};
use smtp::client::ClientBuilder;
use smtp::error::SmtpResult;
use smtp::mailer::Email;
use smtp::mailer::EmailBuilder;
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();
let mut email_builder = EmailBuilder::new();
for destination in recipient_addresses.iter() {
email.to(destination.as_slice());
email_builder = email_builder.to(destination.as_slice());
}
email.from(source_address.as_slice());
email.body(message.as_slice());
email.subject(subject.as_slice());
email.date_now();
let email = email_builder.from(source_address.as_slice())
.body(message.as_slice())
.subject(subject.as_slice())
.build();
let mut client = ClientBuilder::new((server.as_slice(), port)).hello_name(my_hostname)
.enable_connection_reuse(true).build();

View File

@@ -330,7 +330,7 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
self.server_info = Some(
ServerInfo{
name: get_first_word(result.message.as_ref().unwrap().as_slice()).to_string(),
esmtp_features: vec!(),
esmtp_features: vec![],
}
);
Ok(result)
@@ -384,7 +384,7 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// Sends an AUTH command with PLAIN mecanism
fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
let auth_string = format!("{}{}{}{}{}", "", NUL, username, NUL, password);
let auth_string = format!("{}{}{}{}", NUL, username, NUL, password);
self.command(format!("AUTH PLAIN {}", auth_string.as_bytes().to_base64(base64::STANDARD)).as_slice(), [235].iter())
}

View File

@@ -63,7 +63,7 @@ mod test {
}), "name with [EightBitMime]".to_string());
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: vec!()
esmtp_features: vec![]
}), "name with no supported features".to_string());
}

View File

@@ -45,11 +45,11 @@ impl Extension {
fn from_str(s: &str) -> Result<Vec<Extension>, &'static str> {
let splitted : Vec<&str> = s.split(' ').collect();
match (splitted[0], splitted.len()) {
("8BITMIME", 1) => Ok(vec!(EightBitMime)),
("SMTPUTF8", 1) => Ok(vec!(SmtpUtfEight)),
("STARTTLS", 1) => Ok(vec!(StartTls)),
("8BITMIME", 1) => Ok(vec![EightBitMime]),
("SMTPUTF8", 1) => Ok(vec![SmtpUtfEight]),
("STARTTLS", 1) => Ok(vec![StartTls]),
("AUTH", _) => {
let mut mecanisms: Vec<Extension> = vec!();
let mut mecanisms: Vec<Extension> = vec![];
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => mecanisms.push(PlainAuthentication),
@@ -83,24 +83,24 @@ mod test {
#[test]
fn test_from_str() {
assert_eq!(Extension::from_str("8BITMIME"), Ok(vec!(Extension::EightBitMime)));
assert_eq!(Extension::from_str("AUTH PLAIN"), Ok(vec!(Extension::PlainAuthentication)));
assert_eq!(Extension::from_str("AUTH PLAIN LOGIN CRAM-MD5"), Ok(vec!(Extension::PlainAuthentication, Extension::CramMd5Authentication)));
assert_eq!(Extension::from_str("AUTH CRAM-MD5 PLAIN"), Ok(vec!(Extension::CramMd5Authentication, Extension::PlainAuthentication)));
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec!(Extension::PlainAuthentication, Extension::CramMd5Authentication)));
assert_eq!(Extension::from_str("8BITMIME"), Ok(vec![Extension::EightBitMime]));
assert_eq!(Extension::from_str("AUTH PLAIN"), Ok(vec![Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH PLAIN LOGIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
assert_eq!(Extension::from_str("AUTH CRAM-MD5 PLAIN"), Ok(vec![Extension::CramMd5Authentication, Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
}
#[test]
fn test_parse_esmtp_response() {
assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 SIZE 42"),
vec!(Extension::EightBitMime));
vec![Extension::EightBitMime]);
assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 AUTH PLAIN CRAM-MD5\r\n250 UNKNON 42"),
vec!(Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication));
vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
assert_eq!(Extension::parse_esmtp_response("me\r\n250-9BITMIME\r\n250 SIZE a"),
vec!());
vec![]);
assert_eq!(Extension::parse_esmtp_response("me\r\n250-SIZE 42\r\n250 SIZE 43"),
vec!());
vec![]);
assert_eq!(Extension::parse_esmtp_response(""),
vec!());
vec![]);
}
}

View File

@@ -9,16 +9,20 @@
//! # Rust SMTP client
//!
//! The client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
//! a work in progress.
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
//! a work in progress. It is designed to efficiently send emails from a rust application to a
//! relay email server.
//!
//! It should eventually implement the following extensions :
//! It implements the following extensions :
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954))
//!
//! 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))
//!
//! ## Usage
//!
//! ### Simple example
@@ -27,17 +31,17 @@
//!
//! ```rust,no_run
//! use smtp::client::ClientBuilder;
//! use smtp::mailer::Email;
//! use smtp::mailer::EmailBuilder;
//!
//! // Create an email
//! let mut email = Email::new();
//! // Addresses can be specified by the couple (email, alias)
//! email.to(("user@example.org", "Firstname Lastname"));
//! // ... or by an address only
//! email.from("user@example.com");
//! email.subject("Hello world");
//! email.body("Hi, Hello world.");
//! email.date_now();
//! let email = EmailBuilder::new()
//! // Addresses can be specified by the couple (email, alias)
//! .to(("user@example.org", "Firstname Lastname"))
//! // ... or by an address only
//! .from("user@example.com")
//! .subject("Hi, Hello world")
//! .body("Hello world.")
//! .build();
//!
//! // Open a local connection on port 25
//! let mut client = ClientBuilder::localhost().build();
@@ -51,26 +55,29 @@
//!
//! ```rust,no_run
//! use smtp::client::ClientBuilder;
//! use smtp::mailer::Email;
//! use smtp::mailer::EmailBuilder;
//!
//! let mut email = Email::new();
//! email.to(("user@example.org", "Alias name"));
//! email.cc(("user@example.net", "Alias name"));
//! email.from("no-reply@example.com");
//! email.from("no-reply@example.eu");
//! email.sender("no-reply@example.com");
//! email.subject("Hello world");
//! email.body("Hi, Hello world.");
//! email.reply_to("contact@example.com");
//! email.add_header(("X-Custom-Header", "my header"));
//! email.date_now();
//! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name"));
//! builder = builder.cc(("user@example.net", "Alias name"));
//! builder = builder.from("no-reply@example.com");
//! builder = builder.from("no-reply@example.eu");
//! builder = builder.sender("no-reply@example.com");
//! builder = builder.subject("Hello world");
//! builder = builder.body("Hi, Hello world.");
//! builder = builder.reply_to("contact@example.com");
//! builder = builder.add_header(("X-Custom-Header", "my header"));
//!
//! let email = builder.build();
//!
//! // Connect to a remote server on a custom port
//! 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();
//! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name("my.hostname.tld".to_string())
//! // Add credentials for authentication
//! .credentials("username".to_string(), "password".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

View File

@@ -21,6 +21,15 @@ use sendable_email::SendableEmail;
pub mod header;
pub mod address;
/// TODO
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct EmailBuilder {
/// Email content
content: Email,
/// Date issued
date_issued: bool,
}
/// Simple email representation
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Email {
@@ -45,87 +54,100 @@ impl Display for Email {
}
}
impl Email {
impl EmailBuilder {
/// Creates a new empty email
pub fn new() -> Email {
Email{headers: vec!(), body: "".to_string(), to: vec!(), from: None}
}
/// Clear the email content
pub fn clear(&mut self) {
self.headers.clear();
self.body = "".to_string();
self.to.clear();
self.from = None;
pub fn new() -> EmailBuilder {
EmailBuilder {
content: Email {
headers: vec![],
body: "".to_string(),
to: vec![],
from: None,
},
date_issued: false,
}
}
/// Sets the email body
pub fn body(&mut self, body: &str) {
self.body = body.to_string();
pub fn body(mut self, body: &str) -> EmailBuilder {
self.content.body = body.to_string();
self
}
/// Add a generic header
pub fn add_header<A: ToHeader>(&mut self, header: A) {
self.headers.push(header.to_header());
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
self.content.headers.push(header.to_header());
self
}
/// Adds a `From` header and store the sender address
pub fn from<A: ToAddress>(&mut self, address: A) {
self.from = Some(address.to_address().get_address());
self.headers.push(
pub fn from<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.from = Some(address.to_address().get_address());
self.content.headers.push(
Header::From(address.to_address())
);
self
}
/// Adds a `To` header and store the recipient address
pub fn to<A: ToAddress>(&mut self, address: A) {
self.to.push(address.to_address().get_address());
self.headers.push(
pub fn to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.to.push(address.to_address().get_address());
self.content.headers.push(
Header::To(address.to_address())
);
self
}
/// Adds a `Cc` header and store the recipient address
pub fn cc<A: ToAddress>(&mut self, address: A) {
self.to.push(address.to_address().get_address());
self.headers.push(
pub fn cc<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.to.push(address.to_address().get_address());
self.content.headers.push(
Header::Cc(address.to_address())
);
self
}
/// Adds a `Reply-To` header
pub fn reply_to<A: ToAddress>(&mut self, address: A) {
self.headers.push(
pub fn reply_to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.headers.push(
Header::ReplyTo(address.to_address())
);
self
}
/// Adds a `Sender` header
pub fn sender<A: ToAddress>(&mut self, address: A) {
self.headers.push(
pub fn sender<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.headers.push(
Header::Sender(address.to_address())
);
self
}
/// Adds a `Subject` header
pub fn subject(&mut self, subject: &str) {
self.headers.push(
pub fn subject(mut self, subject: &str) -> EmailBuilder {
self.content.headers.push(
Header::Subject(subject.to_string())
);
}
/// Adds a `Date` header with the current date
pub fn date_now(&mut self) {
self.headers.push(
Header::Date(now())
);
self
}
/// Adds a `Date` header with the given date
pub fn date(&mut self, date: Tm) {
self.headers.push(
pub fn date(mut self, date: Tm) -> EmailBuilder {
self.content.headers.push(
Header::Date(date)
);
self.date_issued = true;
self
}
/// Build the Email
pub fn build(mut self) -> Email {
if !self.date_issued {
self.content.headers.push(
Header::Date(now())
);
}
self.content
}
}
@@ -160,50 +182,73 @@ impl SendableEmail for Email {
#[cfg(test)]
mod test {
use super::Email;
use super::{Email, EmailBuilder};
use mailer::header::Header;
#[test]
fn test_new() {
assert_eq!(
Email::new(),
Email{headers: vec!(), body: "".to_string(), to: vec!(), from: None}
EmailBuilder::new(),
EmailBuilder{content: Email{headers: vec![], body: "".to_string(), to: vec![], from: None}, date_issued: false}
)
}
#[test]
fn test_body() {
let mut email = Email::new();
email.body("test message");
let email = EmailBuilder::new().body("test message");
assert_eq!(
email,
Email{headers: vec!(), body: "test message".to_string(), to: vec!(), from: None}
EmailBuilder{content: Email {headers: vec![], body: "test message".to_string(), to: vec![], from: None}, date_issued: false}
)
}
#[test]
fn test_add_header() {
let mut email = Email::new();
email.add_header(("X-My-Header", "value"));
let mut email = EmailBuilder::new()
.add_header(("X-My-Header", "value"));
assert_eq!(
email,
Email{
headers: vec!(Header::new("X-My-Header", "value")),
body: "".to_string(),
to: vec!(),
from: None
EmailBuilder{
content: Email {
headers: vec![Header::new("X-My-Header", "value")],
body: "".to_string(),
to: vec![],
from: None
},
date_issued: false,
}
);
email.add_header(("X-My-Header-2", "value-2"));
email = email.add_header(("X-My-Header-2", "value-2"));
assert_eq!(
email,
Email{
headers: vec!(Header::new("X-My-Header", "value"),
Header::new("X-My-Header-2", "value-2")),
body: "".to_string(),
to: vec!(),
from: None
EmailBuilder{
content: Email {
headers: vec![Header::new("X-My-Header", "value"),
Header::new("X-My-Header-2", "value-2")],
body: "".to_string(),
to: vec![],
from: None
},
date_issued: false,
}
);
email = email.add_header(("X-My-Header-3", "value-3")).add_header(("X-My-Header-4", "value-4"));
assert_eq!(
email,
EmailBuilder{
content: Email {
headers: vec![Header::new("X-My-Header", "value"),
Header::new("X-My-Header-2", "value-2"),
Header::new("X-My-Header-3", "value-3"),
Header::new("X-My-Header-4", "value-4")],
body: "".to_string(),
to: vec![],
from: None
},
date_issued: false,
}
);
}
// TODO test Email
}

View File

@@ -36,7 +36,7 @@ impl SimpleSendableEmail {
pub fn new(from_address: &str, to_address: &str, message: &str) -> SimpleSendableEmail {
SimpleSendableEmail {
from: from_address.to_string(),
to: vec!(to_address.to_string()),
to: vec![to_address.to_string()],
message: message.to_string(),
}
}