Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af20cfa8ff | ||
|
|
7a9f9111a5 | ||
|
|
89c0be219d | ||
|
|
6ee7fdb3d1 | ||
|
|
b7ac3a897f | ||
|
|
c436716277 | ||
|
|
eabdb960b0 | ||
|
|
59ba9e84dc | ||
|
|
e569c030bc | ||
|
|
72aea756fa | ||
|
|
655ae6d2ff | ||
|
|
150536d242 | ||
|
|
7d707fab25 | ||
|
|
4ec34987f8 | ||
|
|
7f3680f125 | ||
|
|
67566c2152 | ||
|
|
d863a7677e | ||
|
|
c1fe40479b | ||
|
|
8d03545062 | ||
|
|
489a6e892e | ||
|
|
b3fe1e0f65 | ||
|
|
3612ffca7a | ||
|
|
7940ad6c15 | ||
|
|
5ffb169bc9 | ||
|
|
ea0bb256cd | ||
|
|
9f177047f8 | ||
|
|
48eb859804 | ||
|
|
8d9877233d | ||
|
|
09f61a9fc9 | ||
|
|
40e749a04a | ||
|
|
4efb560bc8 | ||
|
|
500c4fb39d | ||
|
|
d488910010 | ||
|
|
4155e44dbd | ||
|
|
401118ee68 | ||
|
|
e6dd9d5a46 | ||
|
|
c8187c4a7c | ||
|
|
8f211c88a8 | ||
|
|
62df24c5b1 | ||
|
|
7ac43b73c3 | ||
|
|
3c91c065d6 | ||
|
|
9e30e7185e | ||
|
|
4da9e16bfc | ||
|
|
2977eb0509 | ||
|
|
2884da8f90 | ||
|
|
31a7504d54 | ||
|
|
9a93feea96 | ||
|
|
f102f321d3 | ||
|
|
1ba47e473c | ||
|
|
3acf21a316 | ||
|
|
544894def9 | ||
|
|
f74fb4f89c | ||
|
|
085998c730 | ||
|
|
d3d7c4b44e | ||
|
|
88b9cfb847 | ||
|
|
250ed7bcf4 | ||
|
|
63d9216df3 | ||
|
|
54758ebde9 | ||
|
|
bd67d80d3e | ||
|
|
a91db14869 | ||
|
|
b5c6663629 | ||
|
|
4b4150ed99 | ||
|
|
5f911dce12 | ||
|
|
47d6870d93 | ||
|
|
51de392086 | ||
|
|
fefb5f7978 | ||
|
|
5d125bdbdb | ||
|
|
a1bf0170db | ||
|
|
5bedba4b24 | ||
|
|
813f09a314 | ||
|
|
6a6023431b | ||
|
|
7f6aa0ffae | ||
|
|
1830f084c0 | ||
|
|
49a995f68d | ||
|
|
9c34b5a055 |
@@ -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: x86_64-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 '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: .\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
|
||||
- env OPENSSL_LIB_DIR=C:/OpenSSL-Win64 OPENSSL_INCLUDE_DIR=C:/OpenSSL-Win64/include cargo build --verbose
|
||||
|
||||
42
.travis.yml
42
.travis.yml
@@ -1,21 +1,37 @@
|
||||
language: rust
|
||||
sudo: required
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- stable
|
||||
- beta
|
||||
- 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:
|
||||
- pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH
|
||||
- smtp-sink 2525 1000&
|
||||
script:
|
||||
- |
|
||||
travis-cargo build &&
|
||||
travis-cargo test &&
|
||||
travis-cargo doc
|
||||
- travis-cargo build
|
||||
- travis-cargo test
|
||||
- travis-cargo doc
|
||||
after_success:
|
||||
- travis-cargo --only stable doc-upload
|
||||
- travis-cargo --only stable coveralls
|
||||
|
||||
- travis-cargo --only nightly bench
|
||||
- travis-cargo --only stable coveralls --no-sudo
|
||||
- travis-cargo --only stable doc-upload
|
||||
env:
|
||||
global:
|
||||
secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc="
|
||||
|
||||
13
Cargo.toml
13
Cargo.toml
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
|
||||
name = "smtp"
|
||||
version = "0.1.1"
|
||||
description = "Simple SMTP client"
|
||||
name = "lettre"
|
||||
version = "0.5.1"
|
||||
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.7"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.3"
|
||||
|
||||
13
README.md
13
README.md
@@ -1,8 +1,8 @@
|
||||
rust-smtp [](https://travis-ci.org/amousset/rust-smtp) [](https://coveralls.io/github/amousset/rust-smtp?branch=master) [](https://crates.io/crates/smtp)[](./LICENSE)
|
||||
lettre [](https://travis-ci.org/lettre/lettre) [](https://coveralls.io/github/lettre/lettre?branch=master) [](https://crates.io/crates/lettre) [](./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,9 +11,14 @@ To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smtp = "0.1"
|
||||
lettre = "0.5"
|
||||
```
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
The tests require a mail server listening locally on port 25.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
|
||||
44
benches/transport_smtp.rs
Normal file
44
benches/transport_smtp.rs
Normal 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()
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate smtp;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use smtp::sender::SenderBuilder;
|
||||
use smtp::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 mut threads = Vec::new();
|
||||
for _ in 1..5 {
|
||||
|
||||
let th_sender = sender.clone();
|
||||
threads.push(thread::spawn(move || {
|
||||
|
||||
let email = EmailBuilder::new()
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.body("Hello World!")
|
||||
.subject("Hello")
|
||||
.build();
|
||||
|
||||
let _ = th_sender.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();
|
||||
|
||||
let mut sender = sender.lock().unwrap();
|
||||
let result = sender.send(email);
|
||||
sender.close();
|
||||
|
||||
match result {
|
||||
Ok(..) => info!("Email sent successfully"),
|
||||
Err(error) => error!("{:?}", error),
|
||||
}
|
||||
}
|
||||
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
reorder_imports = true
|
||||
@@ -1,94 +0,0 @@
|
||||
//! Provides authentication mecanisms
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
|
||||
use serialize::base64::{self, ToBase64, FromBase64};
|
||||
use serialize::hex::ToHex;
|
||||
use crypto::hmac::Hmac;
|
||||
use crypto::md5::Md5;
|
||||
use crypto::mac::Mac;
|
||||
|
||||
use NUL;
|
||||
use error::Error;
|
||||
|
||||
/// Represents authentication mecanisms
|
||||
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
|
||||
pub enum Mecanism {
|
||||
/// PLAIN authentication mecanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
Plain,
|
||||
/// CRAM-MD5 authentication mecanism
|
||||
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||
CramMd5,
|
||||
}
|
||||
|
||||
impl Display for Mecanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}",
|
||||
match *self {
|
||||
Mecanism::Plain => "PLAIN",
|
||||
Mecanism::CramMd5 => "CRAM-MD5",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mecanism {
|
||||
/// Does the mecanism supports initial response
|
||||
pub fn supports_initial_response(&self) -> bool {
|
||||
match *self {
|
||||
Mecanism::Plain => true,
|
||||
Mecanism::CramMd5 => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)),
|
||||
}
|
||||
},
|
||||
Mecanism::CramMd5 => {
|
||||
let encoded_challenge = match challenge {
|
||||
Some(challenge) => challenge,
|
||||
None => return Err(Error::ClientError("This mecanism does expect a challenge")),
|
||||
};
|
||||
|
||||
let decoded_challenge = match encoded_challenge.from_base64() {
|
||||
Ok(challenge) => challenge,
|
||||
Err(error) => return Err(Error::ChallengeParsingError(error)),
|
||||
};
|
||||
|
||||
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))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Mecanism;
|
||||
|
||||
#[test]
|
||||
fn test_plain() {
|
||||
let mecanism = Mecanism::Plain;
|
||||
|
||||
assert_eq!(mecanism.response("username", "password", None).unwrap(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cram_md5() {
|
||||
let mecanism = Mecanism::CramMd5;
|
||||
|
||||
assert_eq!(mecanism.response("alice", "wonderland",
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")).unwrap(),
|
||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! Simple email (very incomplete)
|
||||
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use email_format::{MimeMessage, Header, Mailbox};
|
||||
use time::{now, Tm};
|
||||
use email_format::{Header, Mailbox, MimeMessage};
|
||||
use time::{Tm, now};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Converts an adress or an address with an alias to a `Address`
|
||||
@@ -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
|
||||
@@ -206,57 +219,48 @@ pub struct SimpleSendableEmail {
|
||||
|
||||
impl SimpleSendableEmail {
|
||||
/// 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 {
|
||||
from: from_address.to_string(),
|
||||
to: vec![to_address.to_string()],
|
||||
to: to_address,
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,9 +269,9 @@ mod test {
|
||||
use time::now;
|
||||
|
||||
use uuid::Uuid;
|
||||
use email_format::{MimeMessage, Header};
|
||||
use email_format::{Header, MimeMessage};
|
||||
|
||||
use super::{SendableEmail, EmailBuilder, Email};
|
||||
use super::{Email, EmailBuilder, SendableEmail};
|
||||
|
||||
#[test]
|
||||
fn test_email_display() {
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
197
src/extension.rs
197
src/extension.rs
@@ -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));
|
||||
}
|
||||
}
|
||||
229
src/lib.rs
229
src/lib.rs
@@ -1,37 +1,39 @@
|
||||
//! # Rust SMTP 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
|
||||
//! 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.
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! 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:
|
||||
//!
|
||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and
|
||||
//! CRAM-MD5 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! 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
|
||||
//! * email: generates the email to be sent with the sender
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ### Simple example
|
||||
//!
|
||||
//! This is the most basic example of usage:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use smtp::sender::{Sender, SenderBuilder};
|
||||
//! use smtp::email::EmailBuilder;
|
||||
//! ```rust
|
||||
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//!
|
||||
//! // Create an email
|
||||
//! let email = EmailBuilder::new()
|
||||
@@ -41,12 +43,13 @@
|
||||
//! .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 =
|
||||
//! SmtpTransportBuilder::localhost().unwrap().build();
|
||||
//! // Send the email
|
||||
//! let result = sender.send(email);
|
||||
//! let result = mailer.send(email);
|
||||
//!
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
@@ -54,8 +57,12 @@
|
||||
//! ### Complete example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use smtp::sender::{Sender, SenderBuilder};
|
||||
//! use smtp::email::EmailBuilder;
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//! use lettre::transport::smtp::{SecurityLevel, SmtpTransport,
|
||||
//! SmtpTransportBuilder};
|
||||
//! use lettre::transport::smtp::authentication::Mechanism;
|
||||
//! use lettre::transport::smtp::SUBMISSION_PORT;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//!
|
||||
//! let mut builder = EmailBuilder::new();
|
||||
//! builder = builder.to(("user@example.org", "Alias name"));
|
||||
@@ -68,60 +75,48 @@
|
||||
//! 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 = 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 mechanisms
|
||||
//! .authentication_mechanisms(vec![Mechanism::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();
|
||||
//! ```
|
||||
//!
|
||||
//! ### Using the client directly
|
||||
//!
|
||||
//! 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;
|
||||
//!
|
||||
//! // Create a minimal email
|
||||
//! let email = SimpleSendableEmail::new(
|
||||
//! "test@example.com",
|
||||
//! "test@example.org",
|
||||
//! "Hello world !"
|
||||
//! );
|
||||
//!
|
||||
//! let mut sender = SenderBuilder::localhost().unwrap().build();
|
||||
//! let result = sender.send(email);
|
||||
//! assert!(result.is_ok());
|
||||
//! mailer.close();
|
||||
//! ```
|
||||
//!
|
||||
//! ### 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
|
||||
//! use smtp::client::Client;
|
||||
//! use smtp::client::net::SmtpStream;
|
||||
//! use smtp::SMTP_PORT;
|
||||
//! use std::net::TcpStream;
|
||||
//! ```rust
|
||||
//! 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), None);
|
||||
//! let _ = email_client.ehlo("my_hostname");
|
||||
//! let _ = email_client.mail("user@example.com", None);
|
||||
//! let _ = email_client.rcpt("user@example.org");
|
||||
@@ -129,50 +124,86 @@
|
||||
//! let _ = email_client.message("Test email");
|
||||
//! 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)]
|
||||
|
||||
#[macro_use] extern crate log;
|
||||
extern crate rustc_serialize as serialize;
|
||||
#![deny(missing_docs, unsafe_code, unstable_features)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate rustc_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";
|
||||
|
||||
558
src/response.rs
558
src/response.rs
@@ -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::Result as RResult;
|
||||
|
||||
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) -> RResult<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) -> RResult<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) -> RResult<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) -> RResult<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);
|
||||
}
|
||||
}
|
||||
258
src/sender.rs
258
src/sender.rs
@@ -1,258 +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,
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
}),
|
||||
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
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
|
||||
// TODO: Use PLAIN AUTH in encrypted connections, CRAM-MD5 otherwise
|
||||
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
|
||||
|
||||
let (username, password) = self.client_info.credentials.clone().unwrap();
|
||||
|
||||
if self.server_info.as_ref().unwrap().supports_auth_mecanism(Mecanism::CramMd5) {
|
||||
let result = self.client.auth(Mecanism::CramMd5, &username, &password);
|
||||
try_smtp!(result, self);
|
||||
} else if self.server_info.as_ref().unwrap().supports_auth_mecanism(Mecanism::Plain) {
|
||||
let result = self.client.auth(Mecanism::Plain, &username, &password);
|
||||
try_smtp!(result, self);
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
use std::error::Error as StdError;
|
||||
use std::io;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use response::{Severity, Response};
|
||||
use serialize::base64::FromBase64Error;
|
||||
use transport::smtp::response::{Response, Severity};
|
||||
use rustc_serialize::base64::FromBase64Error;
|
||||
use self::Error::*;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
@@ -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,9 +82,4 @@ impl From<&'static str> for Error {
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type SmtpResult = Result<Response, Error>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// TODO
|
||||
}
|
||||
pub type EmailResult = Result<Response, Error>;
|
||||
53
src/transport/file/mod.rs
Normal file
53
src/transport/file/mod.rs
Normal 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) {
|
||||
()
|
||||
}
|
||||
}
|
||||
16
src/transport/mod.rs
Normal file
16
src/transport/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
//! Represents an Email transport
|
||||
pub mod smtp;
|
||||
pub mod error;
|
||||
pub mod stub;
|
||||
pub mod file;
|
||||
|
||||
use transport::error::EmailResult;
|
||||
use email::SendableEmail;
|
||||
|
||||
/// Transport method for emails
|
||||
pub trait EmailTransport {
|
||||
/// Sends the email
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult;
|
||||
/// Close the transport explicitely
|
||||
fn close(&mut self);
|
||||
}
|
||||
112
src/transport/smtp/authentication.rs
Normal file
112
src/transport/smtp/authentication.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! Provides authentication mechanisms
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use rustc_serialize::base64::{self, FromBase64, ToBase64};
|
||||
use rustc_serialize::hex::ToHex;
|
||||
use crypto::hmac::Hmac;
|
||||
use crypto::md5::Md5;
|
||||
use crypto::mac::Mac;
|
||||
|
||||
use transport::smtp::NUL;
|
||||
use transport::error::Error;
|
||||
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
|
||||
pub enum Mechanism {
|
||||
/// PLAIN authentication mechanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
Plain,
|
||||
/// CRAM-MD5 authentication mechanism
|
||||
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||
CramMd5,
|
||||
}
|
||||
|
||||
impl Display for Mechanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"{}",
|
||||
match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::CramMd5 => "CRAM-MD5",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports initial response
|
||||
pub fn supports_initial_response(&self) -> bool {
|
||||
match *self {
|
||||
Mechanism::Plain => true,
|
||||
Mechanism::CramMd5 => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
Mechanism::Plain => {
|
||||
match challenge {
|
||||
Some(_) =>
|
||||
Err(Error::ClientError("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
|
||||
.as_bytes()
|
||||
.to_base64(base64::STANDARD)),
|
||||
}
|
||||
}
|
||||
Mechanism::CramMd5 => {
|
||||
let encoded_challenge = match challenge {
|
||||
Some(challenge) => challenge,
|
||||
None =>
|
||||
return Err(Error::ClientError("This mechanism does expect a challenge")),
|
||||
};
|
||||
|
||||
let decoded_challenge = match encoded_challenge.from_base64() {
|
||||
Ok(challenge) => challenge,
|
||||
Err(error) => return Err(Error::ChallengeParsingError(error)),
|
||||
};
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Mechanism;
|
||||
|
||||
#[test]
|
||||
fn test_plain() {
|
||||
let mechanism = Mechanism::Plain;
|
||||
|
||||
assert_eq!(mechanism.response("username", "password", None).unwrap(),
|
||||
"AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||
assert!(mechanism.response("username", "password", Some("test")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cram_md5() {
|
||||
let mechanism = Mechanism::CramMd5;
|
||||
|
||||
assert_eq!(mechanism.response("alice",
|
||||
"wonderland",
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=\
|
||||
="))
|
||||
.unwrap(),
|
||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||
assert!(mechanism.response("alice", "wonderland", Some("tést")).is_err());
|
||||
assert!(mechanism.response("alice", "wonderland", None).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
//! SMTP client
|
||||
|
||||
use std::string::String;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::io;
|
||||
use std::io::{BufRead, Read, Write};
|
||||
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::error::{EmailResult, Error};
|
||||
use transport::smtp::response::ResponseParser;
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
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,55 @@ 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<()> {
|
||||
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,
|
||||
ssl_context: Option<&SslContext>)
|
||||
-> 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, ssl_context)));
|
||||
|
||||
self.get_reply()
|
||||
}
|
||||
@@ -97,22 +116,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 +134,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 +162,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 {
|
||||
/// Sends an AUTH command with the given mechanism
|
||||
pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> EmailResult {
|
||||
|
||||
if mecanism.supports_initial_response() {
|
||||
self.command(&format!("AUTH {} {}", mecanism, try!(mecanism.response(username, password, None))))
|
||||
if mechanism.supports_initial_response() {
|
||||
self.command(&format!("AUTH {} {}",
|
||||
mechanism,
|
||||
try!(mechanism.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!(mechanism.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 +224,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));
|
||||
@@ -222,7 +249,7 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{escape_dot, remove_crlf, escape_crlf};
|
||||
use super::{escape_crlf, escape_dot, remove_crlf};
|
||||
|
||||
#[test]
|
||||
fn test_escape_dot() {
|
||||
@@ -236,19 +263,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>");
|
||||
}
|
||||
}
|
||||
94
src/transport/smtp/client/net.rs
Normal file
94
src/transport/smtp/client/net.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! A trait to represent a stream
|
||||
|
||||
use std::io;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::{SocketAddr, 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: Sized {
|
||||
/// 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::connect_generic(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::connect_generic(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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
220
src/transport/smtp/extension.rs
Normal file
220
src/transport/smtp/extension.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
//! ESMTP features
|
||||
|
||||
use std::result::Result;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use transport::error::Error;
|
||||
use transport::smtp::response::Response;
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
|
||||
/// 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 mechanism
|
||||
Authentication(Mechanism),
|
||||
}
|
||||
|
||||
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 mechanism) => write!(f, "{} {}", "AUTH", mechanism),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 &mechanism in &splitted[1..] {
|
||||
match mechanism {
|
||||
"PLAIN" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Plain));
|
||||
}
|
||||
"CRAM-MD5" => {
|
||||
features.insert(Extension::Authentication(Mechanism::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_mechanism(&self, mechanism: Mechanism) -> bool {
|
||||
self.features.contains(&Extension::Authentication(mechanism))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{Extension, ServerInfo};
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
use transport::smtp::response::{Category, Code, Response, Severity};
|
||||
|
||||
#[test]
|
||||
fn test_extension_fmt() {
|
||||
assert_eq!(format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string());
|
||||
assert_eq!(format!("{}", Extension::Authentication(Mechanism::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(Mechanism::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_mechanism(Mechanism::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(Mechanism::Plain)));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::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_mechanism(Mechanism::Plain));
|
||||
assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
|
||||
assert!(!server_info2.supports_feature(&Extension::StartTls));
|
||||
}
|
||||
}
|
||||
401
src/transport/smtp/mod.rs
Normal file
401
src/transport/smtp/mod.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
//! Sends an email using the client
|
||||
|
||||
use std::string::String;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
|
||||
use openssl::ssl::{SslContext, SslMethod};
|
||||
|
||||
use transport::error::{EmailResult, Error};
|
||||
use transport::smtp::extension::{Extension, ServerInfo};
|
||||
use transport::smtp::client::Client;
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
use transport::EmailTransport;
|
||||
use email::SendableEmail;
|
||||
|
||||
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 {
|
||||
/// 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,
|
||||
/// Use TLS when available (with STARTTLS)
|
||||
///
|
||||
/// Default mode.
|
||||
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 mechanism, sorted by priority
|
||||
authentication_mechanisms: Vec<Mechanism>,
|
||||
}
|
||||
|
||||
/// 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_mechanisms: vec![Mechanism::CramMd5, Mechanism::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
|
||||
}
|
||||
|
||||
/// Set the security level for SSL/TLS
|
||||
pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder {
|
||||
self.security_level = level;
|
||||
self
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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 mechanisms
|
||||
pub fn authentication_mechanisms(mut self, mechanisms: Vec<Mechanism>) -> SmtpTransportBuilder {
|
||||
self.authentication_mechanisms = mechanisms;
|
||||
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<T: SendableEmail>(&mut self, email: T) -> EmailResult {
|
||||
|
||||
// Extract email information
|
||||
let message_id = email.message_id();
|
||||
let from_address = email.from_address();
|
||||
let to_addresses = email.to_addresses();
|
||||
let message = email.message();
|
||||
|
||||
// Check if the connection is still available
|
||||
if self.state.connection_reuse_count > 0 {
|
||||
if !self.client.is_connected() {
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if self.state.connection_reuse_count == 0 {
|
||||
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
|
||||
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, _) => (),
|
||||
(&SecurityLevel::EncryptedWrapper, _) => (),
|
||||
(_, 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 mechanism in self.client_info.authentication_mechanisms.clone() {
|
||||
if self.server_info.as_ref().unwrap().supports_auth_mechanism(mechanism) {
|
||||
found = true;
|
||||
try_smtp!(self.client.auth(mechanism, &username, &password), self);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
info!("No supported authentication mechanisms 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::SmtpUtfEight)) {
|
||||
(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();
|
||||
}
|
||||
}
|
||||
589
src/transport/smtp/response.rs
Normal file
589
src/transport/smtp/response.rs
Normal file
@@ -0,0 +1,589 @@
|
||||
//! 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::{Category, Code, Response, ResponseParser, Severity};
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
26
src/transport/stub/mod.rs
Normal file
26
src/transport/stub/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
//! This transport is a stub that only logs the message, and always returns
|
||||
//! succes
|
||||
|
||||
use transport::error::EmailResult;
|
||||
use transport::smtp::response::{Category, Code, Response, Severity};
|
||||
use transport::EmailTransport;
|
||||
use email::SendableEmail;
|
||||
|
||||
/// This transport does nothing exept logging the message enveloppe
|
||||
pub struct StubEmailTransport;
|
||||
|
||||
impl EmailTransport for StubEmailTransport {
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
|
||||
|
||||
info!("{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
email.from_address(),
|
||||
email.to_addresses());
|
||||
Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
|
||||
vec!["Ok: email logged".to_string()]))
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
()
|
||||
}
|
||||
}
|
||||
17
tests/lib.rs
17
tests/lib.rs
@@ -1,14 +1,5 @@
|
||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||
// 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.
|
||||
extern crate lettre;
|
||||
|
||||
#[test]
|
||||
|
||||
fn foo() {
|
||||
assert!(true);
|
||||
}
|
||||
mod transport_smtp;
|
||||
mod transport_stub;
|
||||
mod transport_file;
|
||||
|
||||
37
tests/transport_file.rs
Normal file
37
tests/transport_file.rs
Normal 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
19
tests/transport_smtp.rs
Normal 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
19
tests/transport_stub.rs
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user