Rename rust-smtp to lettre, add multiple transports support

This commit is contained in:
Alexis Mousset
2015-10-14 23:44:25 +02:00
parent bd67d80d3e
commit 54758ebde9
15 changed files with 328 additions and 229 deletions

View File

@@ -1,13 +1,13 @@
[package] [package]
name = "smtp" name = "lettre"
version = "0.4.0" version = "0.4.0"
description = "Simple SMTP client" description = "Email client"
readme = "README.md" readme = "README.md"
documentation = "http://amousset.me/rust-smtp/smtp/" documentation = "http://lettre.github.io/lettre/"
repository = "https://github.com/amousset/rust-smtp" repository = "https://github.com/lettre/lettre"
license = "MIT" license = "MIT"
authors = ["Alexis Mousset <alexis.mousset@gmx.fr>"] authors = ["Alexis Mousset <contact@amousset.me>"]
keywords = ["email", "smtp", "mailer"] keywords = ["email", "smtp", "mailer"]
[dependencies] [dependencies]

View File

@@ -1,8 +1,8 @@
rust-smtp [![Build Status](https://travis-ci.org/amousset/rust-smtp.svg?branch=master)](https://travis-ci.org/amousset/rust-smtp) [![Coverage Status](https://coveralls.io/repos/github/amousset/rust-smtp/badge.svg?branch=master)](https://coveralls.io/github/amousset/rust-smtp?branch=master) [![Crate](https://meritbadge.herokuapp.com/smtp)](https://crates.io/crates/smtp) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) lettre [![Build Status](https://travis-ci.org/lettre/lettre.svg?branch=master)](https://travis-ci.org/lettre/lettre) [![Coverage Status](https://coveralls.io/repos/github/lettre/lettre/badge.svg?branch=master)](https://coveralls.io/github/lettre/lettre?branch=master) [![Crate](https://meritbadge.herokuapp.com/smtp)](https://crates.io/crates/smtp) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
========= =========
This library implements a simple SMTP client. This is an email library written in Rust.
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information. See the [documentation](http://lettre.github.io/) for more information.
Install Install
------- -------
@@ -11,7 +11,7 @@ To use this library, add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
smtp = "0.3" lettre = "0.4"
``` ```
License License

View File

@@ -1,24 +1,27 @@
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate env_logger; extern crate env_logger;
extern crate smtp; extern crate lettre;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use smtp::sender::SenderBuilder; use lettre::transport::smtp::SmtpTransportBuilder;
use smtp::email::EmailBuilder; use lettre::transport::EmailTransport;
use lettre::mailer::Mailer;
use lettre::email::EmailBuilder;
fn main() { fn main() {
env_logger::init().unwrap(); env_logger::init().unwrap();
let sender = Arc::new(Mutex::new(SenderBuilder::localhost().unwrap().hello_name("localhost") let sender = SmtpTransportBuilder::localhost().unwrap().hello_name("localhost")
.enable_connection_reuse(true).build())); .enable_connection_reuse(true).build();
let mailer = Arc::new(Mutex::new(Mailer::new(sender)));
let mut threads = Vec::new(); let mut threads = Vec::new();
for _ in 1..5 { for _ in 1..5 {
let th_sender = sender.clone(); let th_mailer = mailer.clone();
threads.push(thread::spawn(move || { threads.push(thread::spawn(move || {
let email = EmailBuilder::new() let email = EmailBuilder::new()
@@ -26,9 +29,9 @@ fn main() {
.from("user@localhost") .from("user@localhost")
.body("Hello World!") .body("Hello World!")
.subject("Hello") .subject("Hello")
.build(); .build().unwrap();
let _ = th_sender.lock().unwrap().send(email); let _ = th_mailer.lock().unwrap().send(email);
})); }));
} }
@@ -41,11 +44,11 @@ fn main() {
.from("user@localhost") .from("user@localhost")
.body("Hello World!") .body("Hello World!")
.subject("Hello Bis") .subject("Hello Bis")
.build(); .build().unwrap();
let mut sender = sender.lock().unwrap(); let mut mailer = mailer.lock().unwrap();
let result = sender.send(email); let result = mailer.send(email);
sender.close(); mailer.close();
match result { match result {
Ok(..) => info!("Email sent successfully"), Ok(..) => info!("Email sent successfully"),

View File

@@ -1,6 +1,7 @@
//! Simple email (very incomplete) //! Simple email (very incomplete)
use std::fmt::{Display, Formatter, Result}; use std::fmt::{Display, Formatter};
use std::fmt;
use email_format::{MimeMessage, Header, Mailbox}; use email_format::{MimeMessage, Header, Mailbox};
use time::{now, Tm}; use time::{now, Tm};
@@ -53,8 +54,12 @@ impl<'a> ToMailbox for (&'a str, &'a str) {
/// Builds an `Email` structure /// Builds an `Email` structure
#[derive(PartialEq,Eq,Clone,Debug)] #[derive(PartialEq,Eq,Clone,Debug)]
pub struct EmailBuilder { pub struct EmailBuilder {
/// Email content /// Message
content: Email, message: MimeMessage,
/// The enveloppe recipients addresses
to: Vec<String>,
/// The enveloppe sender address
from: Option<String>,
/// Date issued /// Date issued
date_issued: bool, date_issued: bool,
} }
@@ -67,13 +72,13 @@ pub struct Email {
/// The enveloppe recipients addresses /// The enveloppe recipients addresses
to: Vec<String>, to: Vec<String>,
/// The enveloppe sender address /// The enveloppe sender address
from: Option<String>, from: String,
/// Message-ID /// Message-ID
message_id: Uuid, message_id: Uuid,
} }
impl Display for Email { impl Display for Email {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.message.as_string()) write!(f, "{}", self.message.as_string())
} }
} }
@@ -81,30 +86,17 @@ impl Display for Email {
impl EmailBuilder { impl EmailBuilder {
/// Creates a new empty email /// Creates a new empty email
pub fn new() -> EmailBuilder { pub fn new() -> EmailBuilder {
let current_message = Uuid::new_v4(); EmailBuilder {
let mut email = Email {
message: MimeMessage::new_blank_message(), message: MimeMessage::new_blank_message(),
to: vec![], to: vec![],
from: None, from: None,
message_id: current_message,
};
match Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>", current_message)) {
Ok(header) => email.message.headers.insert(header),
Err(_) => (),
}
EmailBuilder {
content: email,
date_issued: false, date_issued: false,
} }
} }
/// Sets the email body /// Sets the email body
pub fn body(mut self, body: &str) -> EmailBuilder { pub fn body(mut self, body: &str) -> EmailBuilder {
self.content.message.body = body.to_string(); self.message.body = body.to_string();
self self
} }
@@ -115,14 +107,14 @@ impl EmailBuilder {
} }
fn insert_header<A: ToHeader>(&mut self, header: A) { fn insert_header<A: ToHeader>(&mut self, header: A) {
self.content.message.headers.insert(header.to_header()); self.message.headers.insert(header.to_header());
} }
/// Adds a `From` header and store the sender address /// Adds a `From` header and store the sender address
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder { pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox(); let mailbox = address.to_mailbox();
self.insert_header(("From", mailbox.to_string().as_ref())); self.insert_header(("From", mailbox.to_string().as_ref()));
self.content.from = Some(mailbox.address); self.from = Some(mailbox.address);
self self
} }
@@ -130,7 +122,7 @@ impl EmailBuilder {
pub fn to<A: ToMailbox>(mut self, address: A) -> EmailBuilder { pub fn to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox(); let mailbox = address.to_mailbox();
self.insert_header(("To", mailbox.to_string().as_ref())); self.insert_header(("To", mailbox.to_string().as_ref()));
self.content.to.push(mailbox.address); self.to.push(mailbox.address);
self self
} }
@@ -138,7 +130,7 @@ impl EmailBuilder {
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder { pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox(); let mailbox = address.to_mailbox();
self.insert_header(("Cc", mailbox.to_string().as_ref())); self.insert_header(("Cc", mailbox.to_string().as_ref()));
self.content.to.push(mailbox.address); self.to.push(mailbox.address);
self self
} }
@@ -153,7 +145,7 @@ impl EmailBuilder {
pub fn sender<A: ToMailbox>(mut self, address: A) -> EmailBuilder { pub fn sender<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox(); let mailbox = address.to_mailbox();
self.insert_header(("Sender", mailbox.to_string().as_ref())); self.insert_header(("Sender", mailbox.to_string().as_ref()));
self.content.from = Some(mailbox.address); self.from = Some(mailbox.address);
self self
} }
@@ -171,12 +163,34 @@ impl EmailBuilder {
} }
/// Build the Email /// Build the Email
pub fn build(mut self) -> Email { pub fn build(mut self) -> Result<Email, &'static str> {
if self.from.is_none() {
return Err("No from address")
}
if self.to.is_empty() {
return Err("No to address")
}
if !self.date_issued { if !self.date_issued {
self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref())); self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
} }
self.content.message.update_headers();
self.content let message_id = Uuid::new_v4();
match Header::new_with_value("Message-ID".to_string(),
format!("<{}.lettre@localhost>", message_id)) {
Ok(header) => self.insert_header(header),
Err(_) => (),
}
self.message.update_headers();
Ok(Email {
message: self.message,
to: self.to,
from: self.from.unwrap(),
message_id: message_id,
})
} }
} }
@@ -184,13 +198,13 @@ impl EmailBuilder {
/// Email sendable by an SMTP client /// Email sendable by an SMTP client
pub trait SendableEmail { pub trait SendableEmail {
/// From address /// From address
fn from_address(&self) -> Option<String>; fn from_address(&self) -> String;
/// To addresses /// To addresses
fn to_addresses(&self) -> Option<Vec<String>>; fn to_addresses(&self) -> Vec<String>;
/// Message content /// Message content
fn message(&self) -> Option<String>; fn message(&self) -> String;
/// Message ID /// Message ID
fn message_id(&self) -> Option<String>; fn message_id(&self) -> String;
} }
/// Minimal email structure /// Minimal email structure
@@ -215,47 +229,38 @@ impl SimpleSendableEmail {
} }
impl SendableEmail for SimpleSendableEmail { impl SendableEmail for SimpleSendableEmail {
fn from_address(&self) -> Option<String> { fn from_address(&self) -> String {
Some(self.from.clone()) self.from.clone()
} }
fn to_addresses(&self) -> Option<Vec<String>> { fn to_addresses(&self) -> Vec<String> {
Some(self.to.clone()) self.to.clone()
} }
fn message(&self) -> Option<String> { fn message(&self) -> String {
Some(self.message.clone()) self.message.clone()
} }
fn message_id(&self) -> Option<String> { fn message_id(&self) -> String {
Some(format!("<{}@rust-smtp>", Uuid::new_v4())) format!("{}", Uuid::new_v4())
} }
} }
impl SendableEmail for Email { impl SendableEmail for Email {
/// Return the to addresses, and fails if it is not set fn to_addresses(&self) -> Vec<String> {
fn to_addresses(&self) -> Option<Vec<String>> { self.to.clone()
if self.to.is_empty() {
None
} else {
Some(self.to.clone())
}
} }
/// Return the from address, and fails if it is not set fn from_address(&self) -> String {
fn from_address(&self) -> Option<String> { self.from.clone()
match self.from {
Some(ref from_address) => Some(from_address.clone()),
None => None,
}
} }
fn message(&self) -> Option<String> { fn message(&self) -> String {
Some(format!("{}", self)) format!("{}", self)
} }
fn message_id(&self) -> Option<String> { fn message_id(&self) -> String {
Some(format!("{}", self.message_id)) format!("{}", self.message_id)
} }
} }
@@ -275,7 +280,7 @@ mod test {
let mut email = Email { let mut email = Email {
message: MimeMessage::new_blank_message(), message: MimeMessage::new_blank_message(),
to: vec![], to: vec![],
from: None, from: "".to_string(),
message_id: current_message, message_id: current_message,
}; };
@@ -294,7 +299,7 @@ mod test {
assert_eq!(format!("{}", email), assert_eq!(format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n", format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
current_message)); current_message));
assert_eq!(current_message.to_string(), email.message_id().unwrap()); assert_eq!(current_message.to_string(), email.message_id());
} }
#[test] #[test]
@@ -311,15 +316,16 @@ mod test {
.date(&date_now) .date(&date_now)
.subject("Hello") .subject("Hello")
.add_header(("X-test", "value")) .add_header(("X-test", "value"))
.build(); .build()
.unwrap();
assert_eq!(format!("{}", email), assert_eq!(format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: <user@localhost>\r\nFrom: \ format!("To: <user@localhost>\r\nFrom: <user@localhost>\r\nCc: \"Alias\" \
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \ <cc@localhost>\r\nReply-To: <reply@localhost>\r\nSender: \
<reply@localhost>\r\nSender: <sender@localhost>\r\nDate: \ <sender@localhost>\r\nDate: {}\r\nSubject: Hello\r\nX-test: \
{}\r\nSubject: Hello\r\nX-test: value\r\n\r\nHello World!\r\n", value\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nHello World!\r\n",
email.message_id().unwrap(), date_now.rfc822z(),
date_now.rfc822z())); email.message_id()));
} }
#[test] #[test]
@@ -336,13 +342,13 @@ mod test {
.date(&date_now) .date(&date_now)
.subject("Hello") .subject("Hello")
.add_header(("X-test", "value")) .add_header(("X-test", "value"))
.build(); .build()
.unwrap();
assert_eq!(email.from_address().unwrap(), assert_eq!(email.from_address(), "sender@localhost".to_string());
"sender@localhost".to_string()); assert_eq!(email.to_addresses(),
assert_eq!(email.to_addresses().unwrap(),
vec!["user@localhost".to_string(), "cc@localhost".to_string()]); vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
assert_eq!(email.message().unwrap(), format!("{}", email)); assert_eq!(email.message(), format!("{}", email));
} }
} }

