Compare commits

..

54 Commits

Author SHA1 Message Date
Alexis Mousset
af20cfa8ff Merge pull request #41 from amousset/master
Improve doc
2016-01-16 12:22:40 +01:00
Alexis Mousset
7a9f9111a5 Improve doc 2016-01-16 12:13:06 +01:00
Alexis Mousset
89c0be219d Merge pull request #39 from lorenz/patch-3
Allow multiple To addresses in SimpleSendableMail
2015-12-18 22:57:31 +01:00
Lorenz Brun
6ee7fdb3d1 Allow multiple To addresses in SimpleSendableMail
Because the struct internally stores a `Vec<String>` it would be nice to be able to construct messages with multiple To addresses.
This is in its current form a breaking API change, so feel free to change the way it's implemented.
2015-12-05 00:10:24 +01:00
Alexis Mousset
b7ac3a897f Merge pull request #38 from cloudvlts/master
Update 'openssl' to 0.7
2015-11-30 10:47:23 +01:00
Darius Clark
c436716277 Update 'openssl' to 0.7 2015-11-29 20:51:49 -05:00
Alexis Mousset
eabdb960b0 Merge pull request #37 from amousset/master
Fix doc build
2015-11-07 08:47:13 +01:00
Alexis Mousset
59ba9e84dc Fix doc build 2015-11-07 08:46:51 +01:00
Alexis Mousset
e569c030bc Merge pull request #36 from amousset/master
Fix doc build
2015-11-07 08:40:35 +01:00
Alexis Mousset
72aea756fa Fix doc build 2015-11-07 08:40:02 +01:00
Alexis Mousset
655ae6d2ff Update .travis.yml 2015-11-03 22:20:33 +01:00
Alexis Mousset
150536d242 Update .travis.yml 2015-11-03 22:00:17 +01:00
Alexis Mousset
7d707fab25 Update .travis.yml 2015-11-03 21:49:03 +01:00
Alexis Mousset
4ec34987f8 Merge pull request #35 from amousset/improve-doc
Improve documentation
2015-11-03 21:48:02 +01:00
Alexis Mousset
7f3680f125 Improve documentation 2015-11-03 21:47:06 +01:00
Alexis Mousset
67566c2152 Merge pull request #34 from amousset/improve-doc
Improve documentation
2015-11-03 21:22:26 +01:00
Alexis Mousset
d863a7677e Improve documentation 2015-11-03 21:18:18 +01:00
Alexis Mousset
c1fe40479b Improve documentation 2015-11-03 21:08:56 +01:00
Alexis Mousset
8d03545062 Merge pull request #32 from amousset/improve-doc
Improve documentation
2015-10-31 21:41:47 +01:00
Alexis Mousset
489a6e892e Improve documentation 2015-10-31 21:23:06 +01:00
Alexis Mousset
b3fe1e0f65 Merge pull request #31 from amousset/fix-benchmarks
Fix benchmarks
2015-10-30 19:15:32 +01:00
Alexis Mousset
3612ffca7a Fix becnhmarks 2015-10-30 19:08:11 +01:00
Alexis Mousset
7940ad6c15 Merge pull request #30 from amousset/comply-with-rfc1214
Comply with RFC1214
2015-10-30 17:29:01 +01:00
Alexis Mousset
5ffb169bc9 Comply with RFC1214 2015-10-29 23:26:20 +01:00
Alexis Mousset
ea0bb256cd Merge pull request #29 from amousset/add-smtps-support
feature(smtp): Add SMTPS support
2015-10-27 22:47:16 +01:00
Alexis Mousset
9f177047f8 Add SMTPS support 2015-10-27 22:38:45 +01:00
Alexis Mousset
48eb859804 Merge pull request #26 from norcalli/master
Renames and fixes typo from mecanism to mechanism
2015-10-27 14:34:47 +01:00
Ashkan Kiani
8d9877233d Renames and fixes typo from mecanism to mechanism 2015-10-27 06:20:04 -07:00
Alexis Mousset
09f61a9fc9 Merge pull request #24 from amousset/rustfmt
style(formatting): Run rustfmt
2015-10-26 22:58:09 +01:00
Alexis Mousset
40e749a04a style(formatting): Run rustfmt 2015-10-26 22:51:07 +01:00
Alexis Mousset
4efb560bc8 Merge pull request #23 from lettre/update-readme
docs(readme): Update readme for 0.5.0
2015-10-26 21:14:24 +01:00
Alexis Mousset
500c4fb39d Update readme for 0.5.0 2015-10-26 21:05:18 +01:00
Alexis Mousset
d488910010 Merge pull request #22 from lorenz/patch-2
Release version 0.5.0
2015-10-26 21:01:33 +01:00
Lorenz Brun
4155e44dbd Release new version
I'd like to use the new features you introduced in the last few days, especially `transport.send(mail)`. Also the docs are currently built for the `master` branch which means they currently show features not yet available in the Crate.
2015-10-26 20:30:57 +01:00
Alexis Mousset
401118ee68 Improved test for File transport 2015-10-26 00:17:46 +01:00
Alexis Mousset
e6dd9d5a46 Install complete openssl on windows 2015-10-25 23:40:59 +01:00
Alexis Mousset
c8187c4a7c Install packages for kcov 2015-10-25 17:59:56 +01:00
Alexis Mousset
8f211c88a8 Add OpenSSL path to windows build 2015-10-25 17:50:20 +01:00
Alexis Mousset
62df24c5b1 Add file transport 2015-10-25 17:47:07 +01:00
Alexis Mousset
7ac43b73c3 Remove Mailer struct 2015-10-25 15:29:40 +01:00
Alexis Mousset
3c91c065d6 Split integration tests 2015-10-25 12:52:56 +01:00
Alexis Mousset
9e30e7185e Use travis-cargo for benchmarks 2015-10-22 23:52:30 +02:00
Alexis Mousset
4da9e16bfc Add cache for release (used in benchmarks) 2015-10-22 23:51:01 +02:00
Alexis Mousset
2977eb0509 Travis build without sudo 2015-10-22 23:45:20 +02:00
Alexis Mousset
2884da8f90 Use smtp-sink for benchmarks 2015-10-22 23:37:04 +02:00
Alexis Mousset
31a7504d54 Add becnhmarks 2015-10-22 23:34:04 +02:00
Alexis Mousset
9a93feea96 Fix SMTPUTF8 test 2015-10-22 21:51:29 +02:00
Alexis Mousset
f102f321d3 Tests email with travis 2015-10-22 21:45:19 +02:00
Alexis Mousset
1ba47e473c Tests email with travis 2015-10-22 21:41:09 +02:00
Alexis Mousset
3acf21a316 Tests email with travis 2015-10-22 21:32:48 +02:00
Alexis Mousset
544894def9 Add integration testing on travis 2015-10-22 19:30:36 +02:00
Alexis Mousset
f74fb4f89c Add openssl to AppVeyor env 2015-10-22 18:32:00 +02:00
Alexis Mousset
085998c730 Add mingw to AppVeyor 2015-10-22 18:07:20 +02:00
Alexis Mousset
d3d7c4b44e Formatting with rustfmt 2015-10-22 16:53:13 +02:00
24 changed files with 495 additions and 322 deletions

