Compare commits

..

15 Commits

Author SHA1 Message Date
Alexis Mousset
88b9cfb847 Fix documentation link 2015-10-21 23:16:09 +02:00
Alexis Mousset
250ed7bcf4 Add SMTPUTF8 support 2015-10-21 23:02:14 +02:00
Alexis Mousset
63d9216df3 Change crate name 2015-10-15 00:03:07 +02:00
Alexis Mousset
54758ebde9 Rename rust-smtp to lettre, add multiple transports support 2015-10-14 23:44:25 +02:00
Alexis Mousset
bd67d80d3e Try to authenticate only once 2015-10-12 23:46:12 +02:00
Alexis Mousset
a91db14869 Configurable TLS security level 2015-10-12 22:47:53 +02:00
Alexis Mousset
b5c6663629 Update AppVeyor configuration 2015-10-12 02:50:53 +02:00
Alexis Mousset
4b4150ed99 Version 0.3.0 2015-10-12 02:20:39 +02:00
Alexis Mousset
5f911dce12 Formatting with rustfmt 2015-10-12 02:19:33 +02:00
Alexis Mousset
47d6870d93 Add STARTTLS support instead of SMTPS 2015-10-12 00:59:39 +02:00
Alexis Mousset
51de392086 Add documentation 2015-10-11 21:21:31 +02:00
Alexis Mousset
fefb5f7978 smtps with rust-openssl 2015-10-11 19:56:02 +02:00
Alexis Mousset
5d125bdbdb Format code with rustfmt 2015-10-08 20:12:07 +02:00
Alexis Mousset
a1bf0170db Version 0.2.0 2015-10-06 18:42:35 +02:00
Alexis Mousset
5bedba4b24 Use Tm::rfc822z to support local timezones (workaround for time crate incomplete feature) 2015-10-06 18:42:23 +02:00
20 changed files with 1659 additions and 1349 deletions

View File

@@ -1,23 +1,15 @@
environment:
CARGO_TARGET: x86_64-pc-windows-gnu
matrix:
- TARGET: x86_64-pc-windows-msvc
- TARGET: i686-pc-windows-gnu
matrix:
- TARGET: x86_64-pc-windows-msvc
- TARGET: i686-pc-windows-msvc
- TARGET: x86_64-pc-windows-gnu
- TARGET: i686-pc-windows-gnu
install:
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rustc-nightly-${env:TARGET}.tar.gz"
- ps: Start-FileDownload "https://static.rust-lang.org/cargo-dist/cargo-nightly-${env:CARGO_TARGET}.tar.gz"
- 7z x rustc-nightly-%TARGET%.tar.gz > nul
- 7z x rustc-nightly-%TARGET%.tar > nul
- 7z x cargo-nightly-%CARGO_TARGET%.tar.gz > nul
- 7z x cargo-nightly-%CARGO_TARGET%.tar > nul
- call "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" amd64
- set PATH=%PATH%;%cd%/rustc-nightly-%TARGET%/rustc/bin
- set PATH=%PATH%;%cd%/cargo-nightly-%CARGO_TARGET%/cargo/bin
- SET PATH=%PATH%;C:\MinGW\bin
- rustc -V
- cargo -V
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-nightly-${env:TARGET}.exe" -FileName "rust-nightly.exe"
- ps: .\rust-nightly.exe /VERYSILENT /NORESTART /DIR="C:\rust" | Out-Null
- ps: $env:PATH="$env:PATH;C:\rust\bin"
- rustc -vV
- cargo -vV
build: false
test_script:
- cargo test --verbose --no-default-features
- cargo test --verbose --no-default-features

View File