View File

@@ -1,4 +1,4 @@
//! # Rust SMTP client //! # Rust email client
//! //!
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still //! 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 an application to a //! a work in progress. It is designed to efficiently send emails from an application to a
@@ -19,8 +19,8 @@
//! //!
//! This client is divided into three main parts: //! This client is divided into three main parts:
//! //!
//! * client: a low level SMTP client providing all SMTP commands //! * transport: a low level SMTP client providing all SMTP commands
//! * sender: a high level SMTP client providing an easy method to send emails //! * mailer: a high level SMTP client providing an easy method to send emails
//! * email: generates the email to be sent with the sender //! * email: generates the email to be sent with the sender
//! //!
//! ## Usage //! ## Usage
@@ -30,8 +30,10 @@
//! This is the most basic example of usage: //! This is the most basic example of usage:
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder}; //! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
//! use smtp::email::EmailBuilder; //! use lettre::email::EmailBuilder;
//! use lettre::transport::EmailTransport;
//! use lettre::mailer::Mailer;
//! //!
//! // Create an email //! // Create an email
//! let email = EmailBuilder::new() //! let email = EmailBuilder::new()
@@ -41,12 +43,12 @@
//! .from("user@example.com") //! .from("user@example.com")
//! .subject("Hi, Hello world") //! .subject("Hi, Hello world")
//! .body("Hello world.") //! .body("Hello world.")
//! .build(); //! .build().unwrap();
//! //!
//! // Open a local connection on port 25 //! // Open a local connection on port 25
//! let mut sender = SenderBuilder::localhost().unwrap().build(); //! let mut mailer = Mailer::new(SmtpTransportBuilder::localhost().unwrap().build());
//! // Send the email //! // Send the email
//! let result = sender.send(email); //! let result = mailer.send(email);
//! //!
//! assert!(result.is_ok()); //! assert!(result.is_ok());
//! ``` //! ```
@@ -54,10 +56,12 @@
//! ### Complete example //! ### Complete example
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use smtp::sender::{SecurityLevel, Sender, SenderBuilder}; //! use lettre::email::EmailBuilder;
//! use smtp::email::EmailBuilder; //! use lettre::transport::smtp::{SecurityLevel, SmtpTransport, SmtpTransportBuilder};
//! use smtp::authentication::Mecanism; //! use lettre::transport::smtp::authentication::Mecanism;
//! use smtp::SUBMISSION_PORT; //! use lettre::transport::smtp::SUBMISSION_PORT;
//! use lettre::transport::EmailTransport;
//! use lettre::mailer::Mailer;
//! //!
//! let mut builder = EmailBuilder::new(); //! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name")); //! builder = builder.to(("user@example.org", "Alias name"));
@@ -70,10 +74,10 @@
//! builder = builder.reply_to("contact@example.com"); //! builder = builder.reply_to("contact@example.com");
//! builder = builder.add_header(("X-Custom-Header", "my header")); //! builder = builder.add_header(("X-Custom-Header", "my header"));
//! //!
//! let email = builder.build(); //! let email = builder.build().unwrap();
//! //!
//! // Connect to a remote server on a custom port //! // Connect to a remote server on a custom port
//! let mut sender = SenderBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap() //! let mut mailer = Mailer::new(SmtpTransportBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap()
//! // Set the name sent during EHLO/HELO, default is `localhost` //! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name("my.hostname.tld") //! .hello_name("my.hostname.tld")
//! // Add credentials for authentication //! // Add credentials for authentication
@@ -84,17 +88,17 @@
//! // Configure accepted authetication mecanisms //! // Configure accepted authetication mecanisms
//! .authentication_mecanisms(vec![Mecanism::CramMd5]) //! .authentication_mecanisms(vec![Mecanism::CramMd5])
//! // Enable connection reuse //! // Enable connection reuse
//! .enable_connection_reuse(true).build(); //! .enable_connection_reuse(true).build());
//! //!
//! let result_1 = sender.send(email.clone()); //! let result_1 = mailer.send(email.clone());
//! assert!(result_1.is_ok()); //! assert!(result_1.is_ok());
//! //!
//! // The second email will use the same connection //! // The second email will use the same connection
//! let result_2 = sender.send(email); //! let result_2 = mailer.send(email);
//! assert!(result_2.is_ok()); //! assert!(result_2.is_ok());
//! //!
//! // Explicitely close the SMTP transaction as we enabled connection reuse //! // Explicitely close the SMTP transaction as we enabled connection reuse
//! sender.close(); //! mailer.close();
//! ``` //! ```
//! //!
//! ### Using the client directly //! ### Using the client directly
@@ -102,8 +106,10 @@
//! If you just want to send an email without using `Email` to provide headers: //! If you just want to send an email without using `Email` to provide headers:
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder}; //! use lettre::email::SimpleSendableEmail;
//! use smtp::email::SimpleSendableEmail; //! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
//! use lettre::transport::EmailTransport;
//! use lettre::mailer::Mailer;
//! //!
//! // Create a minimal email //! // Create a minimal email
//! let email = SimpleSendableEmail::new( //! let email = SimpleSendableEmail::new(
@@ -112,8 +118,8 @@
//! "Hello world !" //! "Hello world !"
//! ); //! );
//! //!
//! let mut sender = SenderBuilder::localhost().unwrap().build(); //! let mut mailer = Mailer::new(SmtpTransportBuilder::localhost().unwrap().build());
//! let result = sender.send(email); //! let result = mailer.send(email);
//! assert!(result.is_ok()); //! assert!(result.is_ok());
//! ``` //! ```
//! //!
@@ -122,9 +128,9 @@
//! You can also send commands, here is a simple email transaction without error handling: //! You can also send commands, here is a simple email transaction without error handling:
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use smtp::client::Client; //! use lettre::transport::smtp::SMTP_PORT;
//! use smtp::SMTP_PORT; //! use lettre::transport::smtp::client::Client;
//! use smtp::client::net::NetworkStream; //! use lettre::transport::smtp::client::net::NetworkStream;
//! //!
//! let mut email_client: Client<NetworkStream> = Client::new(); //! let mut email_client: Client<NetworkStream> = Client::new();
//! let _ = email_client.connect(&("localhost", SMTP_PORT)); //! let _ = email_client.connect(&("localhost", SMTP_PORT));
@@ -148,39 +154,6 @@ extern crate email as email_format;
extern crate bufstream; extern crate bufstream;
extern crate openssl; extern crate openssl;
mod extension; pub mod transport;
pub mod client;
pub mod sender;
pub mod response;
pub mod error;
pub mod authentication;
pub mod email; pub mod email;
pub mod mailer;
// Registrated port numbers:
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port
pub static SMTP_PORT: u16 = 25;
/// Default smtps port
pub static SMTPS_PORT: u16 = 465;
/// Default submission port
pub static SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub static SP: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub static CRLF: &'static str = "\r\n";
/// Colon
pub static COLON: &'static str = ":";
/// The ending of message content
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub static NUL: &'static str = "\0";