View File

@@ -1,10 +1,10 @@
environment: environment:
matrix: matrix:
- TARGET: x86_64-pc-windows-msvc - TARGET: x86_64-pc-windows-msvc
- TARGET: i686-pc-windows-msvc
- TARGET: x86_64-pc-windows-gnu - TARGET: x86_64-pc-windows-gnu
- TARGET: i686-pc-windows-gnu
install: install:
- ps: Start-FileDownload 'http://slproweb.com/download/Win64OpenSSL-1_0_2d.exe'
- ps: Start-Process "Win64OpenSSL-1_0_2d.exe" -ArgumentList "/silent /verysilent /sp- /suppressmsgboxes" -Wait
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-nightly-${env:TARGET}.exe" -FileName "rust-nightly.exe" - 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: .\rust-nightly.exe /VERYSILENT /NORESTART /DIR="C:\rust" | Out-Null
- ps: $env:PATH="$env:PATH;C:\rust\bin" - ps: $env:PATH="$env:PATH;C:\rust\bin"
@@ -12,4 +12,4 @@ install:
- cargo -vV - cargo -vV
build: false build: false
test_script: test_script:
- cargo test --verbose --no-default-features - env OPENSSL_LIB_DIR=C:/OpenSSL-Win64 OPENSSL_INCLUDE_DIR=C:/OpenSSL-Win64/include cargo build --verbose

View File

@@ -1,21 +1,37 @@
language: rust language: rust
sudo: required
rust: rust:
- stable - stable
- beta - beta
- nightly - nightly
sudo: false
cache:
apt: true
pip: true
directories:
- target/debug/deps
- target/debug/build
- target/release/deps
- target/release/build
install:
- pip install 'travis-cargo<0.2' --user
- export PATH=$HOME/.local/bin:$PATH
addons:
apt:
packages:
- postfix
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev
before_script: before_script:
- pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH - smtp-sink 2525 1000&
script: script:
- | - travis-cargo build
travis-cargo build && - travis-cargo test
travis-cargo test && - travis-cargo doc
travis-cargo doc
after_success: after_success:
- travis-cargo --only stable doc-upload - travis-cargo --only nightly bench
- travis-cargo --only stable coveralls - travis-cargo --only stable coveralls --no-sudo
- travis-cargo --only stable doc-upload
env: env:
global: global:
secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc=" secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc="

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
version = "0.4.0" version = "0.5.1"
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
documentation = "http://lettre.github.io/lettre/" documentation = "http://lettre.github.io/lettre/"
@@ -18,7 +18,7 @@ rustc-serialize = "0.3"
rust-crypto = "0.2" rust-crypto = "0.2"
bufstream = "0.1" bufstream = "0.1"
email = "0.0" email = "0.0"
openssl = "0.6" openssl = "0.7"
[dev-dependencies] [dev-dependencies]
env_logger = "0.3" env_logger = "0.3"

View File

@@ -11,9 +11,14 @@ To use this library, add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
lettre = "0.4" lettre = "0.5"
``` ```
Testing
-------
The tests require a mail server listening locally on port 25.
License License
------- -------

44
benches/transport_smtp.rs Normal file
View File

@@ -0,0 +1,44 @@
#![feature(test)]
extern crate lettre;
extern crate test;
use lettre::transport::smtp::SmtpTransportBuilder;
use lettre::transport::EmailTransport;
use lettre::email::EmailBuilder;
#[bench]
fn bench_simple_send(b: &mut test::Bencher) {
let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525").unwrap().build();
b.iter(|| {
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
.unwrap();
let result = sender.send(email);
assert!(result.is_ok());
});
}
#[bench]
fn bench_reuse_send(b: &mut test::Bencher) {
let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525")
.unwrap()
.connection_reuse(true)
.build();
b.iter(|| {
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
.unwrap();
let result = sender.send(email);
assert!(result.is_ok());
});
sender.close()
}

View File