@@ -1,13 +1,13 @@
[package]
name = "smtp"
version = "0.1.2"
description = "Simple SMTP client"
name = "lettre"
version = "0.4.0"
description = "Email client"
readme = "README.md"
documentation = "http://amousset.me/rust-smtp/smtp/"
repository = "https://github.com/amousset/rust-smtp"
documentation = "http://lettre.github.io/lettre/"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <alexis.mousset@gmx.fr>"]
authors = ["Alexis Mousset <contact@amousset.me>"]
keywords = ["email", "smtp", "mailer"]
[dependencies]
@@ -18,6 +18,7 @@ rustc-serialize = "0.3"
rust-crypto = "0.2"
bufstream = "0.1"
email = "0.0"
openssl = "0.6"
[dev-dependencies]
env_logger = "0.3"

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/lettre/lettre/badge.svg?branch=master&service=github)](https://coveralls.io/github/lettre/lettre?branch=master) [![Crate](https://meritbadge.herokuapp.com/lettre)](https://crates.io/crates/lettre) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
=========
This library implements a simple SMTP client.
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information.
This is an email library written in Rust.
See the [documentation](http://lettre.github.io/lettre) for more information.
Install
-------
@@ -11,7 +11,7 @@ To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
smtp = "0.1"
lettre = "0.4"
```
License

View File

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

View File

@@ -1,23 +0,0 @@
//! A trait to represent a stream
use std::io;
use std::net::SocketAddr;
use std::net::TcpStream;
/// A trait for the concept of opening a stream
pub trait Connector {
/// Opens a connection to the given IP socket
fn connect(addr: &SocketAddr) -> io::Result<Self>;
}
impl Connector for SmtpStream {
fn connect(addr: &SocketAddr) -> io::Result<SmtpStream> {
TcpStream::connect(addr)
}
}
/// Represents an atual SMTP network stream
//Used later for ssl
pub type SmtpStream = TcpStream;

View File

@@ -1,6 +1,7 @@
//! 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 time::{now, Tm};
@@ -53,8 +54,12 @@ impl<'a> ToMailbox for (&'a str, &'a str) {
/// Builds an `Email` structure
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct EmailBuilder {
/// Email content
content: Email,
/// Message
message: MimeMessage,
/// The enveloppe recipients addresses
to: Vec<String>,
/// The enveloppe sender address
from: Option<String>,
/// Date issued
date_issued: bool,
}
@@ -67,45 +72,31 @@ pub struct Email {
/// The enveloppe recipients addresses
to: Vec<String>,
/// The enveloppe sender address
from: Option<String>,
from: String,
/// Message-ID
message_id: Uuid,
}
impl Display for Email {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
self.message.as_string()
)
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.message.as_string())
}
}
impl EmailBuilder {
/// Creates a new empty email
pub fn new() -> EmailBuilder {
let current_message = Uuid::new_v4();
let mut email = Email {
EmailBuilder {
message: MimeMessage::new_blank_message(),
to: vec![],
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,
}
}
/// Sets the email body
pub fn body(mut self, body: &str) -> EmailBuilder {
self.content.message.body = body.to_string();
self.message.body = body.to_string();
self
}
@@ -116,14 +107,14 @@ impl EmailBuilder {
}
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
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox();
self.insert_header(("From", mailbox.to_string().as_ref()));
self.content.from = Some(mailbox.address);
self.from = Some(mailbox.address);
self
}
@@ -131,7 +122,7 @@ impl EmailBuilder {
pub fn to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox();
self.insert_header(("To", mailbox.to_string().as_ref()));
self.content.to.push(mailbox.address);
self.to.push(mailbox.address);
self
}
@@ -139,7 +130,7 @@ impl EmailBuilder {
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox();
self.insert_header(("Cc", mailbox.to_string().as_ref()));
self.content.to.push(mailbox.address);
self.to.push(mailbox.address);
self
}
@@ -154,7 +145,7 @@ impl EmailBuilder {
pub fn sender<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox();
self.insert_header(("Sender", mailbox.to_string().as_ref()));
self.content.from = Some(mailbox.address);
self.from = Some(mailbox.address);
self
}
@@ -166,18 +157,40 @@ impl EmailBuilder {
/// Adds a `Date` header with the given date
pub fn date(mut self, date: &Tm) -> EmailBuilder {
self.insert_header(("Date", Tm::rfc822(date).to_string().as_ref()));
self.insert_header(("Date", Tm::rfc822z(date).to_string().as_ref()));
self.date_issued = true;
self
}
/// Build the Email
pub fn build(mut self) -> Email {
if !self.date_issued {
self.insert_header(("Date", Tm::rfc822(&now()).to_string().as_ref()));
pub fn build(mut self) -> Result<Email, &'static str> {
if self.from.is_none() {
return Err("No from address")
}
self.content.message.update_headers();
self.content
if self.to.is_empty() {
return Err("No to address")
}
if !self.date_issued {
self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
}
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,
})
}
}
@@ -185,13 +198,13 @@ impl EmailBuilder {
/// Email sendable by an SMTP client
pub trait SendableEmail {
/// From address
fn from_address(&self) -> Option<String>;
fn from_address(&self) -> String;
/// To addresses
fn to_addresses(&self) -> Option<Vec<String>>;
fn to_addresses(&self) -> Vec<String>;
/// Message content
fn message(&self) -> Option<String>;
fn message(&self) -> String;
/// Message ID
fn message_id(&self) -> Option<String>;
fn message_id(&self) -> String;
}
/// Minimal email structure
@@ -216,47 +229,38 @@ impl SimpleSendableEmail {
}
impl SendableEmail for SimpleSendableEmail {
fn from_address(&self) -> Option<String> {
Some(self.from.clone())
fn from_address(&self) -> String {
self.from.clone()
}
fn to_addresses(&self) -> Option<Vec<String>> {
Some(self.to.clone())
fn to_addresses(&self) -> Vec<String> {
self.to.clone()
}
fn message(&self) -> Option<String> {
Some(self.message.clone())
fn message(&self) -> String {
self.message.clone()
}
fn message_id(&self) -> Option<String> {
Some(format!("<{}@rust-smtp>", Uuid::new_v4()))
fn message_id(&self) -> String {
format!("{}", Uuid::new_v4())
}
}
impl SendableEmail for Email {
/// Return the to addresses, and fails if it is not set
fn to_addresses(&self) -> Option<Vec<String>> {
if self.to.is_empty() {
None
} else {
Some(self.to.clone())
}
fn to_addresses(&self) -> Vec<String> {
self.to.clone()
}
/// Return the from address, and fails if it is not set
fn from_address(&self) -> Option<String> {
match self.from {
Some(ref from_address) => Some(from_address.clone()),
None => None,
}
fn from_address(&self) -> String {
self.from.clone()
}
fn message(&self) -> Option<String> {
Some(format!("{}", self))
fn message(&self) -> String {
format!("{}", self)
}
fn message_id(&self) -> Option<String> {
Some(format!("{}", self.message_id))
fn message_id(&self) -> String {
format!("{}", self.message_id)
}
}
@@ -276,27 +280,26 @@ mod test {
let mut email = Email {
message: MimeMessage::new_blank_message(),
to: vec![],
from: None,
from: "".to_string(),
message_id: current_message,
};
email.message.headers.insert(
Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>", current_message)
).unwrap()
);
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>",
current_message))
.unwrap());
email.message.headers.insert(
Header::new_with_value("To".to_string(), "to@example.com".to_string()).unwrap()
);
email.message
.headers
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
.unwrap());
email.message.body = "body".to_string();
assert_eq!(
format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n", current_message)
);
assert_eq!(current_message.to_string(), email.message_id().unwrap());
assert_eq!(format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
current_message));
assert_eq!(current_message.to_string(), email.message_id());
}
#[test]
@@ -305,21 +308,24 @@ mod test {
let date_now = now();
let email = email_builder.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.add_header(("X-test", "value"))
.build();
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.add_header(("X-test", "value"))
.build()
.unwrap();
assert_eq!(
format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: <user@localhost>\r\nFrom: <user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: <reply@localhost>\r\nSender: <sender@localhost>\r\nDate: {}\r\nSubject: Hello\r\nX-test: value\r\n\r\nHello World!\r\n",
email.message_id().unwrap(), date_now.rfc822())
);
assert_eq!(format!("{}", email),
format!("To: <user@localhost>\r\nFrom: <user@localhost>\r\nCc: \"Alias\" \
<cc@localhost>\r\nReply-To: <reply@localhost>\r\nSender: \
<sender@localhost>\r\nDate: {}\r\nSubject: Hello\r\nX-test: \
value\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nHello World!\r\n",
date_now.rfc822z(),
email.message_id()));
}
#[test]
@@ -328,28 +334,21 @@ mod test {
let date_now = now();
let email = email_builder.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.add_header(("X-test", "value"))
.build();
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.add_header(("X-test", "value"))
.build()
.unwrap();
assert_eq!(
email.from_address().unwrap(),
"sender@localhost".to_string()
);
assert_eq!(
email.to_addresses().unwrap(),
vec!["user@localhost".to_string(), "cc@localhost".to_string()]
);
assert_eq!(
email.message().unwrap(),
format!("{}", email)
);
assert_eq!(email.from_address(), "sender@localhost".to_string());
assert_eq!(email.to_addresses(),
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
assert_eq!(email.message(), format!("{}", email));
}
}

View File

@@ -1,197 +0,0 @@
//! ESMTP features
use std::result::Result;
use std::fmt::{Display, Formatter};
use std::fmt;
use std::collections::HashSet;
use response::Response;
use error::Error;
use authentication::Mecanism;
/// Supported ESMTP keywords
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
pub enum Extension {
/// 8BITMIME keyword
///
/// RFC 6152: https://tools.ietf.org/html/rfc6152
EightBitMime,
/// SMTPUTF8 keyword
///
/// RFC 6531: https://tools.ietf.org/html/rfc6531
SmtpUtfEight,
/// STARTTLS keyword
///
/// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls,
/// AUTH mecanism
Authentication(Mecanism),
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
Extension::StartTls => write!(f, "{}", "STARTTLS"),
Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism),
}
}
}
/// Contains information about an SMTP server
#[derive(Clone,Debug,Eq,PartialEq)]
pub struct ServerInfo {
/// Server name
///
/// The name given in the server banner
pub name: String,
/// ESMTP features supported by the server
///
/// It contains the features supported by the server and known by the `Extension` module.
pub features: HashSet<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{} with {}",
self.name,
match self.features.is_empty() {
true => "no supported features".to_string(),
false => format! ("{:?}", self.features),
}
)
}
}
impl ServerInfo {
/// Parses a response to create a `ServerInfo`
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(Error::ResponseParsingError("Could not read server name"))
};
let mut features: HashSet<Extension> = HashSet::new();
for line in response.message() {
let splitted : Vec<&str> = line.split_whitespace().collect();
let _ = match splitted[0] {
"8BITMIME" => {features.insert(Extension::EightBitMime);},
"SMTPUTF8" => {features.insert(Extension::SmtpUtfEight);},
"STARTTLS" => {features.insert(Extension::StartTls);},
"AUTH" => {
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => {features.insert(Extension::Authentication(Mecanism::Plain));},
"CRAM-MD5" => {features.insert(Extension::Authentication(Mecanism::CramMd5));},
_ => (),
}
}
},
_ => (),
};
}
Ok(ServerInfo{
name: name,
features: features,
})
}
/// Checks if the server supports an ESMTP feature
pub fn supports_feature(&self, keyword: &Extension) -> bool {
self.features.contains(keyword)
}
/// Checks if the server supports an ESMTP feature
pub fn supports_auth_mecanism(&self, mecanism: Mecanism) -> bool {
self.features.contains(&Extension::Authentication(mecanism))
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::{ServerInfo, Extension};
use authentication::Mecanism;
use response::{Code, Response, Severity, Category};
#[test]
fn test_extension_fmt() {
assert_eq!(format!("{}", Extension::EightBitMime), "8BITMIME".to_string());
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)), "AUTH PLAIN".to_string());
}
#[test]
fn test_serverinfo_fmt() {
let mut eightbitmime = HashSet::new();
assert!(eightbitmime.insert(Extension::EightBitMime));
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
features: eightbitmime.clone()
}), "name with {EightBitMime}".to_string());
let empty = HashSet::new();
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
features: empty,
}), "name with no supported features".to_string());
let mut plain = HashSet::new();
assert!(plain.insert(Extension::Authentication(Mecanism::Plain)));
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
features: plain.clone()
}), "name with {Authentication(Plain)}".to_string());
}
#[test]
fn test_serverinfo() {
let response = Response::new(
Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
);
let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo {
name: "me".to_string(),
features: features,
};
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
assert!(server_info.supports_feature(&Extension::EightBitMime));
assert!(!server_info.supports_feature(&Extension::StartTls));
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
let response2 = Response::new(
Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
vec!["me".to_string(), "AUTH PLAIN CRAM-MD5 OTHER".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
);
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
let server_info2 = ServerInfo {
name: "me".to_string(),
features: features2,
};
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
assert!(server_info2.supports_feature(&Extension::EightBitMime));
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain));
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5));
assert!(!server_info2.supports_feature(&Extension::StartTls));
}
}

View File

@@ -1,17 +1,14 @@
//! # Rust SMTP client
//! # Rust email client
//!
//! 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
//! relay email server, as it relies as much as possible on the relay server for sanity and RFC compliance
//! checks.
//! relay email server, as it relies as much as possible on the relay server for sanity and RFC
//! compliance checks.
//!
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms
//!
//! 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))
//!
@@ -19,8 +16,8 @@
//!
//! This client is divided into three main parts:
//!
//! * client: a low level SMTP client providing all SMTP commands
//! * sender: a high level SMTP client providing an easy method to send emails
//! * transport: a low level SMTP client providing all SMTP commands
//! * mailer: a high level SMTP client providing an easy method to send emails
//! * email: generates the email to be sent with the sender
//!
//! ## Usage
@@ -30,8 +27,10 @@
//! This is the most basic example of usage:
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::EmailBuilder;
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
//! use lettre::email::EmailBuilder;
//! use lettre::transport::EmailTransport;
//! use lettre::mailer::Mailer;
//!
//! // Create an email
//! let email = EmailBuilder::new()
@@ -41,12 +40,12 @@
//! .from("user@example.com")
//! .subject("Hi, Hello world")
//! .body("Hello world.")
//! .build();
//! .build().unwrap();
//!
//! // 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
//! let result = sender.send(email);
//! let result = mailer.send(email);
//!
//! assert!(result.is_ok());
//! ```
@@ -54,9 +53,12 @@
//! ### Complete example
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::EmailBuilder;
//! use smtp::authentication::Mecanism;
//! use lettre::email::EmailBuilder;
//! use lettre::transport::smtp::{SecurityLevel, SmtpTransport, SmtpTransportBuilder};
//! use lettre::transport::smtp::authentication::Mecanism;
//! use lettre::transport::smtp::SUBMISSION_PORT;
//! use lettre::transport::EmailTransport;
//! use lettre::mailer::Mailer;
//!
//! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name"));
@@ -69,28 +71,33 @@
//! builder = builder.reply_to("contact@example.com");
//! 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
//! let mut sender = SenderBuilder::new(("server.tld", 10025)).unwrap()
//! let mut mailer = Mailer::new(SmtpTransportBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap()
//! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name("my.hostname.tld")
//! // Add credentials for authentication
//! .credentials("username", "password")
//! // Specify a TLS security level. You can also specify an SslContext with
//! // .ssl_context(SslContext::Ssl23)
//! .security_level(SecurityLevel::AlwaysEncrypt)
//! // Enable SMTPUTF8 is the server supports it
//! .smtp_utf8(true)
//! // Configure accepted authetication mecanisms
//! .authentication_mecanisms(vec![Mecanism::CramMd5])
//! // Enable connection reuse
//! .enable_connection_reuse(true).build();
//! .connection_reuse(true).build());
//!
//! let result_1 = sender.send(email.clone());
//! let result_1 = mailer.send(email.clone());
//! assert!(result_1.is_ok());
//!
//! // 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());
//!
//! // Explicitely close the SMTP transaction as we enabled connection reuse
//! sender.close();
//! mailer.close();
//! ```
//!
//! ### Using the client directly
@@ -98,8 +105,10 @@
//! If you just want to send an email without using `Email` to provide headers:
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::SimpleSendableEmail;
//! use lettre::email::SimpleSendableEmail;
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
//! use lettre::transport::EmailTransport;
//! use lettre::mailer::Mailer;
//!
//! // Create a minimal email
//! let email = SimpleSendableEmail::new(
@@ -108,8 +117,8 @@
//! "Hello world !"
//! );
//!
//! let mut sender = SenderBuilder::localhost().unwrap().build();
//! let result = sender.send(email);
//! let mut mailer = Mailer::new(SmtpTransportBuilder::localhost().unwrap().build());
//! let result = mailer.send(email);
//! assert!(result.is_ok());
//! ```
//!
@@ -118,13 +127,12 @@
//! You can also send commands, here is a simple email transaction without error handling:
//!
//! ```rust,no_run
//! use smtp::client::Client;
//! use smtp::client::net::SmtpStream;
//! use smtp::SMTP_PORT;
//! use std::net::TcpStream;
//! use lettre::transport::smtp::SMTP_PORT;
//! use lettre::transport::smtp::client::Client;
//! use lettre::transport::smtp::client::net::NetworkStream;
//!
//! let mut email_client: Client<SmtpStream> = Client::new(("localhost", SMTP_PORT)).unwrap();
//! let _ = email_client.connect();
//! let mut email_client: Client<NetworkStream> = Client::new();
//! let _ = email_client.connect(&("localhost", SMTP_PORT));
//! let _ = email_client.ehlo("my_hostname");
//! let _ = email_client.mail("user@example.com", None);
//! let _ = email_client.rcpt("user@example.org");
@@ -135,47 +143,16 @@
#![deny(missing_docs)]
#[macro_use] extern crate log;
#[macro_use]
extern crate log;
extern crate rustc_serialize as serialize;
extern crate crypto;
extern crate time;
extern crate uuid;
extern crate email as email_format;
extern crate bufstream;
extern crate openssl;
mod extension;
pub mod client;
pub mod sender;
pub mod response;
pub mod error;
pub mod authentication;
pub mod transport;
pub mod email;
// 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";
pub mod mailer;

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

@@ -1,558 +0,0 @@
//! SMTP response, containing a mandatory return code and an optional text message
use std::str::FromStr;
use std::fmt::{Display, Formatter, Result};
use std::result;
use self::Severity::*;
use self::Category::*;
use error::{SmtpResult, Error};
/// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Severity {
/// 2yx
PositiveCompletion,
/// 3yz
PositiveIntermediate,
/// 4yz
TransientNegativeCompletion,
/// 5yz
PermanentNegativeCompletion,
}
impl FromStr for Severity {
type Err = Error;
fn from_str(s: &str) -> result::Result<Severity, Error> {
match s {
"2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
}
}
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
match *self {
PositiveCompletion => 2,
PositiveIntermediate => 3,
TransientNegativeCompletion => 4,
PermanentNegativeCompletion => 5,
}
)
}
}
/// Second digit
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Category {
/// x0z
Syntax,
/// x1z
Information,
/// x2z
Connections,
/// x3z
Unspecified3,
/// x4z
Unspecified4,
/// x5z
MailSystem,
}
impl FromStr for Category {
type Err = Error;
fn from_str(s: &str) -> result::Result<Category, Error> {
match s {
"0" => Ok(Syntax),
"1" => Ok(Information),
"2" => Ok(Connections),
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"5" => Ok(MailSystem),
_ => Err(Error::ResponseParsingError("Second digit must be between 0 and 5")),
}
}
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
match *self {
Syntax => 0,
Information => 1,
Connections => 2,
Unspecified3 => 3,
Unspecified4 => 4,
MailSystem => 5,
}
)
}
}
/// Represents a 3 digit SMTP response code
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Code {
/// First digit of the response code
severity: Severity,
/// Second digit of the response code
category: Category,
/// Third digit
detail: u8,
}
impl FromStr for Code {
type Err = Error;
#[inline]
fn from_str(s: &str) -> result::Result<Code, Error> {
if s.len() == 3 {
match (s[0..1].parse::<Severity>(), s[1..2].parse::<Category>(), s[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {severity: severity, category: category, detail: detail}),
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
}
} else {
Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"))
}
}
}
impl Code {
/// Creates a new `Code` structure
pub fn new(severity: Severity, category: Category, detail: u8) -> Code {
Code {
severity: severity,
category: category,
detail: detail,
}
}
/// Returns the reply code
pub fn code(&self) -> String {
format!("{}{}{}", self.severity, self.category, self.detail)
}
}
/// Parses an SMTP response
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct ResponseParser {
/// Response code
code: Option<Code>,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>
}
impl ResponseParser {
/// Creates a new parser
pub fn new() -> ResponseParser {
ResponseParser {
code: None,
message: vec![],
}
}
/// Parses a line and return a `bool` indicating if there are more lines to come
pub fn read_line(&mut self, line: &str) -> result::Result<bool, Error> {
if line.len() < 3 {
return Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"));
}
match self.code {
Some(ref code) => {
if code.code() != line[0..3] {
return Err(Error::ResponseParsingError("Response code has changed during a reponse"));
}
},
None => self.code = Some(try!(line[0..3].parse::<Code>()))
}
if line.len() > 4 {
self.message.push(line[4..].to_string());
if line.as_bytes()[3] == '-' as u8 {
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
}
}
/// Builds a response from a `ResponseParser`
pub fn response(self) -> SmtpResult {
match self.code {
Some(code) => Ok(Response::new(code, self.message)),
None => Err(Error::ResponseParsingError("Incomplete response, could not read response code"))
}
}
}
/// Contains an SMTP reply, with separed code and message
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Response {
/// Response code
code: Code,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>
}
impl Response {
/// Creates a new `Response`
pub fn new(code: Code, message: Vec<String>) -> Response {
Response {
code: code,
message: message,
}
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.code.severity {
PositiveCompletion => true,
PositiveIntermediate => true,
_ => false,
}
}
/// Returns the message
pub fn message(&self) -> Vec<String> {
self.message.clone()
}
/// Returns the severity (i.e. 1st digit)
pub fn severity(&self) -> Severity {
self.code.severity
}
/// Returns the category (i.e. 2nd digit)
pub fn category(&self) -> Category {
self.code.category
}
/// Returns the detail (i.e. 3rd digit)
pub fn detail(&self) -> u8 {
self.code.detail
}
/// Returns the reply code
fn code(&self) -> String {
self.code.code()
}
/// Tests code equality
pub fn has_code(&self, code: u16) -> bool {
self.code() == format!("{}", code)
}
/// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<String> {
match self.message.is_empty() {
true => None,
false => match self.message[0].split_whitespace().next() {
Some(word) => Some(word.to_string()),
None => None,
}
}
}
}
#[cfg(test)]
mod test {
use super::{Severity, Category, Response, ResponseParser, Code};
#[test]
fn test_severity_from_str() {
assert_eq!("2".parse::<Severity>().unwrap(), Severity::PositiveCompletion);
assert_eq!("4".parse::<Severity>().unwrap(), Severity::TransientNegativeCompletion);
assert!("1".parse::<Severity>().is_err());
}
#[test]
fn test_severity_fmt() {
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
}
#[test]
fn test_category_from_str() {
assert_eq!("2".parse::<Category>().unwrap(), Category::Connections);
assert_eq!("4".parse::<Category>().unwrap(), Category::Unspecified4);
assert!("6".parse::<Category>().is_err());
}
#[test]
fn test_category_fmt() {
assert_eq!(format!("{}", Category::Unspecified4), "4");
}
#[test]
fn test_code_new() {
assert_eq!(
Code::new(Severity::TransientNegativeCompletion, Category::Connections, 0),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 0,
}
);
}
#[test]
fn test_code_from_str() {
assert_eq!(
"421".parse::<Code>().unwrap(),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
}
);
}
#[test]
fn test_code_code() {
let code = Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
};
assert_eq!(code.code(), "421");
}
#[test]
fn test_response_new() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
), Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
});
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![]
), Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec![],
});
}
#[test]
fn test_response_parser() {
let mut parser = ResponseParser::new();
assert!(parser.read_line("250-me").unwrap());
assert!(parser.read_line("250-8BITMIME").unwrap());
assert!(parser.read_line("250-SIZE 42").unwrap());
assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap());
let response = parser.response().unwrap();
assert_eq!(
response,
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: 0,
},
message: vec!["me".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "AUTH PLAIN CRAM-MD5".to_string()],
}
);
}
#[test]
fn test_response_is_positive() {
assert!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
assert!(! Response::new(
Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
}
#[test]
fn test_response_message() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
let empty_message: Vec<String> = vec![];
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![]
).message(), empty_message);
}
#[test]
fn test_response_severity() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PositiveCompletion);
assert_eq!(Response::new(
Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PermanentNegativeCompletion);
}
#[test]
fn test_response_category() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).category(), Category::Unspecified4);
}
#[test]
fn test_response_detail() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).detail(), 1);
}
#[test]
fn test_response_code() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).code(), "241");
}
#[test]
fn test_response_has_code() {
assert!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(241));
assert!(! Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(251));
}
#[test]
fn test_response_first_word() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![]
).first_word(), None);
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![" ".to_string()]
).first_word(), None);
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![" ".to_string()]
).first_word(), None);
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["".to_string()]
).first_word(), None);
}
}