30
src/mailer.rs Normal file
View File

@@ -0,0 +1,30 @@
//! TODO
use transport::EmailTransport;
use email::SendableEmail;
use transport::error::EmailResult;
/// TODO
pub struct Mailer<T: EmailTransport> {
transport: T,
}
impl<T: EmailTransport> Mailer<T> {
/// TODO
pub fn new(transport: T) -> Mailer<T> {
Mailer { transport: transport }
}
/// TODO
pub fn send<S: SendableEmail>(&mut self, email: S) -> EmailResult {
self.transport.send(email.to_addresses(),
email.from_address(),
email.message(),
email.message_id())
}
/// TODO
pub fn close(&mut self) {
self.transport.close()
}
}

View File

@@ -5,7 +5,7 @@ use std::io;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::fmt; use std::fmt;
use response::{Severity, Response}; use transport::smtp::response::{Severity, Response};
use serialize::base64::FromBase64Error; use serialize::base64::FromBase64Error;
use self::Error::*; use self::Error::*;
@@ -82,7 +82,7 @@ impl From<&'static str> for Error {
} }
/// SMTP result type /// SMTP result type
pub type SmtpResult = Result<Response, Error>; pub type EmailResult = Result<Response, Error>;
#[cfg(test)] #[cfg(test)]
mod test { mod test {

19
src/transport/mod.rs Normal file
View File

@@ -0,0 +1,19 @@
//! TODO
pub mod smtp;
pub mod stub;
pub mod error;
use transport::error::EmailResult;
/// Transport method for emails
pub trait EmailTransport {
/// Sends the email
fn send(&mut self,
to_addresses: Vec<String>,
from_address: String,
message: String,
message_id: String)
-> EmailResult;
/// Close the transport explicitely
fn close(&mut self);
}

View File

@@ -9,8 +9,8 @@ use crypto::hmac::Hmac;
use crypto::md5::Md5; use crypto::md5::Md5;
use crypto::mac::Mac; use crypto::mac::Mac;
use NUL; use transport::smtp::NUL;
use error::Error; use transport::error::Error;
/// Represents authentication mecanisms /// Represents authentication mecanisms
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)] #[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]