@@ -1,57 +0,0 @@
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate lettre;
use std::sync::{Arc, Mutex};
use std::thread;
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 = 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_mailer = mailer.clone();
threads.push(thread::spawn(move || {
let email = EmailBuilder::new()
.to("user@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.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().unwrap();
let mut mailer = mailer.lock().unwrap();
let result = mailer.send(email);
mailer.close();
match result {
Ok(..) => info!("Email sent successfully"),
Err(error) => error!("{:?}", error),
}
}

1
rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
reorder_imports = true

View File

@@ -1,10 +1,10 @@
//! Simple email (very incomplete) //! Simple email (very incomplete)
use std::fmt::{Display, Formatter};
use std::fmt; use std::fmt;
use std::fmt::{Display, Formatter};
use email_format::{MimeMessage, Header, Mailbox}; use email_format::{Header, Mailbox, MimeMessage};
use time::{now, Tm}; use time::{Tm, now};
use uuid::Uuid; use uuid::Uuid;
/// Converts an adress or an address with an alias to a `Address` /// Converts an adress or an address with an alias to a `Address`
@@ -165,10 +165,10 @@ impl EmailBuilder {
/// Build the Email /// Build the Email
pub fn build(mut self) -> Result<Email, &'static str> { pub fn build(mut self) -> Result<Email, &'static str> {
if self.from.is_none() { if self.from.is_none() {
return Err("No from address") return Err("No from address");
} }
if self.to.is_empty() { if self.to.is_empty() {
return Err("No to address") return Err("No to address");
} }
if !self.date_issued { if !self.date_issued {
@@ -219,10 +219,10 @@ pub struct SimpleSendableEmail {
impl SimpleSendableEmail { impl SimpleSendableEmail {
/// Returns a new email /// Returns a new email
pub fn new(from_address: &str, to_address: &str, message: &str) -> SimpleSendableEmail { pub fn new(from_address: &str, to_address: Vec<String>, message: &str) -> SimpleSendableEmail {
SimpleSendableEmail { SimpleSendableEmail {
from: from_address.to_string(), from: from_address.to_string(),
to: vec![to_address.to_string()], to: to_address,
message: message.to_string(), message: message.to_string(),
} }
} }
@@ -269,9 +269,9 @@ mod test {
use time::now; use time::now;
use uuid::Uuid; use uuid::Uuid;
use email_format::{MimeMessage, Header}; use email_format::{Header, MimeMessage};
use super::{SendableEmail, EmailBuilder, Email}; use super::{Email, EmailBuilder, SendableEmail};
#[test] #[test]
fn test_email_display() { fn test_email_display() {

View File

@@ -1,36 +1,39 @@
//! # Rust email client //! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
//! //!
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still //! ## Architecture
//! 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 //! This mailer is divided into:
//!
//! * An `email` part: builds the email message
//! * A `transport` part: contains the available transports for your emails. To be sendable, the
//! emails have to implement `SendableEmail`.
//!
//! ## SMTP transport
//!
//! This SMTP follows [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. //! compliance checks.
//! //!
//! It implements the following extensions: //! It implements the following extensions:
//! //!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152)) //! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms //! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and
//! CRAM-MD5 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487)) //! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531)) //! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//! //!
//! ## Architecture
//!
//! This client is divided into three main parts:
//!
//! * 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
//!
//! ### Simple example //! ### Simple example
//! //!
//! This is the most basic example of usage: //! This is the most basic example of usage:
//! //!
//! ```rust,no_run //! ```rust
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder}; //! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
//! use lettre::email::EmailBuilder; //! use lettre::email::EmailBuilder;
//! use lettre::transport::EmailTransport; //! use lettre::transport::EmailTransport;
//! use lettre::mailer::Mailer;
//! //!
//! // Create an email //! // Create an email
//! let email = EmailBuilder::new() //! let email = EmailBuilder::new()
@@ -43,7 +46,8 @@
//! .build().unwrap(); //! .build().unwrap();
//! //!
//! // Open a local connection on port 25 //! // Open a local connection on port 25
//! let mut mailer = Mailer::new(SmtpTransportBuilder::localhost().unwrap().build()); //! let mut mailer =
//! SmtpTransportBuilder::localhost().unwrap().build();
//! // Send the email //! // Send the email
//! let result = mailer.send(email); //! let result = mailer.send(email);
//! //!
@@ -54,11 +58,11 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use lettre::email::EmailBuilder; //! use lettre::email::EmailBuilder;
//! use lettre::transport::smtp::{SecurityLevel, SmtpTransport, SmtpTransportBuilder}; //! use lettre::transport::smtp::{SecurityLevel, SmtpTransport,
//! use lettre::transport::smtp::authentication::Mecanism; //! SmtpTransportBuilder};
//! use lettre::transport::smtp::authentication::Mechanism;
//! use lettre::transport::smtp::SUBMISSION_PORT; //! use lettre::transport::smtp::SUBMISSION_PORT;
//! use lettre::transport::EmailTransport; //! 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"));
@@ -74,7 +78,8 @@
//! let email = builder.build().unwrap(); //! 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 mailer = Mailer::new(SmtpTransportBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap() //! let mut mailer = 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,10 +89,10 @@
//! .security_level(SecurityLevel::AlwaysEncrypt) //! .security_level(SecurityLevel::AlwaysEncrypt)
//! // Enable SMTPUTF8 is the server supports it //! // Enable SMTPUTF8 is the server supports it
//! .smtp_utf8(true) //! .smtp_utf8(true)
//! // Configure accepted authetication mecanisms //! // Configure accepted authetication mechanisms
//! .authentication_mecanisms(vec![Mecanism::CramMd5]) //! .authentication_mechanisms(vec![Mechanism::CramMd5])
//! // Enable connection reuse //! // Enable connection reuse
//! .connection_reuse(true).build()); //! .connection_reuse(true).build();
//! //!
//! let result_1 = mailer.send(email.clone()); //! let result_1 = mailer.send(email.clone());
//! assert!(result_1.is_ok()); //! assert!(result_1.is_ok());
@@ -100,39 +105,18 @@
//! mailer.close(); //! mailer.close();
//! ``` //! ```
//! //!
//! ### Using the client directly
//!
//! If you just want to send an email without using `Email` to provide headers:
//!
//! ```rust,no_run
//! 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(
//! "test@example.com",
//! "test@example.org",
//! "Hello world !"
//! );
//!
//! let mut mailer = Mailer::new(SmtpTransportBuilder::localhost().unwrap().build());
//! let result = mailer.send(email);
//! assert!(result.is_ok());
//! ```
//!
//! ### Lower level //! ### Lower level
//! //!
//! 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
//! use lettre::transport::smtp::SMTP_PORT; //! use lettre::transport::smtp::SMTP_PORT;
//! use lettre::transport::smtp::client::Client; //! use lettre::transport::smtp::client::Client;
//! use lettre::transport::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), None);
//! let _ = email_client.ehlo("my_hostname"); //! let _ = email_client.ehlo("my_hostname");
//! let _ = email_client.mail("user@example.com", None); //! let _ = email_client.mail("user@example.com", None);
//! let _ = email_client.rcpt("user@example.org"); //! let _ = email_client.rcpt("user@example.org");
@@ -140,12 +124,80 @@
//! let _ = email_client.message("Test email"); //! let _ = email_client.message("Test email");
//! let _ = email_client.quit(); //! let _ = email_client.quit();
//! ``` //! ```
//!
//! ## Stub transport
//!
//! The stub transport only logs message envelope and drops the content. It can be useful for
//! testing purposes.
//!
//! ```rust
//! use lettre::transport::stub::StubEmailTransport;
//! use lettre::transport::EmailTransport;
//! use lettre::email::EmailBuilder;
//!
//! let mut sender = StubEmailTransport;
//! let email = EmailBuilder::new()
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .unwrap();
//! let result = sender.send(email);
//! assert!(result.is_ok());
//! ```
//!
//! Will log the line:
//!
//! ```text
//! b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
//! ```
//!
//! ## File transport
//!
//! The file transport writes the emails to the given directory. The name of the file will be
//! `message_id.txt`.
//! It can be useful for testing purposes.
//!
//! ```rust
//! use std::env::temp_dir;
//!
//! use lettre::transport::file::FileEmailTransport;
//! use lettre::transport::EmailTransport;
//! use lettre::email::{EmailBuilder, SendableEmail};
//!
//! // Write to the local temp directory
//! let mut sender = FileEmailTransport::new(temp_dir());
//! let email = EmailBuilder::new()
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .unwrap();
//!
//! let result = sender.send(email);
//! assert!(result.is_ok());
//! ```
//! Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.txt`:
//!
//! ```text
//! b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
//! To: <root@localhost>
//! From: <user@localhost>
//! Subject: Hello
//! Date: Sat, 31 Oct 2015 13:42:19 +0100
//! Message-ID: <b7c211bc-9811-45ce-8cd9-68eab575d695.lettre@localhost>
//!
//! Hello World!
//! ```
#![deny(missing_docs)]
#![deny(missing_docs, unsafe_code, unstable_features)]
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate rustc_serialize as serialize; extern crate rustc_serialize;
extern crate crypto; extern crate crypto;
extern crate time; extern crate time;
extern crate uuid; extern crate uuid;
@@ -155,4 +207,3 @@ extern crate openssl;
pub mod transport; pub mod transport;
pub mod email; pub mod email;
pub mod mailer;

View File

@@ -1,30 +0,0 @@
//! 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

@@ -2,11 +2,11 @@
use std::error::Error as StdError; use std::error::Error as StdError;
use std::io; use std::io;
use std::fmt::{Display, Formatter};
use std::fmt; use std::fmt;
use std::fmt::{Display, Formatter};
use transport::smtp::response::{Severity, Response}; use transport::smtp::response::{Response, Severity};
use serialize::base64::FromBase64Error; use rustc_serialize::base64::FromBase64Error;
use self::Error::*; use self::Error::*;
/// An enum of all error kinds. /// An enum of all error kinds.
@@ -83,8 +83,3 @@ impl From<&'static str> for Error {
/// SMTP result type /// SMTP result type
pub type EmailResult = Result<Response, Error>; pub type EmailResult = Result<Response, Error>;
#[cfg(test)]
mod test {
// TODO
}

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

@@ -0,0 +1,53 @@
//! This transport creates a file for each email, containing the enveloppe information and the email
//! itself.
use std::path::{Path, PathBuf};
use std::io::prelude::*;
use std::fs::File;
use transport::EmailTransport;
use transport::error::EmailResult;
use transport::smtp::response::Response;
use transport::smtp::response::{Category, Code, Severity};
use email::SendableEmail;
/// Writes the content and the enveloppe information to a file
pub struct FileEmailTransport {
path: PathBuf,
}
impl FileEmailTransport {
/// Creates a new transport to the given directory
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport {
let mut path_buf = PathBuf::new();
path_buf.push(path);
FileEmailTransport { path: path_buf }
}
}
impl EmailTransport for FileEmailTransport {
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
let mut file = self.path.clone();
file.push(format!("{}.txt", email.message_id()));
let mut f = try!(File::create(file.as_path()));
let log_line = format!("{}: from=<{}> to=<{}>\n",
email.message_id(),
email.from_address(),
email.to_addresses().join("> to=<"));
try!(f.write_all(log_line.as_bytes()));
try!(f.write_all(format!("{}", email.message()).as_bytes()));
info!("{} status=<written>", log_line);
Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
vec![format!("Ok: email written to {}",
file.to_str().unwrap_or("non-UTF-8 path"))]))
}
fn close(&mut self) {
()
}
}

View File

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

View File

@@ -1,10 +1,10 @@
//! Provides authentication mecanisms //! Provides authentication mechanisms
use std::fmt::{Display, Formatter};
use std::fmt; use std::fmt;
use std::fmt::{Display, Formatter};
use serialize::base64::{self, ToBase64, FromBase64}; use rustc_serialize::base64::{self, FromBase64, ToBase64};
use serialize::hex::ToHex; use rustc_serialize::hex::ToHex;
use crypto::hmac::Hmac; use crypto::hmac::Hmac;
use crypto::md5::Md5; use crypto::md5::Md5;
use crypto::mac::Mac; use crypto::mac::Mac;
@@ -12,34 +12,34 @@ use crypto::mac::Mac;
use transport::smtp::NUL; use transport::smtp::NUL;
use transport::error::Error; use transport::error::Error;
/// Represents authentication mecanisms /// Represents authentication mechanisms
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)] #[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
pub enum Mecanism { pub enum Mechanism {
/// PLAIN authentication mecanism /// PLAIN authentication mechanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616 /// RFC 4616: https://tools.ietf.org/html/rfc4616
Plain, Plain,
/// CRAM-MD5 authentication mecanism /// CRAM-MD5 authentication mechanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195 /// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5, CramMd5,
} }
impl Display for Mecanism { impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, write!(f,
"{}", "{}",
match *self { match *self {
Mecanism::Plain => "PLAIN", Mechanism::Plain => "PLAIN",
Mecanism::CramMd5 => "CRAM-MD5", Mechanism::CramMd5 => "CRAM-MD5",
}) })
} }
} }
impl Mecanism { impl Mechanism {
/// Does the mecanism supports initial response /// Does the mechanism supports initial response
pub fn supports_initial_response(&self) -> bool { pub fn supports_initial_response(&self) -> bool {
match *self { match *self {
Mecanism::Plain => true, Mechanism::Plain => true,
Mecanism::CramMd5 => false, Mechanism::CramMd5 => false,
} }
} }
@@ -51,18 +51,20 @@ impl Mecanism {
challenge: Option<&str>) challenge: Option<&str>)
-> Result<String, Error> { -> Result<String, Error> {
match *self { match *self {
Mecanism::Plain => { Mechanism::Plain => {
match challenge { match challenge {
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")), Some(_) =>
Err(Error::ClientError("This mechanism does not expect a challenge")),
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password) None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
.as_bytes() .as_bytes()
.to_base64(base64::STANDARD)), .to_base64(base64::STANDARD)),
} }
} }
Mecanism::CramMd5 => { Mechanism::CramMd5 => {
let encoded_challenge = match challenge { let encoded_challenge = match challenge {
Some(challenge) => challenge, Some(challenge) => challenge,
None => return Err(Error::ClientError("This mecanism does expect a challenge")), None =>
return Err(Error::ClientError("This mechanism does expect a challenge")),
}; };
let decoded_challenge = match encoded_challenge.from_base64() { let decoded_challenge = match encoded_challenge.from_base64() {
@@ -83,27 +85,28 @@ impl Mecanism {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::Mecanism; use super::Mechanism;
#[test] #[test]
fn test_plain() { fn test_plain() {
let mecanism = Mecanism::Plain; let mechanism = Mechanism::Plain;
assert_eq!(mecanism.response("username", "password", None).unwrap(), assert_eq!(mechanism.response("username", "password", None).unwrap(),
"AHVzZXJuYW1lAHBhc3N3b3Jk"); "AHVzZXJuYW1lAHBhc3N3b3Jk");
assert!(mecanism.response("username", "password", Some("test")).is_err()); assert!(mechanism.response("username", "password", Some("test")).is_err());
} }
#[test] #[test]
fn test_cram_md5() { fn test_cram_md5() {
let mecanism = Mecanism::CramMd5; let mechanism = Mechanism::CramMd5;
assert_eq!(mecanism.response("alice", assert_eq!(mechanism.response("alice",
"wonderland", "wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")) Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=\
.unwrap(), ="))
.unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA="); "YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
assert!(mecanism.response("alice", "wonderland", Some("tést")).is_err()); assert!(mechanism.response("alice", "wonderland", Some("tést")).is_err());
assert!(mecanism.response("alice", "wonderland", None).is_err()); assert!(mechanism.response("alice", "wonderland", None).is_err());
} }
} }

View File

@@ -2,16 +2,16 @@
use std::string::String; use std::string::String;
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
use std::io::{BufRead, Read, Write};
use std::io; use std::io;
use std::io::{BufRead, Read, Write};
use std::fmt::Debug; use std::fmt::Debug;
use bufstream::BufStream; use bufstream::BufStream;
use openssl::ssl::SslContext; use openssl::ssl::SslContext;
use transport::error::{EmailResult, Error};
use transport::smtp::response::ResponseParser; use transport::smtp::response::ResponseParser;
use transport::smtp::authentication::Mecanism; use transport::smtp::authentication::Mechanism;
use transport::error::{Error, EmailResult};
use transport::smtp::client::net::{Connector, NetworkStream}; use transport::smtp::client::net::{Connector, NetworkStream};
use transport::smtp::{CRLF, MESSAGE_ENDING}; use transport::smtp::{CRLF, MESSAGE_ENDING};
@@ -27,8 +27,8 @@ fn escape_dot(string: &str) -> String {
} else { } else {
string.to_string() string.to_string()
} }
.replace("\r.", "\r..") .replace("\r.", "\r..")
.replace("\n.", "\n..") .replace("\n.", "\n..")
} }
/// Returns the string replacing all the CRLF with "\<CRLF\>" /// Returns the string replacing all the CRLF with "\<CRLF\>"
@@ -80,7 +80,6 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
/// Upgrades the underlying connection to SSL/TLS /// Upgrades the underlying connection to SSL/TLS
pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> { pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> {
//let current_stream = self.stream.clone();
if self.stream.is_some() { if self.stream.is_some() {
self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context) self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context)
} else { } else {
@@ -89,7 +88,10 @@ 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) -> EmailResult { pub fn connect<A: ToSocketAddrs>(&mut self,
addr: &A,
ssl_context: Option<&SslContext>)
-> 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);
@@ -103,7 +105,7 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
}; };
// Try to connect // Try to connect
self.set_stream(try!(Connector::connect(&server_addr, None))); self.set_stream(try!(Connector::connect(&server_addr, ssl_context)));
self.get_reply() self.get_reply()
} }
@@ -174,13 +176,13 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
self.command("RSET") self.command("RSET")
} }
/// Sends an AUTH command with the given mecanism /// Sends an AUTH command with the given mechanism
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> EmailResult { pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> EmailResult {
if mecanism.supports_initial_response() { if mechanism.supports_initial_response() {
self.command(&format!("AUTH {} {}", self.command(&format!("AUTH {} {}",
mecanism, mechanism,
try!(mecanism.response(username, password, None)))) try!(mechanism.response(username, password, None))))
} else { } else {
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() { let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
Some(challenge) => challenge, Some(challenge) => challenge,
@@ -189,9 +191,9 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
debug!("CRAM challenge: {}", encoded_challenge); debug!("CRAM challenge: {}", encoded_challenge);
let cram_response = try!(mecanism.response(username, let cram_response = try!(mechanism.response(username,
password, password,
Some(&encoded_challenge))); Some(&encoded_challenge)));
self.command(&format!("{}", cram_response)) self.command(&format!("{}", cram_response))
} }
@@ -247,7 +249,7 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{escape_dot, remove_crlf, escape_crlf}; use super::{escape_crlf, escape_dot, remove_crlf};
#[test] #[test]
fn test_escape_dot() { fn test_escape_dot() {

View File

@@ -1,16 +1,15 @@
//! A trait to represent a stream //! A trait to represent a stream
use std::io; use std::io;
use std::io::{Read, Write, ErrorKind}; use std::io::{ErrorKind, Read, Write};
use std::net::SocketAddr; use std::net::{SocketAddr, 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
pub trait Connector { pub trait Connector: Sized {
/// Opens a connection to the given IP socket /// Opens a connection to the given IP socket
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<Self>; fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<Self>;
/// Upgrades to TLS connection /// Upgrades to TLS connection
@@ -22,7 +21,7 @@ impl Connector for NetworkStream {
let tcp_stream = try!(TcpStream::connect(addr)); let tcp_stream = try!(TcpStream::connect(addr));
match ssl_context { match ssl_context {
Some(context) => match SslStream::new(&context, tcp_stream) { Some(context) => match SslStream::connect_generic(context, tcp_stream) {
Ok(stream) => Ok(NetworkStream::Ssl(stream)), Ok(stream) => Ok(NetworkStream::Ssl(stream)),
Err(err) => Err(io::Error::new(ErrorKind::Other, err)), Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
}, },
@@ -32,7 +31,7 @@ impl Connector for NetworkStream {
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> { fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
*self = match self.clone() { *self = match self.clone() {
NetworkStream::Plain(stream) => match SslStream::new(ssl_context, stream) { NetworkStream::Plain(stream) => match SslStream::connect_generic(ssl_context, stream) {
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream), Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)), Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
}, },

View File

@@ -1,13 +1,13 @@
//! ESMTP features //! ESMTP features
use std::result::Result; use std::result::Result;
use std::fmt::{Display, Formatter};
use std::fmt; use std::fmt;
use std::fmt::{Display, Formatter};
use std::collections::HashSet; use std::collections::HashSet;
use transport::smtp::response::Response;
use transport::error::Error; use transport::error::Error;
use transport::smtp::authentication::Mecanism; use transport::smtp::response::Response;
use transport::smtp::authentication::Mechanism;
/// Supported ESMTP keywords /// Supported ESMTP keywords
#[derive(PartialEq,Eq,Hash,Clone,Debug)] #[derive(PartialEq,Eq,Hash,Clone,Debug)]
@@ -24,8 +24,8 @@ pub enum Extension {
/// ///
/// RFC 2487: https://tools.ietf.org/html/rfc2487 /// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls, StartTls,
/// AUTH mecanism /// AUTH mechanism
Authentication(Mecanism), Authentication(Mechanism),
} }
impl Display for Extension { impl Display for Extension {
@@ -34,7 +34,7 @@ impl Display for Extension {
Extension::EightBitMime => write!(f, "{}", "8BITMIME"), Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"), Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
Extension::StartTls => write!(f, "{}", "STARTTLS"), Extension::StartTls => write!(f, "{}", "STARTTLS"),
Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism), Extension::Authentication(ref mechanism) => write!(f, "{} {}", "AUTH", mechanism),
} }
} }
} }
@@ -88,13 +88,13 @@ impl ServerInfo {
features.insert(Extension::StartTls); features.insert(Extension::StartTls);
} }
"AUTH" => { "AUTH" => {
for &mecanism in &splitted[1..] { for &mechanism in &splitted[1..] {
match mecanism { match mechanism {
"PLAIN" => { "PLAIN" => {
features.insert(Extension::Authentication(Mecanism::Plain)); features.insert(Extension::Authentication(Mechanism::Plain));
} }
"CRAM-MD5" => { "CRAM-MD5" => {
features.insert(Extension::Authentication(Mecanism::CramMd5)); features.insert(Extension::Authentication(Mechanism::CramMd5));
} }
_ => (), _ => (),
} }
@@ -116,8 +116,8 @@ impl ServerInfo {
} }
/// Checks if the server supports an ESMTP feature /// Checks if the server supports an ESMTP feature
pub fn supports_auth_mecanism(&self, mecanism: Mecanism) -> bool { pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
self.features.contains(&Extension::Authentication(mecanism)) self.features.contains(&Extension::Authentication(mechanism))
} }
} }
@@ -125,15 +125,15 @@ impl ServerInfo {
mod test { mod test {
use std::collections::HashSet; use std::collections::HashSet;
use super::{ServerInfo, Extension}; use super::{Extension, ServerInfo};
use transport::smtp::authentication::Mecanism; use transport::smtp::authentication::Mechanism;
use transport::smtp::response::{Code, Response, Severity, Category}; use transport::smtp::response::{Category, Code, Response, Severity};
#[test] #[test]
fn test_extension_fmt() { fn test_extension_fmt() {
assert_eq!(format!("{}", Extension::EightBitMime), assert_eq!(format!("{}", Extension::EightBitMime),
"8BITMIME".to_string()); "8BITMIME".to_string());
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)), assert_eq!(format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string()); "AUTH PLAIN".to_string());
} }
@@ -159,7 +159,7 @@ mod test {
"name with no supported features".to_string()); "name with no supported features".to_string());
let mut plain = HashSet::new(); let mut plain = HashSet::new();
assert!(plain.insert(Extension::Authentication(Mecanism::Plain))); assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
assert_eq!(format!("{}", assert_eq!(format!("{}",
ServerInfo { ServerInfo {
@@ -190,7 +190,7 @@ mod test {
assert!(server_info.supports_feature(&Extension::EightBitMime)); assert!(server_info.supports_feature(&Extension::EightBitMime));
assert!(!server_info.supports_feature(&Extension::StartTls)); assert!(!server_info.supports_feature(&Extension::StartTls));
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5)); assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
let response2 = Response::new(Code::new(Severity::PositiveCompletion, let response2 = Response::new(Code::new(Severity::PositiveCompletion,
Category::Unspecified4, Category::Unspecified4,
@@ -202,8 +202,8 @@ mod test {
let mut features2 = HashSet::new(); let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime)); assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mecanism::Plain))); assert!(features2.insert(Extension::Authentication(Mechanism::Plain)));
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5))); assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5)));
let server_info2 = ServerInfo { let server_info2 = ServerInfo {
name: "me".to_string(), name: "me".to_string(),
@@ -213,8 +213,8 @@ mod test {
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2); assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
assert!(server_info2.supports_feature(&Extension::EightBitMime)); assert!(server_info2.supports_feature(&Extension::EightBitMime));
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain)); assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5)); assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
assert!(!server_info2.supports_feature(&Extension::StartTls)); assert!(!server_info2.supports_feature(&Extension::StartTls));
} }
} }