View File

@@ -1,269 +0,0 @@
//! Sends an email using the client
use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use SMTP_PORT;
use extension::{Extension, ServerInfo};
use error::{SmtpResult, Error};
use email::SendableEmail;
use client::Client;
use client::net::SmtpStream;
use authentication::Mecanism;
/// Contains client configuration
pub struct SenderBuilder {
/// Maximum connection reuse
///
/// Zero means no limitation
connection_reuse_count_limit: u16,
/// Enable connection reuse
enable_connection_reuse: bool,
/// Name sent during HELO or EHLO
hello_name: String,
/// Credentials
credentials: Option<(String, String)>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// List of authentication mecanism, sorted by priority
authentication_mecanisms: Vec<Mecanism>,
}
/// Builder for the SMTP Sender
impl SenderBuilder {
/// Creates a new local SMTP client
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SenderBuilder, Error> {
let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() {
Some(addr) => Ok(SenderBuilder {
server_addr: addr,
credentials: None,
connection_reuse_count_limit: 100,
enable_connection_reuse: false,
hello_name: "localhost".to_string(),
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
}),
None => Err(From::from("Could nor resolve hostname")),
}
}
/// Creates a new local SMTP client to port 25
pub fn localhost() -> Result<SenderBuilder, Error> {
SenderBuilder::new(("localhost", SMTP_PORT))
}
/// Set the name used during HELO or EHLO
pub fn hello_name(mut self, name: &str) -> SenderBuilder {
self.hello_name = name.to_string();
self
}
/// Enable connection reuse
pub fn enable_connection_reuse(mut self, enable: bool) -> SenderBuilder {
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) -> SenderBuilder {
self.connection_reuse_count_limit = limit;
self
}
/// Set the client credentials
pub fn credentials(mut self, username: &str, password: &str) -> SenderBuilder {
self.credentials = Some((username.to_string(), password.to_string()));
self
}
/// Set the authentication mecanisms
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SenderBuilder {
self.authentication_mecanisms = mecanisms;
self
}
/// Build the SMTP client
///
/// It does not connects to the server, but only creates the `Sender`
pub fn build(self) -> Sender {
Sender::new(self)
}
}
/// Represents the state of a client
#[derive(Debug)]
struct State {
/// Panic state
pub panic: bool,
/// Connection reuse counter
pub connection_reuse_count: u16,
}
/// Structure that implements the high level SMTP client
pub struct Sender {
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
/// Sender variable states
state: State,
/// Information about the client
client_info: SenderBuilder,
/// Low level client
client: Client<SmtpStream>,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.reset();
}
return Err(err)
},
}
})
);
impl Sender {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Sender`
pub fn new(builder: SenderBuilder) -> Sender {
let client: Client<SmtpStream> = Client::new(builder.server_addr).unwrap();
Sender{
client: client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
/// Reset the client state
fn reset(&mut self) {
// Close the SMTP transaction if needed
self.close();
// Reset the client state
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
}
/// Closes the inner connection
pub fn close(&mut self) {
self.client.close();
}
/// Sends an email
pub fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
// Check if the connection is still available
if self.state.connection_reuse_count > 0 {
if !self.client.is_connected() {
self.reset();
}
}
// If there is a usable connection, test if the server answers and hello has been sent
if self.state.connection_reuse_count == 0 {
try!(self.client.connect());
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
// Extended Hello or Hello if needed
let hello_response = match self.client.ehlo(&self.client_info.hello_name) {
Ok(response) => response,
Err(error) => match error {
Error::PermanentError(ref response) if response.has_code(550) => {
match self.client.helo(&self.client_info.hello_name) {
Ok(response) => response,
Err(error) => try_smtp!(Err(error), self)
}
},
_ => {
try_smtp!(Err(error), self)
},
},
};
self.server_info = Some(try_smtp!(ServerInfo::from_response(&hello_response), self));
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
}
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
let (username, password) = self.client_info.credentials.clone().unwrap();
let mut found = false;
for mecanism in self.client_info.authentication_mecanisms.clone() {
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
found = true;
let result = self.client.auth(mecanism, &username, &password);
try_smtp!(result, self);
}
}
if !found {
debug!("No supported authentication mecanisms available");
}
}
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
let mail_options = match self.server_info.as_ref().unwrap().supports_feature(&Extension::EightBitMime) {
true => Some("BODY=8BITMIME"),
false => None,
};
try_smtp!(self.client.mail(&from_address, mail_options), self);
// Log the mail command
info!("{}: from=<{}>", current_message, from_address);
// Recipient
for to_address in to_addresses.iter() {
try_smtp!(self.client.rcpt(&to_address), self);
// Log the rcpt command
info!("{}: to=<{}>", current_message, to_address);
}
// Data
try_smtp!(self.client.data(), self);
// Message content
let result = self.client.message(&message);
if result.is_ok() {
// Increment the connection reuse counter
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
// Log the message
info!("{}: conn_use={}, size={}, status=sent ({})", current_message,
self.state.connection_reuse_count, message.len(),
result.as_ref().ok().unwrap().message().iter().next().unwrap_or(&"no response".to_string())
);
}
// Test if we can reuse the existing connection
if (!self.client_info.enable_connection_reuse) ||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
self.reset();
}
result
}
}

View File

@@ -5,7 +5,7 @@ use std::io;
use std::fmt::{Display, Formatter};
use std::fmt;
use response::{Severity, Response};
use transport::smtp::response::{Severity, Response};
use serialize::base64::FromBase64Error;
use self::Error::*;
@@ -70,7 +70,7 @@ impl From<Response> for Error {
match response.severity() {
Severity::TransientNegativeCompletion => TransientError(response),
Severity::PermanentNegativeCompletion => PermanentError(response),
_ => ClientError("Unknown error code")
_ => ClientError("Unknown error code"),
}
}
}
@@ -82,7 +82,7 @@ impl From<&'static str> for Error {
}
/// SMTP result type
pub type SmtpResult = Result<Response, Error>;
pub type EmailResult = Result<Response, Error>;
#[cfg(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::mac::Mac;
use NUL;
use error::Error;
use transport::smtp::NUL;
use transport::error::Error;
/// Represents authentication mecanisms
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
@@ -25,12 +25,12 @@ pub enum Mecanism {
impl Display for Mecanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}",
match *self {
Mecanism::Plain => "PLAIN",
Mecanism::CramMd5 => "CRAM-MD5",
}
)
write!(f,
"{}",
match *self {
Mecanism::Plain => "PLAIN",
Mecanism::CramMd5 => "CRAM-MD5",
})
}
}
@@ -43,15 +43,22 @@ impl Mecanism {
}
}
/// Returns the string to send to the server, using the provided username, password and challenge in some cases
pub fn response(&self, username: &str, password: &str, challenge: Option<&str>) -> Result<String, Error> {
/// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases
pub fn response(&self,
username: &str,
password: &str,
challenge: Option<&str>)
-> Result<String, Error> {
match *self {
Mecanism::Plain => {
match challenge {
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")),
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD)),
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
.as_bytes()
.to_base64(base64::STANDARD)),
}
},
}
Mecanism::CramMd5 => {
let encoded_challenge = match challenge {
Some(challenge) => challenge,
@@ -66,8 +73,10 @@ impl Mecanism {
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
hmac.input(&decoded_challenge);
Ok(format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD))
},
Ok(format!("{} {}", username, hmac.result().code().to_hex())
.as_bytes()
.to_base64(base64::STANDARD))
}
}
}
}
@@ -80,7 +89,8 @@ mod test {
fn test_plain() {
let mecanism = Mecanism::Plain;
assert_eq!(mecanism.response("username", "password", None).unwrap(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
assert_eq!(mecanism.response("username", "password", None).unwrap(),
"AHVzZXJuYW1lAHBhc3N3b3Jk");
assert!(mecanism.response("username", "password", Some("test")).is_err());
}
@@ -88,9 +98,11 @@ mod test {
fn test_cram_md5() {
let mecanism = Mecanism::CramMd5;
assert_eq!(mecanism.response("alice", "wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")).unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
assert_eq!(mecanism.response("alice",
"wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
.unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
assert!(mecanism.response("alice", "wonderland", Some("tést")).is_err());
assert!(mecanism.response("alice", "wonderland", None).is_err());
}

View File

@@ -1,16 +1,19 @@
//! SMTP client
use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use std::net::ToSocketAddrs;
use std::io::{BufRead, Read, Write};
use std::io;
use std::fmt::Debug;
use bufstream::BufStream;
use openssl::ssl::SslContext;
use response::ResponseParser;
use authentication::Mecanism;
use error::{Error, SmtpResult};
use client::net::{Connector, SmtpStream};
use {CRLF, MESSAGE_ENDING};
use transport::smtp::response::ResponseParser;
use transport::smtp::authentication::Mecanism;
use transport::error::{Error, EmailResult};
use transport::smtp::client::net::{Connector, NetworkStream};
use transport::smtp::{CRLF, MESSAGE_ENDING};
pub mod net;
@@ -23,8 +26,9 @@ fn escape_dot(string: &str) -> String {
format!(".{}", string)
} else {
string.to_string()
}.replace("\r.", "\r..")
.replace("\n.", "\n..")
}
.replace("\r.", "\r..")
.replace("\n.", "\n..")
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
@@ -40,12 +44,11 @@ fn remove_crlf(string: &str) -> String {
}
/// Structure that implements the SMTP client
pub struct Client<S: Write + Read = SmtpStream> {
#[derive(Debug)]
pub struct Client<S: Write + Read = NetworkStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<BufStream<S>>,
/// Socket we are connecting to
server_addr: SocketAddr,
}
macro_rules! return_err (
@@ -54,39 +57,53 @@ macro_rules! return_err (
})
);
impl<S: Write + Read = SmtpStream> Client<S> {
impl<S: Write + Read = NetworkStream> Client<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<Client<S>, Error> {
let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() {
Some(addr) => Ok(Client {
stream: None,
server_addr: addr,
}),
None => Err(From::from("Could nor resolve hostname")),
}
pub fn new() -> Client<S> {
Client { stream: None }
}
}
impl<S: Connector + Write + Read = SmtpStream> Client<S> {
impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.quit();
self.stream = None;
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: S) {
self.stream = Some(BufStream::new(stream));
}
/// Upgrades the underlying connection to SSL/TLS
pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> {
//let current_stream = self.stream.clone();
if self.stream.is_some() {
self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context)
} else {
Ok(())
}
}
/// Connects to the configured server
pub fn connect(&mut self) -> SmtpResult {
pub fn connect<A: ToSocketAddrs>(&mut self, addr: &A) -> EmailResult {
// Connect should not be called when the client is already connected
if self.stream.is_some() {
return_err!("The connection is already established", self);
}
let mut addresses = try!(addr.to_socket_addrs());
let server_addr = match addresses.next() {
Some(addr) => addr,
None => return_err!("Could not resolve hostname", self),
};
// Try to connect
self.stream = Some(BufStream::new(try!(Connector::connect(&self.server_addr))));
self.set_stream(try!(Connector::connect(&server_addr, None)));
self.get_reply()
}
@@ -97,22 +114,17 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
}
/// 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)
}
/// Send a HELO command and fills `server_info`
pub fn helo(&mut self, hostname: &str) -> SmtpResult {
self.command(&format!("HELO {}", hostname))
}
/// Sends a EHLO command and fills `server_info`
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
/// Sends a EHLO command
pub fn ehlo(&mut self, hostname: &str) -> EmailResult {
self.command(&format!("EHLO {}", hostname))
}
/// 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 {
Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)),
None => self.command(&format!("MAIL FROM:<{}>", address)),
@@ -120,27 +132,27 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
}
/// 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))
}
/// Sends a DATA command
pub fn data(&mut self) -> SmtpResult {
pub fn data(&mut self) -> EmailResult {
self.command("DATA")
}
/// Sends a QUIT command
pub fn quit(&mut self) -> SmtpResult {
pub fn quit(&mut self) -> EmailResult {
self.command("QUIT")
}
/// Sends a NOOP command
pub fn noop(&mut self) -> SmtpResult {
pub fn noop(&mut self) -> EmailResult {
self.command("NOOP")
}
/// Sends a HELP command
pub fn help(&mut self, argument: Option<&str>) -> SmtpResult {
pub fn help(&mut self, argument: Option<&str>) -> EmailResult {
match argument {
Some(ref argument) => self.command(&format!("HELP {}", argument)),
None => self.command("HELP"),
@@ -148,44 +160,55 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
}
/// 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))
}
/// 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))
}
/// Sends a RSET command
pub fn rset(&mut self) -> SmtpResult {
pub fn rset(&mut self) -> EmailResult {
self.command("RSET")
}
/// 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() {
self.command(&format!("AUTH {} {}", mecanism, try!(mecanism.response(username, password, None))))
self.command(&format!("AUTH {} {}",
mecanism,
try!(mecanism.response(username, password, None))))
} else {
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
Some(challenge) => challenge,
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
};
let cram_response = try!(mecanism.response(username, password, Some(&encoded_challenge)));
debug!("CRAM challenge: {}", encoded_challenge);
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
let cram_response = try!(mecanism.response(username,
password,
Some(&encoded_challenge)));
self.command(&format!("{}", cram_response))
}
}
/// Sends a STARTTLS command
pub fn starttls(&mut self) -> EmailResult {
self.command("STARTTLS")
}
/// 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)
}
/// 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() {
return Err(From::from("Connection closed"));
}
@@ -199,13 +222,15 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
}
/// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult {
fn get_reply(&mut self) -> EmailResult {
let mut parser = ResponseParser::new();
let mut line = String::new();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
debug!("Read: {}", escape_crlf(line.as_ref()));
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
line.clear();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
@@ -236,19 +261,15 @@ mod test {
fn test_remove_crlf() {
assert_eq!(remove_crlf("\r\n"), "");
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
assert_eq!(
remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_nameSIZE 42"
);
assert_eq!(remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_nameSIZE 42");
}
#[test]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
assert_eq!(
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
);
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
}
}

View File

@@ -0,0 +1,95 @@
//! A trait to represent a stream
use std::io;
use std::io::{Read, Write, ErrorKind};
use std::net::SocketAddr;
use std::net::TcpStream;
use std::fmt;
use std::fmt::{Debug, Formatter};
use openssl::ssl::{SslContext, SslStream};
/// A trait for the concept of opening a stream
pub trait Connector {
/// Opens a connection to the given IP socket
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<Self>;
/// Upgrades to TLS connection
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()>;
}
impl Connector for NetworkStream {
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<NetworkStream> {
let tcp_stream = try!(TcpStream::connect(addr));
match ssl_context {
Some(context) => match SslStream::new(&context, tcp_stream) {
Ok(stream) => Ok(NetworkStream::Ssl(stream)),
Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
},
None => Ok(NetworkStream::Plain(tcp_stream)),
}
}
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
*self = match self.clone() {
NetworkStream::Plain(stream) => match SslStream::new(ssl_context, stream) {
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
},
NetworkStream::Ssl(stream) => NetworkStream::Ssl(stream),
};
Ok(())
}
}
/// Represents the different types of underlying network streams
pub enum NetworkStream {
/// Plain TCP
Plain(TcpStream),
/// SSL over TCP
Ssl(SslStream<TcpStream>),
}
impl Clone for NetworkStream {
#[inline]
fn clone(&self) -> NetworkStream {
match self {
&NetworkStream::Plain(ref stream) => NetworkStream::Plain(stream.try_clone().unwrap()),
&NetworkStream::Ssl(ref stream) => NetworkStream::Ssl(stream.try_clone().unwrap()),
}
}
}
impl Debug for NetworkStream {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NetworkStream(_)")
}
}
impl Read for NetworkStream {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.read(buf),
NetworkStream::Ssl(ref mut stream) => stream.read(buf),
}
}
}
impl Write for NetworkStream {
#[inline]
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.write(msg),
NetworkStream::Ssl(ref mut stream) => stream.write(msg),
}
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.flush(),
NetworkStream::Ssl(ref mut stream) => stream.flush(),
}
}
}

View File

@@ -0,0 +1,220 @@
//! ESMTP features
use std::result::Result;
use std::fmt::{Display, Formatter};
use std::fmt;
use std::collections::HashSet;
use transport::smtp::response::Response;
use transport::error::Error;
use transport::smtp::authentication::Mecanism;
/// Supported ESMTP keywords
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
pub enum Extension {
/// 8BITMIME keyword
///
/// RFC 6152: https://tools.ietf.org/html/rfc6152
EightBitMime,
/// SMTPUTF8 keyword
///
/// RFC 6531: https://tools.ietf.org/html/rfc6531
SmtpUtfEight,
/// STARTTLS keyword
///
/// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls,
/// AUTH mecanism
Authentication(Mecanism),
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
Extension::StartTls => write!(f, "{}", "STARTTLS"),
Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism),
}
}
}
/// Contains information about an SMTP server
#[derive(Clone,Debug,Eq,PartialEq)]
pub struct ServerInfo {
/// Server name
///
/// The name given in the server banner
pub name: String,
/// ESMTP features supported by the server
///
/// It contains the features supported by the server and known by the `Extension` module.
pub features: HashSet<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f,
"{} with {}",
self.name,
match self.features.is_empty() {
true => "no supported features".to_string(),
false => format!("{:?}", self.features),
})
}
}
impl ServerInfo {
/// Parses a response to create a `ServerInfo`
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(Error::ResponseParsingError("Could not read server name")),
};
let mut features: HashSet<Extension> = HashSet::new();
for line in response.message() {
let splitted: Vec<&str> = line.split_whitespace().collect();
let _ = match splitted[0] {
"8BITMIME" => {
features.insert(Extension::EightBitMime);
}
"SMTPUTF8" => {
features.insert(Extension::SmtpUtfEight);
}
"STARTTLS" => {
features.insert(Extension::StartTls);
}
"AUTH" => {
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mecanism::Plain));
}
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mecanism::CramMd5));
}
_ => (),
}
}
}
_ => (),
};
}
Ok(ServerInfo {
name: name,
features: features,
})
}
/// Checks if the server supports an ESMTP feature
pub fn supports_feature(&self, keyword: &Extension) -> bool {
self.features.contains(keyword)
}
/// Checks if the server supports an ESMTP feature
pub fn supports_auth_mecanism(&self, mecanism: Mecanism) -> bool {
self.features.contains(&Extension::Authentication(mecanism))
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::{ServerInfo, Extension};
use transport::smtp::authentication::Mecanism;
use transport::smtp::response::{Code, Response, Severity, Category};
#[test]
fn test_extension_fmt() {
assert_eq!(format!("{}", Extension::EightBitMime),
"8BITMIME".to_string());
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)),
"AUTH PLAIN".to_string());
}
#[test]
fn test_serverinfo_fmt() {
let mut eightbitmime = HashSet::new();
assert!(eightbitmime.insert(Extension::EightBitMime));
assert_eq!(format!("{}",
ServerInfo {
name: "name".to_string(),
features: eightbitmime.clone(),
}),
"name with {EightBitMime}".to_string());
let empty = HashSet::new();
assert_eq!(format!("{}",
ServerInfo {
name: "name".to_string(),
features: empty,
}),
"name with no supported features".to_string());
let mut plain = HashSet::new();
assert!(plain.insert(Extension::Authentication(Mecanism::Plain)));
assert_eq!(format!("{}",
ServerInfo {
name: "name".to_string(),
features: plain.clone(),
}),
"name with {Authentication(Plain)}".to_string());
}
#[test]
fn test_serverinfo() {
let response = Response::new(Code::new(Severity::PositiveCompletion,
Category::Unspecified4,
1),
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]);
let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo {
name: "me".to_string(),
features: features,
};
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
assert!(server_info.supports_feature(&Extension::EightBitMime));
assert!(!server_info.supports_feature(&Extension::StartTls));
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
let response2 = Response::new(Code::new(Severity::PositiveCompletion,
Category::Unspecified4,
1),
vec!["me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]);
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
let server_info2 = ServerInfo {
name: "me".to_string(),
features: features2,
};
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
assert!(server_info2.supports_feature(&Extension::EightBitMime));
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain));
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5));
assert!(!server_info2.supports_feature(&Extension::StartTls));
}
}

369
src/transport/smtp/mod.rs Normal file
View File

@@ -0,0 +1,369 @@
//! Sends an email using the client
use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use openssl::ssl::{SslMethod, SslContext};
use email::SendableEmail;
use transport::smtp::extension::{Extension, ServerInfo};
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 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
#[derive(Debug)]
pub enum SecurityLevel {
/// Only send an email on encrypted connection
AlwaysEncrypt,
/// Use TLS when available
Opportunistic,
/// Never use TLS
NeverEncrypt,
}
/// Contains client configuration
pub struct SmtpTransportBuilder {
/// Maximum connection reuse
///
/// Zero means no limitation
connection_reuse_count_limit: u16,
/// Enable connection reuse
connection_reuse: bool,
/// Name sent during HELO or EHLO
hello_name: String,
/// Credentials
credentials: Option<(String, String)>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// SSL contexyt to use
ssl_context: SslContext,
/// TLS security level
security_level: SecurityLevel,
/// Enable UTF8 mailboxes in enveloppe or headers
smtp_utf8: bool,
/// List of authentication mecanism, sorted by priority
authentication_mecanisms: Vec<Mecanism>,
}
/// Builder for the SMTP SmtpTransport
impl SmtpTransportBuilder {
/// Creates a new local SMTP client
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() {
Some(addr) => Ok(SmtpTransportBuilder {
server_addr: addr,
ssl_context: SslContext::new(SslMethod::Tlsv1).unwrap(),
security_level: SecurityLevel::Opportunistic,
smtp_utf8: false,
credentials: None,
connection_reuse_count_limit: 100,
connection_reuse: false,
hello_name: "localhost".to_string(),
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
}),
None => Err(From::from("Could nor resolve hostname")),
}
}
/// Creates a new local SMTP client to port 25
pub fn localhost() -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(("localhost", SMTP_PORT))
}
/// Use STARTTLS with a specific context
pub fn ssl_context(mut self, ssl_context: SslContext) -> SmtpTransportBuilder {
self.ssl_context = ssl_context;
self
}
/// Require SSL/TLS using STARTTLS
pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder {
self.security_level = level;
self
}
/// Require SSL/TLS using STARTTLS
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder {
self.smtp_utf8 = enabled;
self
}
/// Set the name used during HELO or EHLO
pub fn hello_name(mut self, name: &str) -> SmtpTransportBuilder {
self.hello_name = name.to_string();
self
}
/// Enable connection reuse
pub fn connection_reuse(mut self, enable: bool) -> SmtpTransportBuilder {
self.connection_reuse = enable;
self
}
/// Set the maximum number of emails sent using one connection
pub fn connection_reuse_count_limit(mut self, limit: u16) -> SmtpTransportBuilder {
self.connection_reuse_count_limit = limit;
self
}
/// Set the client credentials
pub fn credentials(mut self, username: &str, password: &str) -> SmtpTransportBuilder {
self.credentials = Some((username.to_string(), password.to_string()));
self
}
/// Set the authentication mecanisms
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SmtpTransportBuilder {
self.authentication_mecanisms = mecanisms;
self
}
/// Build the SMTP client
///
/// It does not connects to the server, but only creates the `SmtpTransport`
pub fn build(self) -> SmtpTransport {
SmtpTransport::new(self)
}
}
/// Represents the state of a client
#[derive(Debug)]
struct State {
/// Panic state
pub panic: bool,
/// Connection reuse counter
pub connection_reuse_count: u16,
}
/// Structure that implements the high level SMTP client
pub struct SmtpTransport {
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
/// SmtpTransport variable states
state: State,
/// Information about the client
client_info: SmtpTransportBuilder,
/// Low level client
client: Client,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.reset();
}
return Err(From::from(err))
},
}
})
);
impl SmtpTransport {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
let client = Client::new();
SmtpTransport {
client: client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
/// Reset the client state
fn reset(&mut self) {
// Close the SMTP transaction if needed
self.close();
// Reset the client state
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
}
/// Gets the EHLO response and updates server information
pub fn get_ehlo(&mut self) -> EmailResult {
// Extended Hello
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
Ok(ehlo_response)
}
}
impl EmailTransport for SmtpTransport {
/// Sends an email
fn send(&mut self,
to_addresses: Vec<String>,
from_address: String,
message: String,
message_id: String)
-> EmailResult {
// Check if the connection is still available
if self.state.connection_reuse_count > 0 {
if !self.client.is_connected() {
self.reset();
}
}
// If there is a usable connection, test if the server answers and hello has been sent
if self.state.connection_reuse_count == 0 {
try!(self.client.connect(&self.client_info.server_addr));
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
try!(self.get_ehlo());
match (&self.client_info.security_level,
self.server_info.as_ref().unwrap().supports_feature(&Extension::StartTls)) {
(&SecurityLevel::AlwaysEncrypt, false) =>
return Err(From::from("Could not encrypt connection, aborting")),
(&SecurityLevel::Opportunistic, false) => (),
(&SecurityLevel::NeverEncrypt, _) => (),
(_, true) => {
try_smtp!(self.client.starttls(), self);
try_smtp!(self.client.upgrade_tls_stream(&self.client_info.ssl_context),
self);
debug!("connection encrypted");
// Send EHLO again
try!(self.get_ehlo());
}
}
if self.client_info.credentials.is_some() {
let (username, password) = self.client_info.credentials.clone().unwrap();
let mut found = false;
for mecanism in self.client_info.authentication_mecanisms.clone() {
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
found = true;
try_smtp!(self.client.auth(mecanism, &username, &password), self);
break;
}
}
if !found {
info!("No supported authentication mecanisms available");
}
}
}
// Mail
let mail_options = match (self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::EightBitMime),
self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::EightBitMime)) {
(true, true) => Some("BODY=8BITMIME SMTPUTF8"),
(true, false) => Some("BODY=8BITMIME"),
(false, _) => None,
};
try_smtp!(self.client.mail(&from_address, mail_options), self);
// Log the mail command
info!("{}: from=<{}>", message_id, from_address);
// Recipient
for to_address in to_addresses.iter() {
try_smtp!(self.client.rcpt(&to_address), self);
// Log the rcpt command
info!("{}: to=<{}>", message_id, to_address);
}
// Data
try_smtp!(self.client.data(), self);
// Message content
let result = self.client.message(&message);
if result.is_ok() {
// Increment the connection reuse counter
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
// Log the message
info!("{}: conn_use={}, size={}, status=sent ({})",
message_id,
self.state.connection_reuse_count,
message.len(),
result.as_ref()
.ok()
.unwrap()
.message()
.iter()
.next()
.unwrap_or(&"no response".to_string()));
}
// Test if we can reuse the existing connection
if (!self.client_info.connection_reuse) ||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
self.reset();
}
result
}
/// Closes the inner connection
fn close(&mut self) {
self.client.close();
}
}

View File

@@ -0,0 +1,588 @@
//! SMTP response, containing a mandatory return code and an optional text message
use std::str::FromStr;
use std::fmt::{Display, Formatter, Result};
use std::result;
use self::Severity::*;
use self::Category::*;
use transport::error::{EmailResult, Error};
/// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Severity {
/// 2yx
PositiveCompletion,
/// 3yz
PositiveIntermediate,
/// 4yz
TransientNegativeCompletion,
/// 5yz
PermanentNegativeCompletion,
}
impl FromStr for Severity {
type Err = Error;
fn from_str(s: &str) -> result::Result<Severity, Error> {
match s {
"2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
}
}
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f,
"{}",
match *self {
PositiveCompletion => 2,
PositiveIntermediate => 3,
TransientNegativeCompletion => 4,
PermanentNegativeCompletion => 5,
})
}
}
/// Second digit
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Category {
/// x0z
Syntax,
/// x1z
Information,
/// x2z
Connections,
/// x3z
Unspecified3,
/// x4z
Unspecified4,
/// x5z
MailSystem,
}
impl FromStr for Category {
type Err = Error;
fn from_str(s: &str) -> result::Result<Category, Error> {
match s {
"0" => Ok(Syntax),
"1" => Ok(Information),
"2" => Ok(Connections),
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"5" => Ok(MailSystem),
_ => Err(Error::ResponseParsingError("Second digit must be between 0 and 5")),
}
}
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f,
"{}",
match *self {
Syntax => 0,
Information => 1,
Connections => 2,
Unspecified3 => 3,
Unspecified4 => 4,
MailSystem => 5,
})
}
}
/// Represents a 3 digit SMTP response code
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Code {
/// First digit of the response code
severity: Severity,
/// Second digit of the response code
category: Category,
/// Third digit
detail: u8,
}
impl FromStr for Code {
type Err = Error;
#[inline]
fn from_str(s: &str) -> result::Result<Code, Error> {
if s.len() == 3 {
match (s[0..1].parse::<Severity>(),
s[1..2].parse::<Category>(),
s[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {
severity: severity,
category: category,
detail: detail,
}),
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
}
} else {
Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"))
}
}
}
impl Code {
/// Creates a new `Code` structure
pub fn new(severity: Severity, category: Category, detail: u8) -> Code {
Code {
severity: severity,
category: category,
detail: detail,
}
}
/// Returns the reply code
pub fn code(&self) -> String {
format!("{}{}{}", self.severity, self.category, self.detail)
}
}
/// Parses an SMTP response
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct ResponseParser {
/// Response code
code: Option<Code>,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>,
}
impl ResponseParser {
/// Creates a new parser
pub fn new() -> ResponseParser {
ResponseParser {
code: None,
message: vec![],
}
}
/// Parses a line and return a `bool` indicating if there are more lines to come
pub fn read_line(&mut self, line: &str) -> result::Result<bool, Error> {
if line.len() < 3 {
return Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"));
}
match self.code {
Some(ref code) => {
if code.code() != line[0..3] {
return Err(Error::ResponseParsingError("Response code has changed during a \
reponse"));
}
}
None => self.code = Some(try!(line[0..3].parse::<Code>())),
}
if line.len() > 4 {
self.message.push(line[4..].to_string());
if line.as_bytes()[3] == '-' as u8 {
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
}
}
/// Builds a response from a `ResponseParser`
pub fn response(self) -> EmailResult {
match self.code {
Some(code) => Ok(Response::new(code, self.message)),
None => Err(Error::ResponseParsingError("Incomplete response, could not read \
response code")),
}
}
}
/// Contains an SMTP reply, with separed code and message
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Response {
/// Response code
code: Code,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>,
}
impl Response {
/// Creates a new `Response`
pub fn new(code: Code, message: Vec<String>) -> Response {
Response {
code: code,
message: message,
}
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.code.severity {
PositiveCompletion => true,
PositiveIntermediate => true,
_ => false,
}
}
/// Returns the message
pub fn message(&self) -> Vec<String> {
self.message.clone()
}
/// Returns the severity (i.e. 1st digit)
pub fn severity(&self) -> Severity {
self.code.severity
}
/// Returns the category (i.e. 2nd digit)
pub fn category(&self) -> Category {
self.code.category
}
/// Returns the detail (i.e. 3rd digit)
pub fn detail(&self) -> u8 {
self.code.detail
}
/// Returns the reply code
fn code(&self) -> String {
self.code.code()
}
/// Tests code equality
pub fn has_code(&self, code: u16) -> bool {
self.code() == format!("{}", code)
}
/// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<String> {
match self.message.is_empty() {
true => None,
false => match self.message[0].split_whitespace().next() {
Some(word) => Some(word.to_string()),
None => None,
},
}
}
}
#[cfg(test)]
mod test {
use super::{Severity, Category, Response, ResponseParser, Code};
#[test]
fn test_severity_from_str() {
assert_eq!("2".parse::<Severity>().unwrap(),
Severity::PositiveCompletion);
assert_eq!("4".parse::<Severity>().unwrap(),
Severity::TransientNegativeCompletion);
assert!("1".parse::<Severity>().is_err());
}
#[test]
fn test_severity_fmt() {
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
}
#[test]
fn test_category_from_str() {
assert_eq!("2".parse::<Category>().unwrap(), Category::Connections);
assert_eq!("4".parse::<Category>().unwrap(), Category::Unspecified4);
assert!("6".parse::<Category>().is_err());
}
#[test]
fn test_category_fmt() {
assert_eq!(format!("{}", Category::Unspecified4), "4");
}
#[test]
fn test_code_new() {
assert_eq!(Code::new(Severity::TransientNegativeCompletion,
Category::Connections,
0),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 0,
});
}
#[test]
fn test_code_from_str() {
assert_eq!("421".parse::<Code>().unwrap(),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
});
}
#[test]
fn test_code_code() {
let code = Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
};
assert_eq!(code.code(), "421");
}
#[test]
fn test_response_new() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]),
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()],
});
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![]),
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec![],
});
}
#[test]
fn test_response_parser() {
let mut parser = ResponseParser::new();
assert!(parser.read_line("250-me").unwrap());
assert!(parser.read_line("250-8BITMIME").unwrap());
assert!(parser.read_line("250-SIZE 42").unwrap());
assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap());
let response = parser.response().unwrap();
assert_eq!(response,
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: 0,
},
message: vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"AUTH PLAIN CRAM-MD5".to_string()],
});
}
#[test]
fn test_response_is_positive() {
assert!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.is_positive());
assert!(!Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.is_positive());
}
#[test]
fn test_response_message() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.message(),
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
let empty_message: Vec<String> = vec![];
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![])
.message(),
empty_message);
}
#[test]
fn test_response_severity() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.severity(),
Severity::PositiveCompletion);
assert_eq!(Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.severity(),
Severity::PermanentNegativeCompletion);
}
#[test]
fn test_response_category() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.category(),
Category::Unspecified4);
}
#[test]
fn test_response_detail() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.detail(),
1);
}
#[test]
fn test_response_code() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.code(),
"241");
}
#[test]
fn test_response_has_code() {
assert!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(241));
assert!(!Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(251));
}
#[test]
fn test_response_first_word() {
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.first_word(),
Some("me".to_string()));
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.first_word(),
Some("me".to_string()));
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![])
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![" ".to_string()])
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![" ".to_string()])
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["".to_string()])
.first_word(),
None);
}
}

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) {
()
}
}