View File

@@ -9,11 +9,11 @@ use std::fmt::Debug;
use bufstream::BufStream; use bufstream::BufStream;
use openssl::ssl::SslContext; use openssl::ssl::SslContext;
use response::ResponseParser; use transport::smtp::response::ResponseParser;
use authentication::Mecanism; use transport::smtp::authentication::Mecanism;
use error::{Error, SmtpResult}; use transport::error::{Error, EmailResult};
use client::net::{Connector, NetworkStream}; use transport::smtp::client::net::{Connector, NetworkStream};
use {CRLF, MESSAGE_ENDING}; use transport::smtp::{CRLF, MESSAGE_ENDING};
pub mod net; pub mod net;
@@ -88,7 +88,7 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
} }
/// Connects to the configured server /// Connects to the configured server
pub fn connect<A: ToSocketAddrs>(&mut self, addr: &A) -> SmtpResult { pub fn connect<A: ToSocketAddrs>(&mut self, addr: &A) -> EmailResult {
// Connect should not be called when the client is already connected // Connect should not be called when the client is already connected
if self.stream.is_some() { if self.stream.is_some() {
return_err!("The connection is already established", self); return_err!("The connection is already established", self);
@@ -113,17 +113,17 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
} }
/// Sends an SMTP command /// Sends an SMTP command
pub fn command(&mut self, command: &str) -> SmtpResult { pub fn command(&mut self, command: &str) -> EmailResult {
self.send_server(command, CRLF) self.send_server(command, CRLF)
} }
/// Sends a EHLO command /// Sends a EHLO command
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult { pub fn ehlo(&mut self, hostname: &str) -> EmailResult {
self.command(&format!("EHLO {}", hostname)) self.command(&format!("EHLO {}", hostname))
} }
/// Sends a MAIL command /// Sends a MAIL command
pub fn mail(&mut self, address: &str, options: Option<&str>) -> SmtpResult { pub fn mail(&mut self, address: &str, options: Option<&str>) -> EmailResult {
match options { match options {
Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)), Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)),
None => self.command(&format!("MAIL FROM:<{}>", address)), None => self.command(&format!("MAIL FROM:<{}>", address)),
@@ -131,27 +131,27 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
} }
/// Sends a RCPT command /// Sends a RCPT command
pub fn rcpt(&mut self, address: &str) -> SmtpResult { pub fn rcpt(&mut self, address: &str) -> EmailResult {
self.command(&format!("RCPT TO:<{}>", address)) self.command(&format!("RCPT TO:<{}>", address))
} }
/// Sends a DATA command /// Sends a DATA command
pub fn data(&mut self) -> SmtpResult { pub fn data(&mut self) -> EmailResult {
self.command("DATA") self.command("DATA")
} }
/// Sends a QUIT command /// Sends a QUIT command
pub fn quit(&mut self) -> SmtpResult { pub fn quit(&mut self) -> EmailResult {
self.command("QUIT") self.command("QUIT")
} }
/// Sends a NOOP command /// Sends a NOOP command
pub fn noop(&mut self) -> SmtpResult { pub fn noop(&mut self) -> EmailResult {
self.command("NOOP") self.command("NOOP")
} }
/// Sends a HELP command /// Sends a HELP command
pub fn help(&mut self, argument: Option<&str>) -> SmtpResult { pub fn help(&mut self, argument: Option<&str>) -> EmailResult {
match argument { match argument {
Some(ref argument) => self.command(&format!("HELP {}", argument)), Some(ref argument) => self.command(&format!("HELP {}", argument)),
None => self.command("HELP"), None => self.command("HELP"),
@@ -159,22 +159,22 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
} }
/// Sends a VRFY command /// Sends a VRFY command
pub fn vrfy(&mut self, address: &str) -> SmtpResult { pub fn vrfy(&mut self, address: &str) -> EmailResult {
self.command(&format!("VRFY {}", address)) self.command(&format!("VRFY {}", address))
} }
/// Sends a EXPN command /// Sends a EXPN command
pub fn expn(&mut self, address: &str) -> SmtpResult { pub fn expn(&mut self, address: &str) -> EmailResult {
self.command(&format!("EXPN {}", address)) self.command(&format!("EXPN {}", address))
} }
/// Sends a RSET command /// Sends a RSET command
pub fn rset(&mut self) -> SmtpResult { pub fn rset(&mut self) -> EmailResult {
self.command("RSET") self.command("RSET")
} }
/// Sends an AUTH command with the given mecanism /// Sends an AUTH command with the given mecanism
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult { pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> EmailResult {
if mecanism.supports_initial_response() { if mecanism.supports_initial_response() {
self.command(&format!("AUTH {} {}", self.command(&format!("AUTH {} {}",
@@ -197,17 +197,17 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
} }
/// Sends a STARTTLS command /// Sends a STARTTLS command
pub fn starttls(&mut self) -> SmtpResult { pub fn starttls(&mut self) -> EmailResult {
self.command("STARTTLS") self.command("STARTTLS")
} }
/// Sends the message content /// Sends the message content
pub fn message(&mut self, message_content: &str) -> SmtpResult { pub fn message(&mut self, message_content: &str) -> EmailResult {
self.send_server(&escape_dot(message_content), MESSAGE_ENDING) self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
} }
/// Sends a string to the server and gets the response /// Sends a string to the server and gets the response
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult { fn send_server(&mut self, string: &str, end: &str) -> EmailResult {
if self.stream.is_none() { if self.stream.is_none() {
return Err(From::from("Connection closed")); return Err(From::from("Connection closed"));
} }
@@ -221,7 +221,7 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
} }
/// Gets the SMTP response /// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult { fn get_reply(&mut self) -> EmailResult {
let mut parser = ResponseParser::new(); let mut parser = ResponseParser::new();

View File

@@ -6,6 +6,7 @@ use std::net::SocketAddr;
use std::net::TcpStream; use std::net::TcpStream;
use std::fmt; use std::fmt;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use openssl::ssl::{SslContext, SslStream}; use openssl::ssl::{SslContext, SslStream};
/// A trait for the concept of opening a stream /// A trait for the concept of opening a stream

View File

@@ -5,9 +5,9 @@ use std::fmt::{Display, Formatter};
use std::fmt; use std::fmt;
use std::collections::HashSet; use std::collections::HashSet;
use response::Response; use transport::smtp::response::Response;
use error::Error; use transport::error::Error;
use authentication::Mecanism; use transport::smtp::authentication::Mecanism;
/// Supported ESMTP keywords /// Supported ESMTP keywords
#[derive(PartialEq,Eq,Hash,Clone,Debug)] #[derive(PartialEq,Eq,Hash,Clone,Debug)]
@@ -126,8 +126,8 @@ mod test {
use std::collections::HashSet; use std::collections::HashSet;
use super::{ServerInfo, Extension}; use super::{ServerInfo, Extension};
use authentication::Mecanism; use transport::smtp::authentication::Mecanism;
use response::{Code, Response, Severity, Category}; use transport::smtp::response::{Code, Response, Severity, Category};
#[test] #[test]
fn test_extension_fmt() { fn test_extension_fmt() {

View File

@@ -5,12 +5,46 @@ use std::net::{SocketAddr, ToSocketAddrs};
use openssl::ssl::{SslMethod, SslContext}; use openssl::ssl::{SslMethod, SslContext};
use SMTP_PORT;
use extension::{Extension, ServerInfo};
use error::{SmtpResult, Error};
use email::SendableEmail; use email::SendableEmail;
use client::Client; use transport::smtp::extension::{Extension, ServerInfo};
use authentication::Mecanism; use transport::error::{EmailResult, Error};
use transport::smtp::client::Client;
use transport::smtp::authentication::Mecanism;
use transport::EmailTransport;
pub mod extension;
pub mod authentication;
pub mod response;
pub mod client;
// Registrated port numbers:
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port
pub static SMTP_PORT: u16 = 25;
/// Default smtps port
pub static SMTPS_PORT: u16 = 465;
/// Default submission port
pub static SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub static SP: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub static CRLF: &'static str = "\r\n";
/// Colon
pub static COLON: &'static str = ":";
/// The ending of message content
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub static NUL: &'static str = "\0";
/// TLS security level /// TLS security level
pub enum SecurityLevel { pub enum SecurityLevel {
@@ -23,7 +57,7 @@ pub enum SecurityLevel {
} }
/// Contains client configuration /// Contains client configuration
pub struct SenderBuilder { pub struct SmtpTransportBuilder {
/// Maximum connection reuse /// Maximum connection reuse
/// ///
/// Zero means no limitation /// Zero means no limitation
@@ -44,14 +78,14 @@ pub struct SenderBuilder {
authentication_mecanisms: Vec<Mecanism>, authentication_mecanisms: Vec<Mecanism>,
} }
/// Builder for the SMTP Sender /// Builder for the SMTP SmtpTransport
impl SenderBuilder { impl SmtpTransportBuilder {
/// Creates a new local SMTP client /// Creates a new local SMTP client
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SenderBuilder, Error> { pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = try!(addr.to_socket_addrs()); let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() { match addresses.next() {
Some(addr) => Ok(SenderBuilder { Some(addr) => Ok(SmtpTransportBuilder {
server_addr: addr, server_addr: addr,
ssl_context: SslContext::new(SslMethod::Tlsv1).unwrap(), ssl_context: SslContext::new(SslMethod::Tlsv1).unwrap(),
security_level: SecurityLevel::Opportunistic, security_level: SecurityLevel::Opportunistic,
@@ -66,57 +100,57 @@ impl SenderBuilder {
} }
/// Creates a new local SMTP client to port 25 /// Creates a new local SMTP client to port 25
pub fn localhost() -> Result<SenderBuilder, Error> { pub fn localhost() -> Result<SmtpTransportBuilder, Error> {
SenderBuilder::new(("localhost", SMTP_PORT)) SmtpTransportBuilder::new(("localhost", SMTP_PORT))
} }
/// Use STARTTLS with a specific context /// Use STARTTLS with a specific context
pub fn ssl_context(mut self, ssl_context: SslContext) -> SenderBuilder { pub fn ssl_context(mut self, ssl_context: SslContext) -> SmtpTransportBuilder {
self.ssl_context = ssl_context; self.ssl_context = ssl_context;
self self
} }
/// Require SSL/TLS using STARTTLS /// Require SSL/TLS using STARTTLS
pub fn security_level(mut self, level: SecurityLevel) -> SenderBuilder { pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder {
self.security_level = level; self.security_level = level;
self self
} }
/// Set the name used during HELO or EHLO /// Set the name used during HELO or EHLO
pub fn hello_name(mut self, name: &str) -> SenderBuilder { pub fn hello_name(mut self, name: &str) -> SmtpTransportBuilder {
self.hello_name = name.to_string(); self.hello_name = name.to_string();
self self
} }
/// Enable connection reuse /// Enable connection reuse
pub fn enable_connection_reuse(mut self, enable: bool) -> SenderBuilder { pub fn enable_connection_reuse(mut self, enable: bool) -> SmtpTransportBuilder {
self.enable_connection_reuse = enable; self.enable_connection_reuse = enable;
self self
} }
/// Set the maximum number of emails sent using one connection /// Set the maximum number of emails sent using one connection
pub fn connection_reuse_count_limit(mut self, limit: u16) -> SenderBuilder { pub fn connection_reuse_count_limit(mut self, limit: u16) -> SmtpTransportBuilder {
self.connection_reuse_count_limit = limit; self.connection_reuse_count_limit = limit;
self self
} }
/// Set the client credentials /// Set the client credentials
pub fn credentials(mut self, username: &str, password: &str) -> SenderBuilder { pub fn credentials(mut self, username: &str, password: &str) -> SmtpTransportBuilder {
self.credentials = Some((username.to_string(), password.to_string())); self.credentials = Some((username.to_string(), password.to_string()));
self self
} }
/// Set the authentication mecanisms /// Set the authentication mecanisms
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SenderBuilder { pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SmtpTransportBuilder {
self.authentication_mecanisms = mecanisms; self.authentication_mecanisms = mecanisms;
self self
} }
/// Build the SMTP client /// Build the SMTP client
/// ///
/// It does not connects to the server, but only creates the `Sender` /// It does not connects to the server, but only creates the `SmtpTransport`
pub fn build(self) -> Sender { pub fn build(self) -> SmtpTransport {
Sender::new(self) SmtpTransport::new(self)
} }
} }
@@ -130,14 +164,14 @@ struct State {
} }
/// Structure that implements the high level SMTP client /// Structure that implements the high level SMTP client
pub struct Sender { pub struct SmtpTransport {
/// Information about the server /// Information about the server
/// Value is None before HELO/EHLO /// Value is None before HELO/EHLO
server_info: Option<ServerInfo>, server_info: Option<ServerInfo>,
/// Sender variable states /// SmtpTransport variable states
state: State, state: State,
/// Information about the client /// Information about the client
client_info: SenderBuilder, client_info: SmtpTransportBuilder,
/// Low level client /// Low level client
client: Client, client: Client,
} }
@@ -157,15 +191,15 @@ macro_rules! try_smtp (
}) })
); );
impl Sender { impl SmtpTransport {
/// Creates a new SMTP client /// Creates a new SMTP client
/// ///
/// It does not connects to the server, but only creates the `Sender` /// It does not connects to the server, but only creates the `SmtpTransport`
pub fn new(builder: SenderBuilder) -> Sender { pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
let client = Client::new(); let client = Client::new();
Sender { SmtpTransport {
client: client, client: client,
server_info: None, server_info: None,
client_info: builder, client_info: builder,
@@ -187,13 +221,8 @@ impl Sender {
self.state.connection_reuse_count = 0; self.state.connection_reuse_count = 0;
} }
/// Closes the inner connection
pub fn close(&mut self) {
self.client.close();
}
/// Gets the EHLO response and updates server information /// Gets the EHLO response and updates server information
pub fn get_ehlo(&mut self) -> SmtpResult { pub fn get_ehlo(&mut self) -> EmailResult {
// Extended Hello // Extended Hello
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self); let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
@@ -204,9 +233,16 @@ impl Sender {
Ok(ehlo_response) Ok(ehlo_response)
} }
}
impl EmailTransport for SmtpTransport {
/// Sends an email /// Sends an email
pub fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult { fn send(&mut self,
to_addresses: Vec<String>,
from_address: String,
message: String,
message_id: String)
-> EmailResult {
// Check if the connection is still available // Check if the connection is still available
if self.state.connection_reuse_count > 0 { if self.state.connection_reuse_count > 0 {
if !self.client.is_connected() { if !self.client.is_connected() {
@@ -260,11 +296,6 @@ impl Sender {
} }
} }
let current_message = try!(email.message_id().ok_or("Missing Message-ID"));
let from_address = try!(email.from_address().ok_or("Missing From address"));
let to_addresses = try!(email.to_addresses().ok_or("Missing To address"));
let message = try!(email.message().ok_or("Missing message"));
// Mail // Mail
let mail_options = match self.server_info let mail_options = match self.server_info
.as_ref() .as_ref()
@@ -277,13 +308,13 @@ impl Sender {
try_smtp!(self.client.mail(&from_address, mail_options), self); try_smtp!(self.client.mail(&from_address, mail_options), self);
// Log the mail command // Log the mail command
info!("{}: from=<{}>", current_message, from_address); info!("{}: from=<{}>", message_id, from_address);
// Recipient // Recipient
for to_address in to_addresses.iter() { for to_address in to_addresses.iter() {
try_smtp!(self.client.rcpt(&to_address), self); try_smtp!(self.client.rcpt(&to_address), self);
// Log the rcpt command // Log the rcpt command
info!("{}: to=<{}>", current_message, to_address); info!("{}: to=<{}>", message_id, to_address);
} }
// Data // Data
@@ -298,7 +329,7 @@ impl Sender {
// Log the message // Log the message
info!("{}: conn_use={}, size={}, status=sent ({})", info!("{}: conn_use={}, size={}, status=sent ({})",
current_message, message_id,
self.state.connection_reuse_count, self.state.connection_reuse_count,
message.len(), message.len(),
result.as_ref() result.as_ref()
@@ -318,4 +349,9 @@ impl Sender {
result result
} }
/// Closes the inner connection
fn close(&mut self) {
self.client.close();
}
} }

View File

@@ -6,7 +6,7 @@ use std::result;
use self::Severity::*; use self::Severity::*;
use self::Category::*; use self::Category::*;
use error::{SmtpResult, Error}; use transport::error::{EmailResult, Error};
/// First digit indicates severity /// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)] #[derive(PartialEq,Eq,Copy,Clone,Debug)]
@@ -192,7 +192,7 @@ impl ResponseParser {
} }
/// Builds a response from a `ResponseParser` /// Builds a response from a `ResponseParser`
pub fn response(self) -> SmtpResult { pub fn response(self) -> EmailResult {
match self.code { match self.code {
Some(code) => Ok(Response::new(code, self.message)), Some(code) => Ok(Response::new(code, self.message)),
None => Err(Error::ResponseParsingError("Incomplete response, could not read \ None => Err(Error::ResponseParsingError("Incomplete response, could not read \

31
src/transport/stub/mod.rs Normal file
View File

@@ -0,0 +1,31 @@
//! TODO
use transport::error::EmailResult;
use transport::smtp::response::Response;
use transport::EmailTransport;
use transport::smtp::response::{Code, Category, Severity};
/// TODO
pub struct StubEmailTransport;
impl EmailTransport for StubEmailTransport {
fn send(&mut self,
to_addresses: Vec<String>,
from_address: String,
message: String,
message_id: String)
-> EmailResult {
let _ = message;
info!("message '{}': from '{}' to '{:?}'",
message_id,
from_address,
to_addresses);
Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
vec!["Ok: email logged".to_string()]))
}
fn close(&mut self) {
()
}
}