View File

@@ -3,14 +3,14 @@
use std::string::String; use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs}; use std::net::{SocketAddr, ToSocketAddrs};
use openssl::ssl::{SslMethod, SslContext}; use openssl::ssl::{SslContext, SslMethod};
use email::SendableEmail;
use transport::smtp::extension::{Extension, ServerInfo};
use transport::error::{EmailResult, Error}; use transport::error::{EmailResult, Error};
use transport::smtp::extension::{Extension, ServerInfo};
use transport::smtp::client::Client; use transport::smtp::client::Client;
use transport::smtp::authentication::Mecanism; use transport::smtp::authentication::Mechanism;
use transport::EmailTransport; use transport::EmailTransport;
use email::SendableEmail;
pub mod extension; pub mod extension;
pub mod authentication; pub mod authentication;
@@ -18,7 +18,8 @@ pub mod response;
pub mod client; pub mod client;
// Registrated port numbers: // Registrated port numbers:
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml // https://www.iana.
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port /// Default smtp port
pub static SMTP_PORT: u16 = 25; pub static SMTP_PORT: u16 = 25;
@@ -46,9 +47,17 @@ pub static NUL: &'static str = "\0";
/// TLS security level /// TLS security level
#[derive(Debug)] #[derive(Debug)]
pub enum SecurityLevel { pub enum SecurityLevel {
/// Only send an email on encrypted connection /// Use a TLS wrapped connection
///
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
EncryptedWrapper,
/// Only send an email on encrypted connection (with STARTTLS)
///
/// Recommended mode, prevents MITM when used with verified certificates.
AlwaysEncrypt, AlwaysEncrypt,
/// Use TLS when available /// Use TLS when available (with STARTTLS)
///
/// Default mode.
Opportunistic, Opportunistic,
/// Never use TLS /// Never use TLS
NeverEncrypt, NeverEncrypt,
@@ -74,8 +83,8 @@ pub struct SmtpTransportBuilder {
security_level: SecurityLevel, security_level: SecurityLevel,
/// Enable UTF8 mailboxes in enveloppe or headers /// Enable UTF8 mailboxes in enveloppe or headers
smtp_utf8: bool, smtp_utf8: bool,
/// List of authentication mecanism, sorted by priority /// List of authentication mechanism, sorted by priority
authentication_mecanisms: Vec<Mecanism>, authentication_mechanisms: Vec<Mechanism>,
} }
/// Builder for the SMTP SmtpTransport /// Builder for the SMTP SmtpTransport
@@ -94,7 +103,7 @@ impl SmtpTransportBuilder {
connection_reuse_count_limit: 100, connection_reuse_count_limit: 100,
connection_reuse: false, connection_reuse: false,
hello_name: "localhost".to_string(), hello_name: "localhost".to_string(),
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain], authentication_mechanisms: vec![Mechanism::CramMd5, Mechanism::Plain],
}), }),
None => Err(From::from("Could nor resolve hostname")), None => Err(From::from("Could nor resolve hostname")),
} }
@@ -111,13 +120,29 @@ impl SmtpTransportBuilder {
self self
} }
/// Require SSL/TLS using STARTTLS /// Set the security level for SSL/TLS
pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder { pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder {
self.security_level = level; self.security_level = level;
self self
} }
/// Require SSL/TLS using STARTTLS /// Require SSL/TLS using STARTTLS
///
/// Incompatible with `ssl_wrapper()``
pub fn encrypt(mut self) -> SmtpTransportBuilder {
self.security_level = SecurityLevel::AlwaysEncrypt;
self
}
/// Require SSL/TLS using STARTTLS
///
/// Incompatible with `encrypt()`
pub fn ssl_wrapper(mut self) -> SmtpTransportBuilder {
self.security_level = SecurityLevel::EncryptedWrapper;
self
}
/// Enable SMTPUTF8 if the server supports it
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder { pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder {
self.smtp_utf8 = enabled; self.smtp_utf8 = enabled;
self self
@@ -147,9 +172,9 @@ impl SmtpTransportBuilder {
self self
} }
/// Set the authentication mecanisms /// Set the authentication mechanisms
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SmtpTransportBuilder { pub fn authentication_mechanisms(mut self, mechanisms: Vec<Mechanism>) -> SmtpTransportBuilder {
self.authentication_mecanisms = mecanisms; self.authentication_mechanisms = mechanisms;
self self
} }
@@ -244,12 +269,14 @@ impl SmtpTransport {
impl EmailTransport for SmtpTransport { impl EmailTransport for SmtpTransport {
/// Sends an email /// Sends an email
fn send(&mut self, fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
to_addresses: Vec<String>,
from_address: String, // Extract email information
message: String, let message_id = email.message_id();
message_id: String) let from_address = email.from_address();
-> EmailResult { let to_addresses = email.to_addresses();
let message = email.message();
// 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() {
@@ -257,9 +284,13 @@ impl EmailTransport for SmtpTransport {
} }
} }
// If there is a usable connection, test if the server answers and hello has been sent
if self.state.connection_reuse_count == 0 { if self.state.connection_reuse_count == 0 {
try!(self.client.connect(&self.client_info.server_addr)); try!(self.client.connect(&self.client_info.server_addr,
match &self.client_info.security_level {
&SecurityLevel::EncryptedWrapper =>
Some(&self.client_info.ssl_context),
_ => None,
}));
// Log the connection // Log the connection
info!("connection established to {}", self.client_info.server_addr); info!("connection established to {}", self.client_info.server_addr);
@@ -272,6 +303,7 @@ impl EmailTransport for SmtpTransport {
return Err(From::from("Could not encrypt connection, aborting")), return Err(From::from("Could not encrypt connection, aborting")),
(&SecurityLevel::Opportunistic, false) => (), (&SecurityLevel::Opportunistic, false) => (),
(&SecurityLevel::NeverEncrypt, _) => (), (&SecurityLevel::NeverEncrypt, _) => (),
(&SecurityLevel::EncryptedWrapper, _) => (),
(_, true) => { (_, true) => {
try_smtp!(self.client.starttls(), self); try_smtp!(self.client.starttls(), self);
try_smtp!(self.client.upgrade_tls_stream(&self.client_info.ssl_context), try_smtp!(self.client.upgrade_tls_stream(&self.client_info.ssl_context),
@@ -289,29 +321,29 @@ impl EmailTransport for SmtpTransport {
let mut found = false; let mut found = false;
for mecanism in self.client_info.authentication_mecanisms.clone() { for mechanism in self.client_info.authentication_mechanisms.clone() {
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) { if self.server_info.as_ref().unwrap().supports_auth_mechanism(mechanism) {
found = true; found = true;
try_smtp!(self.client.auth(mecanism, &username, &password), self); try_smtp!(self.client.auth(mechanism, &username, &password), self);
break; break;
} }
} }
if !found { if !found {
info!("No supported authentication mecanisms available"); info!("No supported authentication mechanisms available");
} }
} }
} }
// Mail // Mail
let mail_options = match (self.server_info let mail_options = match (self.server_info
.as_ref() .as_ref()
.unwrap() .unwrap()
.supports_feature(&Extension::EightBitMime), .supports_feature(&Extension::EightBitMime),
self.server_info self.server_info
.as_ref() .as_ref()
.unwrap() .unwrap()
.supports_feature(&Extension::EightBitMime)) { .supports_feature(&Extension::SmtpUtfEight)) {
(true, true) => Some("BODY=8BITMIME SMTPUTF8"), (true, true) => Some("BODY=8BITMIME SMTPUTF8"),
(true, false) => Some("BODY=8BITMIME"), (true, false) => Some("BODY=8BITMIME"),
(false, _) => None, (false, _) => None,

View File

@@ -1,4 +1,5 @@
//! SMTP response, containing a mandatory return code and an optional text message //! SMTP response, containing a mandatory return code and an optional text
//! message
use std::str::FromStr; use std::str::FromStr;
use std::fmt::{Display, Formatter, Result}; use std::fmt::{Display, Formatter, Result};
@@ -275,7 +276,7 @@ impl Response {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{Severity, Category, Response, ResponseParser, Code}; use super::{Category, Code, Response, ResponseParser, Severity};
#[test] #[test]
fn test_severity_from_str() { fn test_severity_from_str() {

View File

@@ -1,26 +1,21 @@
//! TODO //! This transport is a stub that only logs the message, and always returns
//! succes
use transport::error::EmailResult; use transport::error::EmailResult;
use transport::smtp::response::Response; use transport::smtp::response::{Category, Code, Response, Severity};
use transport::EmailTransport; use transport::EmailTransport;
use transport::smtp::response::{Code, Category, Severity}; use email::SendableEmail;
/// TODO /// This transport does nothing exept logging the message enveloppe
pub struct StubEmailTransport; pub struct StubEmailTransport;
impl EmailTransport for StubEmailTransport { impl EmailTransport for StubEmailTransport {
fn send(&mut self, fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
to_addresses: Vec<String>,
from_address: String,
message: String,
message_id: String)
-> EmailResult {
let _ = message; info!("{}: from=<{}> to=<{:?}>",
info!("message '{}': from '{}' to '{:?}'", email.message_id(),
message_id, email.from_address(),
from_address, email.to_addresses());
to_addresses);
Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0), Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
vec!["Ok: email logged".to_string()])) vec!["Ok: email logged".to_string()]))
} }

View File

@@ -1,14 +1,5 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT extern crate lettre;
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
#[test] mod transport_smtp;
mod transport_stub;
fn foo() { mod transport_file;
assert!(true);
}

37
tests/transport_file.rs Normal file
View File

@@ -0,0 +1,37 @@
extern crate lettre;
use std::env::temp_dir;
use std::fs::File;
use std::fs::remove_file;
use std::io::Read;
use lettre::transport::file::FileEmailTransport;
use lettre::transport::EmailTransport;
use lettre::email::{EmailBuilder, SendableEmail};
#[test]
fn file_transport() {
let mut sender = FileEmailTransport::new(temp_dir());
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
.unwrap();
let result = sender.send(email.clone());
assert!(result.is_ok());
let message_id = email.message_id();
let file = format!("{}/{}.txt", temp_dir().to_str().unwrap(), message_id);
let mut f = File::open(file.clone()).unwrap();
let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer);
assert_eq!(buffer,
format!("{}: from=<user@localhost> to=<root@localhost>\n{}",
message_id,
email.message()));
remove_file(file).unwrap();
}

19
tests/transport_smtp.rs Normal file
View File

@@ -0,0 +1,19 @@
extern crate lettre;
use lettre::transport::smtp::SmtpTransportBuilder;
use lettre::transport::EmailTransport;
use lettre::email::EmailBuilder;
#[test]
fn simple_sender() {
let mut sender = SmtpTransportBuilder::localhost().unwrap().build();
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
.unwrap();
let result = sender.send(email);
assert!(result.is_ok());
}

19
tests/transport_stub.rs Normal file
View File

@@ -0,0 +1,19 @@
extern crate lettre;
use lettre::transport::stub::StubEmailTransport;
use lettre::transport::EmailTransport;
use lettre::email::EmailBuilder;
#[test]
fn stub_transport() {
let mut sender = StubEmailTransport;
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
.unwrap();
let result = sender.send(email);
assert!(result.is_ok());
}