Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f9bada4c | ||
|
|
ce7d55ffa8 | ||
|
|
a6ea43a842 | ||
|
|
eac29768ae | ||
|
|
d944aed9d3 | ||
|
|
67318ac759 | ||
|
|
90999bfc24 | ||
|
|
7635830399 | ||
|
|
4fbd06e18b | ||
|
|
5247e2c2aa | ||
|
|
8e21de8de3 | ||
|
|
d976f48b7b | ||
|
|
9ed51a2d3d | ||
|
|
ff72bcf5ef | ||
|
|
4d934685a8 | ||
|
|
a616c0d4c0 | ||
|
|
b938e2d757 | ||
|
|
b4603b4fbc | ||
|
|
495c21b776 | ||
|
|
bc874fa8f4 | ||
|
|
86c45f13c3 | ||
|
|
46ae195ba6 | ||
|
|
75a85831ab | ||
|
|
95e9f31141 | ||
|
|
79ac38c84d | ||
|
|
e2b4a964e7 | ||
|
|
f5f4c026bc | ||
|
|
efc42f5f22 | ||
|
|
d201a56571 | ||
|
|
f57279bdb7 | ||
|
|
d967bbb126 | ||
|
|
2553b32196 | ||
|
|
3f6fc56b3e | ||
|
|
97da0c0869 | ||
|
|
0acc57d36d | ||
|
|
d1bef702d6 | ||
|
|
d20d4f038b | ||
|
|
b7039a7a69 | ||
|
|
8fda23435c | ||
|
|
4f4a1436ae | ||
|
|
a8324795e5 | ||
|
|
f347dcc97b | ||
|
|
df69c8b7c2 | ||
|
|
fd20e90bb5 | ||
|
|
1da35ec17f | ||
|
|
69f29cfb45 | ||
|
|
e047aedee1 | ||
|
|
d14236be4d | ||
|
|
c814300f96 | ||
|
|
3171dec6e9 | ||
|
|
e9b658d868 | ||
|
|
72b8bc3866 | ||
|
|
f3742adc9f | ||
|
|
c5fec283f7 | ||
|
|
2d9ad22102 | ||
|
|
d0eab7a09f | ||
|
|
4f6e6185fc | ||
|
|
256624f3d8 | ||
|
|
74de004e6c | ||
|
|
b7d81016e1 | ||
|
|
6bd2b364ec | ||
|
|
bc764aec5a | ||
|
|
d18eec4d1b | ||
|
|
ec68ca2ca8 | ||
|
|
c9076fef63 | ||
|
|
c770dc6205 | ||
|
|
da8c733939 | ||
|
|
c21bdaff43 | ||
|
|
dab8b111d3 | ||
|
|
521681c0f7 | ||
|
|
2e3f82b98a | ||
|
|
2816e8ee73 | ||
|
|
4441be6b7b | ||
|
|
b992ca9694 | ||
|
|
d7e100692b | ||
|
|
2c979a6fbd | ||
|
|
f8c883f58e | ||
|
|
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 |
@@ -1,15 +1,24 @@
|
||||
environment:
|
||||
OPENSSL_INCLUDE_DIR: C:\OpenSSL\include
|
||||
OPENSSL_LIB_DIR: C:\OpenSSL\lib
|
||||
OPENSSL_LIBS: ssleay32:libeay32
|
||||
matrix:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
BITS: 32
|
||||
# - TARGET: x86_64-pc-windows-msvc
|
||||
# BITS: 64
|
||||
|
||||
install:
|
||||
- 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
|
||||
- ps: Start-FileDownload "http://slproweb.com/download/Win${env:BITS}OpenSSL-1_0_2g.exe"
|
||||
- Win%BITS%OpenSSL-1_0_2g.exe /SILENT /VERYSILENT /SP- /DIR="C:\OpenSSL"
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-beta-${env:TARGET}.exe"
|
||||
- rust-beta-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
|
||||
- SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
|
||||
- SET PATH=%PATH%;C:\MinGW\bin
|
||||
- rustc -V
|
||||
- cargo -V
|
||||
|
||||
build: false
|
||||
|
||||
test_script:
|
||||
- cargo test --verbose --no-default-features
|
||||
- cargo build --verbose
|
||||
|
||||
51
.travis.yml
51
.travis.yml
@@ -1,20 +1,49 @@
|
||||
language: rust
|
||||
sudo: required
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: 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/doc.sh
|
||||
- ./.travis/coverage.sh
|
||||
- travis-cargo --only nightly bench
|
||||
|
||||
env:
|
||||
global:
|
||||
|
||||
20
.travis/coverage.sh
Executable file
20
.travis/coverage.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
|
||||
if [ "$TRAVIS_RUST_VERSION" != "stable" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cargo test --no-run
|
||||
|
||||
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz
|
||||
tar xzf master.tar.gz
|
||||
mkdir kcov-master/build
|
||||
cd kcov-master/build
|
||||
cmake ..
|
||||
make
|
||||
make install DESTDIR=../tmp
|
||||
cd ../..
|
||||
ls target/debug
|
||||
./kcov-master/tmp/usr/local/bin/kcov --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov target/debug/lettre-*
|
||||
38
.travis/doc.sh
Executable file
38
.travis/doc.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit
|
||||
|
||||
if [ "$TRAVIS_RUST_VERSION" != "stable" ] || [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cargo clean
|
||||
cargo doc --no-deps
|
||||
|
||||
git clone --branch gh-pages "https://$GH_TOKEN@github.com/${TRAVIS_REPO_SLUG}.git" deploy_docs
|
||||
cd deploy_docs
|
||||
|
||||
git config user.email "contact@amousset.me"
|
||||
git config user.name "Alexis Mousset"
|
||||
|
||||
if [ "$TRAVIS_BRANCH" == "master" ]; then
|
||||
rm -rf master
|
||||
mv ../target/doc ./master
|
||||
echo "<meta http-equiv=refresh content=0;url=lettre/index.html>" > ./master/index.html
|
||||
elif [ "$TRAVIS_TAG" != "" ]; then
|
||||
rm -rf $TRAVIS_TAG
|
||||
mv ../target/doc ./$TRAVIS_TAG
|
||||
echo "<meta http-equiv=refresh content=0;url=lettre/index.html>" > ./$TRAVIS_TAG/index.html
|
||||
|
||||
latest=$(echo * | tr " " "\n" | sort -V -r | head -n1)
|
||||
if [ "$TRAVIS_TAG" == "$latest" ]; then
|
||||
|
||||
echo "<meta http-equiv=refresh content=0;url=$latest/lettre/index.html>" > index.html
|
||||
fi
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git add -A .
|
||||
git commit -m "Rebuild pages at ${TRAVIS_COMMIT}"
|
||||
git push --quiet origin gh-pages
|
||||
30
CHANGELOG.md
Normal file
30
CHANGELOG.md
Normal file
@@ -0,0 +1,30 @@
|
||||
### v0.6.1 (2016-10-19)
|
||||
|
||||
#### Features
|
||||
|
||||
* **documentation**
|
||||
* #91: Build seperate docs for each release
|
||||
* #96: Add complete documentation information to README
|
||||
|
||||
#### Bugfixes
|
||||
|
||||
* **email**
|
||||
* #85: Use address-list for "To", "From" etc.
|
||||
|
||||
* **tests**
|
||||
* #93: Force building tests before coverage computing
|
||||
|
||||
### v0.6.0 (2016-05-05)
|
||||
|
||||
#### Features
|
||||
|
||||
* **email**
|
||||
* multipart support
|
||||
* add non-consuming methods for Email builders
|
||||
|
||||
#### Beaking Change
|
||||
|
||||
* **email**
|
||||
* `add_header` does not return the builder anymore,
|
||||
for consistency with other methods. Use the `header`
|
||||
method instead
|
||||
38
CONTRIBUTING.md
Normal file
38
CONTRIBUTING.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## Contributing to Lettre
|
||||
|
||||
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
||||
|
||||
### Code formatting
|
||||
|
||||
All code must be formatted using `rustfmt`.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
Each commit message consists of a header, a body and a footer. The header has a special format that includes a type, a scope and a subject:
|
||||
|
||||
```text
|
||||
<type>(<scope>): <subject> <BLANK LINE> <body> <BLANK LINE> <footer>
|
||||
```
|
||||
|
||||
Any line of the commit message cannot be longer 72 characters.
|
||||
|
||||
**type** must be one of the following:
|
||||
|
||||
feat: A new feature
|
||||
fix: A bug fix
|
||||
docs: Documentation only changes
|
||||
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
refactor: A code change that neither fixes a bug or adds a feature
|
||||
perf: A code change that improves performance
|
||||
test: Adding missing tests
|
||||
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
|
||||
|
||||
**scope** is the lettre part that is being touched. Examples:
|
||||
|
||||
email
|
||||
transport-smtp
|
||||
transport-file
|
||||
transport
|
||||
all
|
||||
|
||||
The body explains the change, and the footer contains relevant changelog notes and references to fixed issues.
|
||||
17
Cargo.toml
17
Cargo.toml
@@ -1,24 +1,25 @@
|
||||
[package]
|
||||
|
||||
name = "lettre"
|
||||
version = "0.4.0"
|
||||
version = "0.6.1"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
documentation = "http://lettre.github.io/lettre/"
|
||||
documentation = "https://lettre.github.io/lettre/"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
keywords = ["email", "smtp", "mailer"]
|
||||
|
||||
[dependencies]
|
||||
time = "0.1"
|
||||
uuid = "0.1"
|
||||
log = "0.3"
|
||||
rustc-serialize = "0.3"
|
||||
rust-crypto = "0.2"
|
||||
bufstream = "0.1"
|
||||
email = "0.0"
|
||||
openssl = "0.6"
|
||||
log = "0.3"
|
||||
mime = "0.2"
|
||||
openssl = "0.8"
|
||||
rustc-serialize = "0.3"
|
||||
rust-crypto = "0.2"
|
||||
time = "0.1"
|
||||
uuid = { version = "0.3", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.3"
|
||||
|
||||
46
README.md
46
README.md
@@ -1,22 +1,50 @@
|
||||
lettre [](https://travis-ci.org/lettre/lettre) [](https://coveralls.io/github/lettre/lettre?branch=master) [](https://crates.io/crates/lettre) [](./LICENSE)
|
||||
=========
|
||||
# lettre
|
||||
[](https://travis-ci.org/lettre/lettre)
|
||||
[](https://ci.appveyor.com/project/amousset/lettre/branch/master)
|
||||
[](https://coveralls.io/github/lettre/lettre?branch=master)
|
||||
[](https://crates.io/crates/lettre)
|
||||
[](./LICENSE)
|
||||
[](https://gitter.im/lettre/lettre?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
This is an email library written in Rust.
|
||||
See the [documentation](http://lettre.github.io/lettre) for more information.
|
||||
|
||||
Install
|
||||
-------
|
||||
## Features
|
||||
|
||||
Lettre provides the following features:
|
||||
|
||||
* Multiple transport methods
|
||||
* Unicode support (for email content and addresses)
|
||||
* Secure delivery with SMTP using encryption and authentication
|
||||
* Easy email builders
|
||||
|
||||
## Documentation
|
||||
|
||||
Released versions:
|
||||
|
||||
* [latest](https://lettre.github.io/lettre/)
|
||||
* [v0.6.1](https://lettre.github.io/lettre/v0.6.1/lettre/)
|
||||
* [v0.6.0](https://lettre.github.io/lettre/v0.6.0/lettre/)
|
||||
* [v0.5.1](https://lettre.github.io/lettre/v0.5.1/lettre/)
|
||||
|
||||
Development version:
|
||||
|
||||
* [master](https://lettre.github.io/lettre/master/lettre/)
|
||||
|
||||
## Install
|
||||
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.4"
|
||||
lettre = "0.6"
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
## Testing
|
||||
|
||||
The tests require an open mail server listening locally on port 25.
|
||||
|
||||
## License
|
||||
|
||||
This program is distributed under the terms of the MIT license.
|
||||
|
||||
See LICENSE for details.
|
||||
See [LICENSE](./LICENSE) for details.
|
||||
|
||||
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,57 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate lettre;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use lettre::transport::smtp::SmtpTransportBuilder;
|
||||
use lettre::transport::EmailTransport;
|
||||
use lettre::mailer::Mailer;
|
||||
use lettre::email::EmailBuilder;
|
||||
|
||||
fn main() {
|
||||
env_logger::init().unwrap();
|
||||
|
||||
let sender = SmtpTransportBuilder::localhost().unwrap().hello_name("localhost")
|
||||
.connection_reuse(true).build();
|
||||
let mailer = Arc::new(Mutex::new(Mailer::new(sender)));
|
||||
|
||||
let mut threads = Vec::new();
|
||||
for _ in 1..5 {
|
||||
|
||||
let th_mailer = mailer.clone();
|
||||
threads.push(thread::spawn(move || {
|
||||
|
||||
let email = EmailBuilder::new()
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.body("Hello World!")
|
||||
.subject("Hello")
|
||||
.build().unwrap();
|
||||
|
||||
let _ = th_mailer.lock().unwrap().send(email);
|
||||
}));
|
||||
}
|
||||
|
||||
for thread in threads {
|
||||
let _ = thread.join();
|
||||
}
|
||||
|
||||
let email = EmailBuilder::new()
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.body("Hello World!")
|
||||
.subject("Hello Bis")
|
||||
.build().unwrap();
|
||||
|
||||
let mut mailer = mailer.lock().unwrap();
|
||||
let result = mailer.send(email);
|
||||
mailer.close();
|
||||
|
||||
match result {
|
||||
Ok(..) => info!("Email sent successfully"),
|
||||
Err(error) => error!("{:?}", error),
|
||||
}
|
||||
}
|
||||
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
reorder_imports = true
|
||||
31
src/email/error.rs
Normal file
31
src/email/error.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! Error and result type for emails
|
||||
|
||||
|
||||
use self::Error::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Missinf sender
|
||||
MissingFrom,
|
||||
/// Missing recipient
|
||||
MissingTo,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
MissingFrom => "the sender is missing",
|
||||
MissingTo => "the recipient is missing",
|
||||
}
|
||||
}
|
||||
}
|
||||
591
src/email/mod.rs
591
src/email/mod.rs
@@ -1,13 +1,16 @@
|
||||
//! Simple email (very incomplete)
|
||||
//! Simple email representation
|
||||
pub mod error;
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use email::error::Error;
|
||||
|
||||
use email_format::{Header, Mailbox, Address, MimeMessage, MimeMultipartType};
|
||||
use mime::Mime;
|
||||
use std::fmt;
|
||||
|
||||
use email_format::{MimeMessage, Header, Mailbox};
|
||||
use time::{now, Tm};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use time::{Tm, now};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Converts an adress or an address with an alias to a `Address`
|
||||
/// Converts an address or an address with an alias to a `Header`
|
||||
pub trait ToHeader {
|
||||
/// Converts to a `Header` struct
|
||||
fn to_header(&self) -> Header;
|
||||
@@ -15,7 +18,7 @@ pub trait ToHeader {
|
||||
|
||||
impl ToHeader for Header {
|
||||
fn to_header(&self) -> Header {
|
||||
(*self).clone()
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,28 +54,223 @@ impl<'a> ToMailbox for (&'a str, &'a str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Can be transformed to a sendable email
|
||||
pub trait IntoEmail {
|
||||
/// Builds an email
|
||||
fn into_email(&self) -> Result<Email, Error>;
|
||||
}
|
||||
|
||||
impl IntoEmail for SimpleEmail {
|
||||
fn into_email(&self) -> Result<Email, Error> {
|
||||
let mut builder = EmailBuilder::new();
|
||||
|
||||
if self.from.is_some() {
|
||||
builder.add_from(self.from.as_ref().unwrap().as_str().to_mailbox());
|
||||
}
|
||||
|
||||
for to_address in self.to.as_slice() {
|
||||
builder.add_to(to_address.as_str().to_mailbox());
|
||||
}
|
||||
|
||||
for cc_address in self.cc.as_slice() {
|
||||
builder.add_cc(cc_address.as_str().to_mailbox());
|
||||
}
|
||||
|
||||
// No bcc for now
|
||||
|
||||
if self.reply_to.is_some() {
|
||||
builder.add_reply_to(self.reply_to.as_ref().unwrap().as_str().to_mailbox());
|
||||
}
|
||||
|
||||
if self.subject.is_some() {
|
||||
builder.set_subject(self.subject.as_ref().unwrap().as_str());
|
||||
}
|
||||
|
||||
// No date for now
|
||||
|
||||
match (self.text.as_ref(), self.html.as_ref()) {
|
||||
(Some(text), Some(html)) => builder.set_alternative(html.as_str(), text.as_str()),
|
||||
(Some(text), None) => builder.set_text(text.as_str()),
|
||||
(None, Some(html)) => builder.set_html(html.as_str()),
|
||||
(None, None) => (),
|
||||
}
|
||||
|
||||
for header in self.headers.as_slice() {
|
||||
builder.add_header(header.to_header());
|
||||
}
|
||||
|
||||
Ok(builder.build().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Simple representation of an email, useful for some transports
|
||||
#[derive(PartialEq,Eq,Clone,Debug,Default)]
|
||||
pub struct SimpleEmail {
|
||||
from: Option<String>,
|
||||
to: Vec<String>,
|
||||
cc: Vec<String>,
|
||||
// bcc: Vec<String>,
|
||||
reply_to: Option<String>,
|
||||
subject: Option<String>,
|
||||
date: Option<Tm>,
|
||||
html: Option<String>,
|
||||
text: Option<String>,
|
||||
// attachments: Vec<String>,
|
||||
headers: Vec<Header>,
|
||||
}
|
||||
|
||||
impl SimpleEmail {
|
||||
/// Adds a generic header
|
||||
pub fn header<A: ToHeader>(mut self, header: A) -> SimpleEmail {
|
||||
self.add_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a generic header
|
||||
pub fn add_header<A: ToHeader>(&mut self, header: A) {
|
||||
self.headers.push(header.to_header());
|
||||
}
|
||||
|
||||
/// Adds a `From` header and stores the sender address
|
||||
pub fn from<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
|
||||
self.add_from(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `From` header and stores the sender address
|
||||
pub fn add_from<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.from = Some(mailbox.address);
|
||||
}
|
||||
|
||||
/// Adds a `To` header and stores the recipient address
|
||||
pub fn to<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
|
||||
self.add_to(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and stores the recipient address
|
||||
pub fn add_to<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.to.push(mailbox.address);
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and stores the recipient address
|
||||
pub fn cc<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
|
||||
self.add_cc(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and stores the recipient address
|
||||
pub fn add_cc<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.cc.push(mailbox.address);
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
|
||||
self.add_reply_to(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn add_reply_to<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.reply_to = Some(mailbox.address);
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject(mut self, subject: &str) -> SimpleEmail {
|
||||
self.set_subject(subject);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn set_subject(&mut self, subject: &str) {
|
||||
self.subject = Some(subject.to_string());
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: &Tm) -> SimpleEmail {
|
||||
self.set_date(date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn set_date(&mut self, date: &Tm) {
|
||||
self.date = Some(date.clone());
|
||||
}
|
||||
|
||||
/// Sets the email body to plain text content
|
||||
pub fn text(mut self, body: &str) -> SimpleEmail {
|
||||
self.set_text(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body to plain text content
|
||||
pub fn set_text(&mut self, body: &str) {
|
||||
self.text = Some(body.to_string());
|
||||
}
|
||||
|
||||
/// Sets the email body to HTML content
|
||||
pub fn html(mut self, body: &str) -> SimpleEmail {
|
||||
self.set_html(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body to HTML content
|
||||
pub fn set_html(&mut self, body: &str) {
|
||||
self.html = Some(body.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a `MimeMessage` structure
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct PartBuilder {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
}
|
||||
|
||||
/// Builds an `Email` structure
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct EmailBuilder {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
/// The enveloppe recipients addresses
|
||||
message: PartBuilder,
|
||||
/// The recipients' addresses for the mail header
|
||||
to_header: Vec<Address>,
|
||||
/// The sender addresses for the mail header
|
||||
from_header: Vec<Address>,
|
||||
/// The Cc addresses for the mail header
|
||||
cc_header: Vec<Address>,
|
||||
/// The Reply-To addresses for the mail header
|
||||
reply_to_header: Vec<Address>,
|
||||
/// The sender address for the mail header
|
||||
sender_header: Option<Mailbox>,
|
||||
/// The envelope recipients' addresses
|
||||
to: Vec<String>,
|
||||
/// The enveloppe sender address
|
||||
/// The envelope sender address
|
||||
from: Option<String>,
|
||||
/// Date issued
|
||||
date_issued: bool,
|
||||
}
|
||||
|
||||
/// Simple email enveloppe representation
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct Envelope {
|
||||
/// The envelope recipients' addresses
|
||||
to: Vec<String>,
|
||||
/// The envelope sender address
|
||||
from: String,
|
||||
}
|
||||
|
||||
/// Simple email representation
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct Email {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
/// The enveloppe recipients addresses
|
||||
to: Vec<String>,
|
||||
/// The enveloppe sender address
|
||||
from: String,
|
||||
/// Envelope
|
||||
envelope: Envelope,
|
||||
/// Message-ID
|
||||
message_id: Uuid,
|
||||
}
|
||||
@@ -83,11 +281,85 @@ impl Display for Email {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartBuilder {
|
||||
/// Creates a new empty part
|
||||
pub fn new() -> PartBuilder {
|
||||
PartBuilder { message: MimeMessage::new_blank_message() }
|
||||
}
|
||||
|
||||
/// Adds a generic header
|
||||
pub fn header<A: ToHeader>(mut self, header: A) -> PartBuilder {
|
||||
self.add_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a generic header
|
||||
pub fn add_header<A: ToHeader>(&mut self, header: A) {
|
||||
self.message.headers.insert(header.to_header());
|
||||
}
|
||||
|
||||
/// Sets the body
|
||||
pub fn body(mut self, body: &str) -> PartBuilder {
|
||||
self.set_body(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the body
|
||||
pub fn set_body(&mut self, body: &str) {
|
||||
self.message.body = body.to_string();
|
||||
}
|
||||
|
||||
|
||||
/// Defines a `MimeMultipartType` value
|
||||
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
|
||||
self.set_message_type(mime_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines a `MimeMultipartType` value
|
||||
pub fn set_message_type(&mut self, mime_type: MimeMultipartType) {
|
||||
self.message.message_type = Some(mime_type);
|
||||
}
|
||||
|
||||
/// Adds a `ContentType` header with the given MIME type
|
||||
pub fn content_type(mut self, content_type: Mime) -> PartBuilder {
|
||||
self.set_content_type(content_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `ContentType` header with the given MIME type
|
||||
pub fn set_content_type(&mut self, content_type: Mime) {
|
||||
self.add_header(("Content-Type", format!("{}", content_type).as_ref()));
|
||||
}
|
||||
|
||||
/// Adds a child part
|
||||
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
|
||||
self.add_child(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a child part
|
||||
pub fn add_child(&mut self, child: MimeMessage) {
|
||||
self.message.children.push(child);
|
||||
}
|
||||
|
||||
/// Gets builded `MimeMessage`
|
||||
pub fn build(mut self) -> MimeMessage {
|
||||
self.message.update_headers();
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
EmailBuilder {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
message: PartBuilder::new(),
|
||||
to_header: vec![],
|
||||
from_header: vec![],
|
||||
cc_header: vec![],
|
||||
reply_to_header: vec![],
|
||||
sender_header: None,
|
||||
to: vec![],
|
||||
from: None,
|
||||
date_issued: false,
|
||||
@@ -96,105 +368,254 @@ impl EmailBuilder {
|
||||
|
||||
/// Sets the email body
|
||||
pub fn body(mut self, body: &str) -> EmailBuilder {
|
||||
self.message.body = body.to_string();
|
||||
self.message.set_body(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
pub fn set_body(&mut self, body: &str) {
|
||||
self.message.set_body(body);
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
|
||||
self.message.add_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
|
||||
self.insert_header(header);
|
||||
self
|
||||
pub fn add_header<A: ToHeader>(&mut self, header: A) {
|
||||
self.message.add_header(header);
|
||||
}
|
||||
|
||||
fn insert_header<A: ToHeader>(&mut self, header: A) {
|
||||
self.message.headers.insert(header.to_header());
|
||||
}
|
||||
|
||||
/// Adds a `From` header and store the sender address
|
||||
/// Adds a `From` header and stores 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.from = Some(mailbox.address);
|
||||
self.add_from(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and store the recipient address
|
||||
/// Adds a `From` header and stores the sender address
|
||||
pub fn add_from<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.from = Some(mailbox.address.clone());
|
||||
self.from_header.push(Address::Mailbox(mailbox));
|
||||
}
|
||||
|
||||
/// Adds a `To` header and stores the recipient address
|
||||
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.to.push(mailbox.address);
|
||||
self.add_to(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and store the recipient address
|
||||
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
/// Adds a `To` header and stores the recipient address
|
||||
pub fn add_to<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("Cc", mailbox.to_string().as_ref()));
|
||||
self.to.push(mailbox.address);
|
||||
self.to.push(mailbox.address.clone());
|
||||
self.to_header.push(Address::Mailbox(mailbox));
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and stores the recipient address
|
||||
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
self.add_cc(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and stores the recipient address
|
||||
pub fn add_cc<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.to.push(mailbox.address.clone());
|
||||
self.cc_header.push(Address::Mailbox(mailbox));
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("Reply-To", mailbox.to_string().as_ref()));
|
||||
self.add_reply_to(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn add_reply_to<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.reply_to_header.push(Address::Mailbox(mailbox));
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
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.from = Some(mailbox.address);
|
||||
self.set_sender(address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
pub fn set_sender<A: ToMailbox>(&mut self, address: A) {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.from = Some(mailbox.address.clone());
|
||||
self.sender_header = Some(mailbox);
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject(mut self, subject: &str) -> EmailBuilder {
|
||||
self.insert_header(("Subject", subject));
|
||||
self.set_subject(subject);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn set_subject(&mut self, subject: &str) {
|
||||
self.message.add_header(("Subject", subject));
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
||||
self.insert_header(("Date", Tm::rfc822z(date).to_string().as_ref()));
|
||||
self.date_issued = true;
|
||||
self.set_date(date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Email
|
||||
pub fn build(mut self) -> Result<Email, &'static str> {
|
||||
if self.from.is_none() {
|
||||
return Err("No from address")
|
||||
}
|
||||
if self.to.is_empty() {
|
||||
return Err("No to address")
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn set_date(&mut self, date: &Tm) {
|
||||
self.message.add_header(("Date", Tm::rfc822z(date).to_string().as_ref()));
|
||||
self.date_issued = true;
|
||||
}
|
||||
|
||||
if !self.date_issued {
|
||||
self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
|
||||
/// Set the message type
|
||||
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
|
||||
self.set_message_type(message_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the message type
|
||||
pub fn set_message_type(&mut self, message_type: MimeMultipartType) {
|
||||
self.message.set_message_type(message_type);
|
||||
}
|
||||
|
||||
/// Adds a child
|
||||
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
|
||||
self.add_child(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a child
|
||||
pub fn add_child(&mut self, child: MimeMessage) {
|
||||
self.message.add_child(child);
|
||||
}
|
||||
|
||||
/// Sets the email body to plain text content
|
||||
pub fn text(mut self, body: &str) -> EmailBuilder {
|
||||
self.set_text(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body to plain text content
|
||||
pub fn set_text(&mut self, body: &str) {
|
||||
self.message.set_body(body);
|
||||
self.message
|
||||
.add_header(("Content-Type", format!("{}", mime!(Text/Plain; Charset=Utf8)).as_ref()));
|
||||
}
|
||||
|
||||
/// Sets the email body to HTML content
|
||||
pub fn html(mut self, body: &str) -> EmailBuilder {
|
||||
self.set_html(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body to HTML content
|
||||
pub fn set_html(&mut self, body: &str) {
|
||||
self.message.set_body(body);
|
||||
self.message
|
||||
.add_header(("Content-Type", format!("{}", mime!(Text/Html; Charset=Utf8)).as_ref()));
|
||||
}
|
||||
|
||||
/// Sets the email content
|
||||
pub fn alternative(mut self, body_html: &str, body_text: &str) -> EmailBuilder {
|
||||
self.set_alternative(body_html, body_text);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email content
|
||||
pub fn set_alternative(&mut self, body_html: &str, body_text: &str) {
|
||||
let mut alternate = PartBuilder::new();
|
||||
alternate.set_message_type(MimeMultipartType::Alternative);
|
||||
|
||||
let text = PartBuilder::new()
|
||||
.body(body_text)
|
||||
.header(("Content-Type", format!("{}", mime!(Text/Plain; Charset=Utf8)).as_ref()))
|
||||
.build();
|
||||
|
||||
let html = PartBuilder::new()
|
||||
.body(body_html)
|
||||
.header(("Content-Type", format!("{}", mime!(Text/Html; Charset=Utf8)).as_ref()))
|
||||
.build();
|
||||
|
||||
alternate.add_child(text);
|
||||
alternate.add_child(html);
|
||||
|
||||
self.set_message_type(MimeMultipartType::Mixed);
|
||||
self.add_child(alternate.build());
|
||||
}
|
||||
|
||||
/// Builds the Email
|
||||
pub fn build(mut self) -> Result<Email, Error> {
|
||||
if self.from.is_none() {
|
||||
return Err(Error::MissingFrom);
|
||||
}
|
||||
if self.to.is_empty() {
|
||||
return Err(Error::MissingTo);
|
||||
}
|
||||
// If there are multiple addresses in "From", the "Sender" is required.
|
||||
if self.from_header.len() >= 2 && self.sender_header.is_none() {
|
||||
// So, we must find something to put as Sender.
|
||||
for possible_sender in self.from_header.iter() {
|
||||
// Only a mailbox can be used as sender, not Address::Group.
|
||||
if let &Address::Mailbox(ref mbx) = possible_sender {
|
||||
self.sender_header = Some(mbx.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Address::Group is not yet supported, so the line below will never panic.
|
||||
// If groups are supported one day, add another Error for this case
|
||||
// and return it here, if sender_header is still None at this point.
|
||||
assert!(self.sender_header.is_some());
|
||||
}
|
||||
// Add the sender header, if any.
|
||||
if let Some(v) = self.sender_header {
|
||||
self.message.add_header(("Sender", v.to_string().as_ref()));
|
||||
}
|
||||
// Add the collected addresses as mailbox-list all at once.
|
||||
// The unwraps are fine because the conversions for Vec<Address> never errs.
|
||||
self.message.add_header(Header::new_with_value("To".into(), self.to_header).unwrap());
|
||||
self.message.add_header(Header::new_with_value("From".into(), self.from_header).unwrap());
|
||||
if !self.cc_header.is_empty() {
|
||||
self.message.add_header(Header::new_with_value("Cc".into(), self.cc_header).unwrap());
|
||||
}
|
||||
if !self.reply_to_header.is_empty() {
|
||||
self.message.add_header(Header::new_with_value("Reply-To".into(),
|
||||
self.reply_to_header)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
|
||||
if !self.date_issued {
|
||||
self.message.add_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
|
||||
}
|
||||
|
||||
self.message.add_header(("MIME-Version", "1.0"));
|
||||
|
||||
let message_id = Uuid::new_v4();
|
||||
|
||||
match Header::new_with_value("Message-ID".to_string(),
|
||||
if let Ok(header) = Header::new_with_value("Message-ID".to_string(),
|
||||
format!("<{}.lettre@localhost>", message_id)) {
|
||||
Ok(header) => self.insert_header(header),
|
||||
Err(_) => (),
|
||||
self.message.add_header(header)
|
||||
}
|
||||
|
||||
self.message.update_headers();
|
||||
|
||||
Ok(Email {
|
||||
message: self.message,
|
||||
message: self.message.build(),
|
||||
envelope: Envelope {
|
||||
to: self.to,
|
||||
from: self.from.unwrap(),
|
||||
},
|
||||
message_id: message_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Email sendable by an SMTP client
|
||||
pub trait SendableEmail {
|
||||
/// From address
|
||||
@@ -219,10 +640,10 @@ 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(),
|
||||
}
|
||||
}
|
||||
@@ -248,11 +669,11 @@ impl SendableEmail for SimpleSendableEmail {
|
||||
|
||||
impl SendableEmail for Email {
|
||||
fn to_addresses(&self) -> Vec<String> {
|
||||
self.to.clone()
|
||||
self.envelope.to.clone()
|
||||
}
|
||||
|
||||
fn from_address(&self) -> String {
|
||||
self.from.clone()
|
||||
self.envelope.from.clone()
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
@@ -266,12 +687,12 @@ impl SendableEmail for Email {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use email_format::{Header, MimeMessage};
|
||||
|
||||
use super::{Email, EmailBuilder, Envelope, SendableEmail};
|
||||
use time::now;
|
||||
|
||||
use uuid::Uuid;
|
||||
use email_format::{MimeMessage, Header};
|
||||
|
||||
use super::{SendableEmail, EmailBuilder, Email};
|
||||
|
||||
#[test]
|
||||
fn test_email_display() {
|
||||
@@ -279,8 +700,10 @@ mod test {
|
||||
|
||||
let mut email = Email {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
envelope: Envelope {
|
||||
to: vec![],
|
||||
from: "".to_string(),
|
||||
},
|
||||
message_id: current_message,
|
||||
};
|
||||
|
||||
@@ -303,7 +726,28 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_builder() {
|
||||
fn test_multiple_from() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
let email = email_builder.to("anna@example.com")
|
||||
.from("dieter@example.com")
|
||||
.from("joachim@example.com")
|
||||
.date(&date_now)
|
||||
.subject("Invitation")
|
||||
.body("We invite you!")
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(format!("{}", email),
|
||||
format!("Date: {}\r\nSubject: Invitation\r\nSender: \
|
||||
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
|
||||
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
|
||||
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
|
||||
date_now.rfc822z(),
|
||||
email.message_id()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_email_builder() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
@@ -315,15 +759,16 @@ mod test {
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.add_header(("X-test", "value"))
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
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",
|
||||
format!("Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
|
||||
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
|
||||
<reply@localhost>\r\nMIME-Version: 1.0\r\nMessage-ID: \
|
||||
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
|
||||
date_now.rfc822z(),
|
||||
email.message_id()));
|
||||
}
|
||||
@@ -341,7 +786,7 @@ mod test {
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.add_header(("X-test", "value"))
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
265
src/lib.rs
265
src/lib.rs
@@ -1,80 +1,140 @@
|
||||
//! # Rust email client
|
||||
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
|
||||
//!
|
||||
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
|
||||
//! 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
|
||||
//! ## Overview
|
||||
//!
|
||||
//! 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`.
|
||||
//!
|
||||
//! ## Creating messages
|
||||
//!
|
||||
//! The `email` part builds email messages. For now, it does not support attachments.
|
||||
//! An email is built using an `EmailBuilder`. The simplest email could be:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//!
|
||||
//! // Create an email
|
||||
//! let email = EmailBuilder::new()
|
||||
//! // Addresses can be specified by the tuple (email, alias)
|
||||
//! .to(("user@example.org", "Firstname Lastname"))
|
||||
//! // ... or by an address only
|
||||
//! .from("user@example.com")
|
||||
//! .subject("Hi, Hello world")
|
||||
//! .text("Hello world.")
|
||||
//! .build();
|
||||
//!
|
||||
//! assert!(email.is_ok());
|
||||
//! ```
|
||||
//!
|
||||
//! When the `build` method is called, the `EmailBuilder` will add the missing headers (like
|
||||
//! `Message-ID` or `Date`) and check for missing necessary ones (like `From` or `To`). It will
|
||||
//! then generate an `Email` that can be sent.
|
||||
//!
|
||||
//! The `text()` method will create a plain text email, while the `html()` method will create an
|
||||
//! HTML email. You can use the `alternative()` method to provide both versions, using plain text
|
||||
//! as fallback for the HTML version.
|
||||
//!
|
||||
//! Below is a more complete example, not using method chaining:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//!
|
||||
//! let mut builder = EmailBuilder::new();
|
||||
//! builder.add_to(("user@example.org", "Alias name"));
|
||||
//! builder.add_cc(("user@example.net", "Alias name"));
|
||||
//! builder.add_from("no-reply@example.com");
|
||||
//! builder.add_from("no-reply@example.eu");
|
||||
//! builder.set_sender("no-reply@example.com");
|
||||
//! builder.set_subject("Hello world");
|
||||
//! builder.set_alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.");
|
||||
//! builder.add_reply_to("contact@example.com");
|
||||
//! builder.add_header(("X-Custom-Header", "my header"));
|
||||
//!
|
||||
//! let email = builder.build();
|
||||
//! assert!(email.is_ok());
|
||||
//! ```
|
||||
//!
|
||||
//! See the `EmailBuilder` documentation for a complete list of methods.
|
||||
//!
|
||||
//! ## Sending messages
|
||||
//!
|
||||
//! The following sections describe the available transport methods to handle emails.
|
||||
//!
|
||||
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
|
||||
//! the prefered way of sending emails.
|
||||
//! * The `FileTransport` creates a file containing the email content to be sent. It can be used
|
||||
//! for debugging or if you want to keep all sent emails.
|
||||
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the
|
||||
//! logs.
|
||||
//!
|
||||
//! ### 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
|
||||
//! * 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:
|
||||
//!
|
||||
//! * transport: a low level SMTP client providing all SMTP commands
|
||||
//! * mailer: a high level SMTP client providing an easy method to send emails
|
||||
//! * email: generates the email to be sent with the sender
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ### Simple example
|
||||
//! #### Simple example
|
||||
//!
|
||||
//! This is the most basic example of usage:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! ```rust
|
||||
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//! use lettre::mailer::Mailer;
|
||||
//!
|
||||
//! // Create an email
|
||||
//! let email = EmailBuilder::new()
|
||||
//! // Addresses can be specified by the couple (email, alias)
|
||||
//! .to(("user@example.org", "Firstname Lastname"))
|
||||
//! // ... or by an address only
|
||||
//! .from("user@example.com")
|
||||
//! .subject("Hi, Hello world")
|
||||
//! .body("Hello world.")
|
||||
//! .build().unwrap();
|
||||
//! .to("root@localhost")
|
||||
//! .from("user@localhost")
|
||||
//! .body("Hello World!")
|
||||
//! .subject("Hello")
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! // Open a local connection on port 25
|
||||
//! let mut mailer = Mailer::new(SmtpTransportBuilder::localhost().unwrap().build());
|
||||
//! let mut mailer =
|
||||
//! SmtpTransportBuilder::localhost().unwrap().build();
|
||||
//! // Send the email
|
||||
//! let result = mailer.send(email);
|
||||
//!
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//!
|
||||
//! ### Complete example
|
||||
//! #### Complete example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//! use lettre::transport::smtp::{SecurityLevel, SmtpTransport, SmtpTransportBuilder};
|
||||
//! use lettre::transport::smtp::authentication::Mecanism;
|
||||
//! use lettre::transport::smtp::{SecurityLevel, SmtpTransport,
|
||||
//! SmtpTransportBuilder};
|
||||
//! use lettre::transport::smtp::authentication::Mechanism;
|
||||
//! use lettre::transport::smtp::SUBMISSION_PORT;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//! use lettre::mailer::Mailer;
|
||||
//!
|
||||
//! let mut builder = EmailBuilder::new();
|
||||
//! builder = builder.to(("user@example.org", "Alias name"));
|
||||
//! builder = builder.cc(("user@example.net", "Alias name"));
|
||||
//! builder = builder.from("no-reply@example.com");
|
||||
//! builder = builder.from("no-reply@example.eu");
|
||||
//! builder = builder.sender("no-reply@example.com");
|
||||
//! builder = builder.subject("Hello world");
|
||||
//! builder = builder.body("Hi, Hello world.");
|
||||
//! builder = builder.reply_to("contact@example.com");
|
||||
//! builder = builder.add_header(("X-Custom-Header", "my header"));
|
||||
//!
|
||||
//! let email = builder.build().unwrap();
|
||||
//! let email = EmailBuilder::new()
|
||||
//! .to("root@localhost")
|
||||
//! .from("user@localhost")
|
||||
//! .body("Hello World!")
|
||||
//! .subject("Hello")
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! // Connect to a remote server on a custom port
|
||||
//! let mut mailer = Mailer::new(SmtpTransportBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap()
|
||||
//! let mut mailer = SmtpTransportBuilder::new(("server.tld",
|
||||
//! SUBMISSION_PORT)).unwrap()
|
||||
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
||||
//! .hello_name("my.hostname.tld")
|
||||
//! // Add credentials for authentication
|
||||
@@ -82,12 +142,12 @@
|
||||
//! // 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
|
||||
//! // Enable SMTPUTF8 if the server supports it
|
||||
//! .smtp_utf8(true)
|
||||
//! // Configure accepted authetication mecanisms
|
||||
//! .authentication_mecanisms(vec![Mecanism::CramMd5])
|
||||
//! // Configure expected authentication mechanism
|
||||
//! .authentication_mechanism(Mechanism::CramMd5)
|
||||
//! // Enable connection reuse
|
||||
//! .connection_reuse(true).build());
|
||||
//! .connection_reuse(true).build();
|
||||
//!
|
||||
//! let result_1 = mailer.send(email.clone());
|
||||
//! assert!(result_1.is_ok());
|
||||
@@ -96,43 +156,22 @@
|
||||
//! let result_2 = mailer.send(email);
|
||||
//! assert!(result_2.is_ok());
|
||||
//!
|
||||
//! // Explicitely close the SMTP transaction as we enabled connection reuse
|
||||
//! // Explicitly close the SMTP transaction as we enabled connection reuse
|
||||
//! mailer.close();
|
||||
//! ```
|
||||
//!
|
||||
//! ### Using the client directly
|
||||
//! #### Lower level
|
||||
//!
|
||||
//! If you just want to send an email without using `Email` to provide headers:
|
||||
//! You can also send commands, here is a simple email transaction without
|
||||
//! error handling:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use lettre::email::SimpleSendableEmail;
|
||||
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//! use lettre::mailer::Mailer;
|
||||
//!
|
||||
//! // Create a minimal email
|
||||
//! let email = SimpleSendableEmail::new(
|
||||
//! "test@example.com",
|
||||
//! "test@example.org",
|
||||
//! "Hello world !"
|
||||
//! );
|
||||
//!
|
||||
//! let mut mailer = Mailer::new(SmtpTransportBuilder::localhost().unwrap().build());
|
||||
//! let result = mailer.send(email);
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//!
|
||||
//! ### Lower level
|
||||
//!
|
||||
//! You can also send commands, here is a simple email transaction without error handling:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! ```rust
|
||||
//! use lettre::transport::smtp::SMTP_PORT;
|
||||
//! use lettre::transport::smtp::client::Client;
|
||||
//! use lettre::transport::smtp::client::net::NetworkStream;
|
||||
//!
|
||||
//! let mut email_client: Client<NetworkStream> = Client::new();
|
||||
//! let _ = email_client.connect(&("localhost", SMTP_PORT));
|
||||
//! let _ = email_client.connect(&("localhost", SMTP_PORT), None);
|
||||
//! let _ = email_client.ehlo("my_hostname");
|
||||
//! let _ = email_client.mail("user@example.com", None);
|
||||
//! let _ = email_client.rcpt("user@example.org");
|
||||
@@ -140,12 +179,83 @@
|
||||
//! 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 email = EmailBuilder::new()
|
||||
//! .to("root@localhost")
|
||||
//! .from("user@localhost")
|
||||
//! .body("Hello World!")
|
||||
//! .subject("Hello")
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let mut sender = StubEmailTransport;
|
||||
//! 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, or if you want to keep track of sent messages.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::transport::file::FileEmailTransport;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//! use lettre::email::{EmailBuilder, SendableEmail};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let mut sender = FileEmailTransport::new(temp_dir());
|
||||
//! let email = EmailBuilder::new()
|
||||
//! .to("root@localhost")
|
||||
//! .from("user@localhost")
|
||||
//! .body("Hello World!")
|
||||
//! .subject("Hello")
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let result = sender.send(email);
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//! Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.txt`:
|
||||
//!
|
||||
//! ```text
|
||||
//! b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
|
||||
//! To: <root@localhost>
|
||||
//! From: <user@localhost>
|
||||
//! Subject: Hello
|
||||
//! Date: Sat, 31 Oct 2015 13:42:19 +0100
|
||||
//! Message-ID: <b7c211bc-9811-45ce-8cd9-68eab575d695.lettre@localhost>
|
||||
//!
|
||||
//! Hello World!
|
||||
//! ```
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
#![deny(missing_docs, unsafe_code, unstable_features)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate rustc_serialize as serialize;
|
||||
#[macro_use]
|
||||
extern crate mime;
|
||||
extern crate rustc_serialize;
|
||||
extern crate crypto;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
@@ -155,4 +265,3 @@ extern crate openssl;
|
||||
|
||||
pub mod transport;
|
||||
pub mod email;
|
||||
pub mod mailer;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
//! TODO
|
||||
|
||||
use transport::EmailTransport;
|
||||
use email::SendableEmail;
|
||||
use transport::error::EmailResult;
|
||||
|
||||
/// TODO
|
||||
pub struct Mailer<T: EmailTransport> {
|
||||
transport: T,
|
||||
}
|
||||
|
||||
impl<T: EmailTransport> Mailer<T> {
|
||||
/// TODO
|
||||
pub fn new(transport: T) -> Mailer<T> {
|
||||
Mailer { transport: transport }
|
||||
}
|
||||
|
||||
/// TODO
|
||||
pub fn send<S: SendableEmail>(&mut self, email: S) -> EmailResult {
|
||||
self.transport.send(email.to_addresses(),
|
||||
email.from_address(),
|
||||
email.message(),
|
||||
email.message_id())
|
||||
}
|
||||
|
||||
/// TODO
|
||||
pub fn close(&mut self) {
|
||||
self.transport.close()
|
||||
}
|
||||
}
|
||||
54
src/transport/file/error.rs
Normal file
54
src/transport/file/error.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
|
||||
use self::Error::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Client(_) => "an unknown error occured",
|
||||
Io(_) => "an I/O error occured",
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err as &StdError),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type FileResult = Result<(), Error>;
|
||||
51
src/transport/file/mod.rs
Normal file
51
src/transport/file/mod.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! This transport creates a file for each email, containing the envelope information and the email
|
||||
//! itself.
|
||||
|
||||
use email::SendableEmail;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use transport::EmailTransport;
|
||||
use transport::file::error::FileResult;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Writes the content and the envelope 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<FileResult> for FileEmailTransport {
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> FileResult {
|
||||
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(email.message().clone().as_bytes()));
|
||||
|
||||
info!("{} status=<written>", log_line);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
//! TODO
|
||||
//! Represents an Email transport
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
pub mod error;
|
||||
pub mod file;
|
||||
|
||||
use transport::error::EmailResult;
|
||||
use email::SendableEmail;
|
||||
|
||||
/// Transport method for emails
|
||||
pub trait EmailTransport {
|
||||
pub trait EmailTransport<U> {
|
||||
/// Sends the email
|
||||
fn send(&mut self,
|
||||
to_addresses: Vec<String>,
|
||||
from_address: String,
|
||||
message: String,
|
||||
message_id: String)
|
||||
-> EmailResult;
|
||||
/// Close the transport explicitely
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> U;
|
||||
/// Close the transport explicitly
|
||||
fn close(&mut self);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
//! Provides authentication mecanisms
|
||||
//! Provides authentication mechanisms
|
||||
|
||||
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 crypto::md5::Md5;
|
||||
|
||||
use rustc_serialize::base64::{self, FromBase64, ToBase64};
|
||||
use rustc_serialize::hex::ToHex;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use transport::smtp::NUL;
|
||||
use transport::error::Error;
|
||||
use transport::smtp::error::Error;
|
||||
|
||||
/// Represents authentication mecanisms
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
|
||||
pub enum Mecanism {
|
||||
/// PLAIN authentication mecanism
|
||||
pub enum Mechanism {
|
||||
/// PLAIN authentication mechanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
Plain,
|
||||
/// CRAM-MD5 authentication mecanism
|
||||
/// CRAM-MD5 authentication mechanism
|
||||
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||
CramMd5,
|
||||
}
|
||||
|
||||
impl Display for Mecanism {
|
||||
impl Display for Mechanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"{}",
|
||||
match *self {
|
||||
Mecanism::Plain => "PLAIN",
|
||||
Mecanism::CramMd5 => "CRAM-MD5",
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::CramMd5 => "CRAM-MD5",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Mecanism {
|
||||
/// Does the mecanism supports initial response
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports initial response
|
||||
pub fn supports_initial_response(&self) -> bool {
|
||||
match *self {
|
||||
Mecanism::Plain => true,
|
||||
Mecanism::CramMd5 => false,
|
||||
Mechanism::Plain => true,
|
||||
Mechanism::CramMd5 => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,23 +51,25 @@ impl Mecanism {
|
||||
challenge: Option<&str>)
|
||||
-> Result<String, Error> {
|
||||
match *self {
|
||||
Mecanism::Plain => {
|
||||
Mechanism::Plain => {
|
||||
match challenge {
|
||||
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")),
|
||||
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
|
||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||
None => {
|
||||
Ok(format!("{}{}{}{}", NUL, username, NUL, password)
|
||||
.as_bytes()
|
||||
.to_base64(base64::STANDARD)),
|
||||
.to_base64(base64::STANDARD))
|
||||
}
|
||||
}
|
||||
Mecanism::CramMd5 => {
|
||||
}
|
||||
Mechanism::CramMd5 => {
|
||||
let encoded_challenge = match challenge {
|
||||
Some(challenge) => challenge,
|
||||
None => return Err(Error::ClientError("This mecanism does expect a challenge")),
|
||||
None => return Err(Error::Client("This mechanism does expect a challenge")),
|
||||
};
|
||||
|
||||
let decoded_challenge = match encoded_challenge.from_base64() {
|
||||
Ok(challenge) => challenge,
|
||||
Err(error) => return Err(Error::ChallengeParsingError(error)),
|
||||
Err(error) => return Err(Error::ChallengeParsing(error)),
|
||||
};
|
||||
|
||||
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
||||
@@ -83,27 +85,27 @@ impl Mecanism {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Mecanism;
|
||||
use super::Mechanism;
|
||||
|
||||
#[test]
|
||||
fn test_plain() {
|
||||
let mecanism = Mecanism::Plain;
|
||||
let mechanism = Mechanism::Plain;
|
||||
|
||||
assert_eq!(mecanism.response("username", "password", None).unwrap(),
|
||||
assert_eq!(mechanism.response("username", "password", None).unwrap(),
|
||||
"AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||
assert!(mecanism.response("username", "password", Some("test")).is_err());
|
||||
assert!(mechanism.response("username", "password", Some("test")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cram_md5() {
|
||||
let mecanism = Mecanism::CramMd5;
|
||||
let mechanism = Mechanism::CramMd5;
|
||||
|
||||
assert_eq!(mecanism.response("alice",
|
||||
assert_eq!(mechanism.response("alice",
|
||||
"wonderland",
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
|
||||
.unwrap(),
|
||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||
assert!(mecanism.response("alice", "wonderland", Some("tést")).is_err());
|
||||
assert!(mecanism.response("alice", "wonderland", None).is_err());
|
||||
assert!(mechanism.response("alice", "wonderland", Some("tést")).is_err());
|
||||
assert!(mechanism.response("alice", "wonderland", None).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
//! SMTP client
|
||||
|
||||
use std::string::String;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::io::{BufRead, Read, Write};
|
||||
use std::io;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use bufstream::BufStream;
|
||||
use openssl::ssl::SslContext;
|
||||
|
||||
use transport::smtp::response::ResponseParser;
|
||||
use transport::smtp::authentication::Mecanism;
|
||||
use transport::error::{Error, EmailResult};
|
||||
use transport::smtp::client::net::{Connector, NetworkStream};
|
||||
use std::fmt::Debug;
|
||||
use std::io;
|
||||
use std::io::{BufRead, Read, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::string::String;
|
||||
use transport::smtp::{CRLF, MESSAGE_ENDING};
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
use transport::smtp::client::net::{Connector, NetworkStream};
|
||||
|
||||
use transport::smtp::error::{SmtpResult, Error};
|
||||
use transport::smtp::response::ResponseParser;
|
||||
|
||||
pub mod net;
|
||||
|
||||
@@ -22,7 +22,7 @@ pub mod net;
|
||||
/// Reference : https://tools.ietf.org/html/rfc5321#page-62 (4.5.2. Transparency)
|
||||
#[inline]
|
||||
fn escape_dot(string: &str) -> String {
|
||||
if string.starts_with(".") {
|
||||
if string.starts_with('.') {
|
||||
format!(".{}", string)
|
||||
} else {
|
||||
string.to_string()
|
||||
@@ -57,7 +57,7 @@ macro_rules! return_err (
|
||||
})
|
||||
);
|
||||
|
||||
impl<S: Write + Read = NetworkStream> Client<S> {
|
||||
impl<S: Write + Read> Client<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Client`
|
||||
@@ -66,7 +66,7 @@ impl<S: Write + Read = NetworkStream> Client<S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
impl<S: Connector + Write + Read + Debug> Client<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.quit();
|
||||
@@ -80,16 +80,25 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
|
||||
/// Upgrades the underlying connection to SSL/TLS
|
||||
pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||
//let current_stream = self.stream.clone();
|
||||
if self.stream.is_some() {
|
||||
self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context)
|
||||
} else {
|
||||
Ok(())
|
||||
match self.stream {
|
||||
Some(ref mut stream) => stream.get_mut().upgrade_tls(ssl_context),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.stream {
|
||||
Some(ref stream) => stream.get_ref().is_encrypted(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect<A: ToSocketAddrs>(&mut self, addr: &A) -> EmailResult {
|
||||
pub fn connect<A: ToSocketAddrs>(&mut self,
|
||||
addr: &A,
|
||||
ssl_context: Option<&SslContext>)
|
||||
-> SmtpResult {
|
||||
// Connect should not be called when the client is already connected
|
||||
if self.stream.is_some() {
|
||||
return_err!("The connection is already established", self);
|
||||
@@ -102,8 +111,10 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
None => return_err!("Could not resolve hostname", self),
|
||||
};
|
||||
|
||||
debug!("connecting to {}", server_addr);
|
||||
|
||||
// Try to connect
|
||||
self.set_stream(try!(Connector::connect(&server_addr, None)));
|
||||
self.set_stream(try!(Connector::connect(&server_addr, ssl_context)));
|
||||
|
||||
self.get_reply()
|
||||
}
|
||||
@@ -114,17 +125,17 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn command(&mut self, command: &str) -> EmailResult {
|
||||
pub fn command(&mut self, command: &str) -> SmtpResult {
|
||||
self.send_server(command, CRLF)
|
||||
}
|
||||
|
||||
/// Sends a EHLO command
|
||||
pub fn ehlo(&mut self, hostname: &str) -> EmailResult {
|
||||
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
|
||||
self.command(&format!("EHLO {}", hostname))
|
||||
}
|
||||
|
||||
/// Sends a MAIL command
|
||||
pub fn mail(&mut self, address: &str, options: Option<&str>) -> EmailResult {
|
||||
pub fn mail(&mut self, address: &str, options: Option<&str>) -> SmtpResult {
|
||||
match options {
|
||||
Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)),
|
||||
None => self.command(&format!("MAIL FROM:<{}>", address)),
|
||||
@@ -132,27 +143,27 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
}
|
||||
|
||||
/// Sends a RCPT command
|
||||
pub fn rcpt(&mut self, address: &str) -> EmailResult {
|
||||
pub fn rcpt(&mut self, address: &str) -> SmtpResult {
|
||||
self.command(&format!("RCPT TO:<{}>", address))
|
||||
}
|
||||
|
||||
/// Sends a DATA command
|
||||
pub fn data(&mut self) -> EmailResult {
|
||||
pub fn data(&mut self) -> SmtpResult {
|
||||
self.command("DATA")
|
||||
}
|
||||
|
||||
/// Sends a QUIT command
|
||||
pub fn quit(&mut self) -> EmailResult {
|
||||
pub fn quit(&mut self) -> SmtpResult {
|
||||
self.command("QUIT")
|
||||
}
|
||||
|
||||
/// Sends a NOOP command
|
||||
pub fn noop(&mut self) -> EmailResult {
|
||||
pub fn noop(&mut self) -> SmtpResult {
|
||||
self.command("NOOP")
|
||||
}
|
||||
|
||||
/// Sends a HELP command
|
||||
pub fn help(&mut self, argument: Option<&str>) -> EmailResult {
|
||||
pub fn help(&mut self, argument: Option<&str>) -> SmtpResult {
|
||||
match argument {
|
||||
Some(ref argument) => self.command(&format!("HELP {}", argument)),
|
||||
None => self.command("HELP"),
|
||||
@@ -160,55 +171,55 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
}
|
||||
|
||||
/// Sends a VRFY command
|
||||
pub fn vrfy(&mut self, address: &str) -> EmailResult {
|
||||
pub fn vrfy(&mut self, address: &str) -> SmtpResult {
|
||||
self.command(&format!("VRFY {}", address))
|
||||
}
|
||||
|
||||
/// Sends a EXPN command
|
||||
pub fn expn(&mut self, address: &str) -> EmailResult {
|
||||
pub fn expn(&mut self, address: &str) -> SmtpResult {
|
||||
self.command(&format!("EXPN {}", address))
|
||||
}
|
||||
|
||||
/// Sends a RSET command
|
||||
pub fn rset(&mut self) -> EmailResult {
|
||||
pub fn rset(&mut self) -> SmtpResult {
|
||||
self.command("RSET")
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mecanism
|
||||
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> EmailResult {
|
||||
/// Sends an AUTH command with the given mechanism
|
||||
pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> SmtpResult {
|
||||
|
||||
if mecanism.supports_initial_response() {
|
||||
if mechanism.supports_initial_response() {
|
||||
self.command(&format!("AUTH {} {}",
|
||||
mecanism,
|
||||
try!(mecanism.response(username, password, None))))
|
||||
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")),
|
||||
None => return Err(Error::ResponseParsing("Could not read CRAM challenge")),
|
||||
};
|
||||
|
||||
debug!("CRAM challenge: {}", encoded_challenge);
|
||||
|
||||
let cram_response = try!(mecanism.response(username,
|
||||
let cram_response = try!(mechanism.response(username,
|
||||
password,
|
||||
Some(&encoded_challenge)));
|
||||
|
||||
self.command(&format!("{}", cram_response))
|
||||
self.command(&cram_response.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a STARTTLS command
|
||||
pub fn starttls(&mut self) -> EmailResult {
|
||||
pub fn starttls(&mut self) -> SmtpResult {
|
||||
self.command("STARTTLS")
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message_content: &str) -> EmailResult {
|
||||
pub fn message(&mut self, message_content: &str) -> SmtpResult {
|
||||
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) -> EmailResult {
|
||||
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
|
||||
if self.stream.is_none() {
|
||||
return Err(From::from("Connection closed"));
|
||||
}
|
||||
@@ -222,9 +233,9 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
fn get_reply(&mut self) -> EmailResult {
|
||||
fn get_reply(&mut self) -> SmtpResult {
|
||||
|
||||
let mut parser = ResponseParser::new();
|
||||
let mut parser = ResponseParser::default();
|
||||
|
||||
let mut line = String::new();
|
||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||
@@ -238,16 +249,18 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
|
||||
let response = try!(parser.response());
|
||||
|
||||
match response.is_positive() {
|
||||
true => Ok(response),
|
||||
false => Err(From::from(response)),
|
||||
if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(From::from(response))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[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() {
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
//! A trait to represent a stream
|
||||
|
||||
use std::io;
|
||||
use std::io::{Read, Write, ErrorKind};
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpStream;
|
||||
use std::fmt;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
use openssl::ssl::{SslContext, SslStream};
|
||||
use std::fmt;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::io;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
|
||||
/// A trait for the concept of opening a stream
|
||||
pub trait Connector {
|
||||
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<()>;
|
||||
/// Is the NetworkStream encrypted
|
||||
fn is_encrypted(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Connector for NetworkStream {
|
||||
@@ -22,23 +23,37 @@ impl Connector for NetworkStream {
|
||||
let tcp_stream = try!(TcpStream::connect(addr));
|
||||
|
||||
match ssl_context {
|
||||
Some(context) => match SslStream::new(&context, tcp_stream) {
|
||||
Some(context) => {
|
||||
match SslStream::connect(context, tcp_stream) {
|
||||
Ok(stream) => Ok(NetworkStream::Ssl(stream)),
|
||||
Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
}
|
||||
}
|
||||
None => Ok(NetworkStream::Plain(tcp_stream)),
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||
*self = match self.clone() {
|
||||
NetworkStream::Plain(stream) => match SslStream::new(ssl_context, stream) {
|
||||
|
||||
*self = match *self {
|
||||
NetworkStream::Plain(ref mut stream) => {
|
||||
match SslStream::connect(ssl_context, stream.try_clone().unwrap()) {
|
||||
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
|
||||
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
NetworkStream::Ssl(stream) => NetworkStream::Ssl(stream),
|
||||
}
|
||||
}
|
||||
NetworkStream::Ssl(_) => return Ok(()),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
fn is_encrypted(&self) -> bool {
|
||||
match *self {
|
||||
NetworkStream::Plain(_) => false,
|
||||
NetworkStream::Ssl(_) => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,16 +66,6 @@ pub enum NetworkStream {
|
||||
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(_)")
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use std::error::Error as StdError;
|
||||
use std::io;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
|
||||
use transport::smtp::response::{Severity, Response};
|
||||
use serialize::base64::FromBase64Error;
|
||||
use rustc_serialize::base64::FromBase64Error;
|
||||
use self::Error::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
|
||||
use transport::smtp::response::{Response, Severity};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
@@ -15,21 +15,21 @@ pub enum Error {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
TransientError(Response),
|
||||
Transient(Response),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
PermanentError(Response),
|
||||
Permanent(Response),
|
||||
/// Error parsing a response
|
||||
ResponseParsingError(&'static str),
|
||||
ResponseParsing(&'static str),
|
||||
/// Error parsing a base64 string in response
|
||||
ChallengeParsingError(FromBase64Error),
|
||||
ChallengeParsing(FromBase64Error),
|
||||
/// Internal client error
|
||||
ClientError(&'static str),
|
||||
Client(&'static str),
|
||||
/// DNS resolution error
|
||||
ResolutionError,
|
||||
Resolution,
|
||||
/// IO error
|
||||
IoError(io::Error),
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
@@ -41,19 +41,19 @@ impl Display for Error {
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
TransientError(_) => "a transient error occured during the SMTP transaction",
|
||||
PermanentError(_) => "a permanent error occured during the SMTP transaction",
|
||||
ResponseParsingError(_) => "an error occured while parsing an SMTP response",
|
||||
ChallengeParsingError(_) => "an error occured while parsing a CRAM-MD5 challenge",
|
||||
ResolutionError => "Could no resolve hostname",
|
||||
ClientError(_) => "an unknown error occured",
|
||||
IoError(_) => "an I/O error occured",
|
||||
Transient(_) => "a transient error occured during the SMTP transaction",
|
||||
Permanent(_) => "a permanent error occured during the SMTP transaction",
|
||||
ResponseParsing(_) => "an error occured while parsing an SMTP response",
|
||||
ChallengeParsing(_) => "an error occured while parsing a CRAM-MD5 challenge",
|
||||
Resolution => "could not resolve hostname",
|
||||
Client(_) => "an unknown error occured",
|
||||
Io(_) => "an I/O error occured",
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
match *self {
|
||||
IoError(ref err) => Some(&*err as &StdError),
|
||||
Io(ref err) => Some(&*err as &StdError),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -61,30 +61,25 @@ impl StdError for Error {
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
IoError(err)
|
||||
Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for Error {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.severity() {
|
||||
Severity::TransientNegativeCompletion => TransientError(response),
|
||||
Severity::PermanentNegativeCompletion => PermanentError(response),
|
||||
_ => ClientError("Unknown error code"),
|
||||
Severity::TransientNegativeCompletion => Transient(response),
|
||||
Severity::PermanentNegativeCompletion => Permanent(response),
|
||||
_ => Client("Unknown error code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
ClientError(string)
|
||||
Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type EmailResult = Result<Response, Error>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// TODO
|
||||
}
|
||||
pub type SmtpResult = Result<Response, Error>;
|
||||
@@ -1,13 +1,13 @@
|
||||
//! ESMTP features
|
||||
|
||||
use std::result::Result;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::result::Result;
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
|
||||
use transport::smtp::error::Error;
|
||||
use transport::smtp::response::Response;
|
||||
use transport::error::Error;
|
||||
use transport::smtp::authentication::Mecanism;
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
|
||||
@@ -24,8 +24,8 @@ pub enum Extension {
|
||||
///
|
||||
/// RFC 2487: https://tools.ietf.org/html/rfc2487
|
||||
StartTls,
|
||||
/// AUTH mecanism
|
||||
Authentication(Mecanism),
|
||||
/// AUTH mechanism
|
||||
Authentication(Mechanism),
|
||||
}
|
||||
|
||||
impl Display for Extension {
|
||||
@@ -34,7 +34,7 @@ impl Display for Extension {
|
||||
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
|
||||
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
|
||||
Extension::StartTls => write!(f, "{}", "STARTTLS"),
|
||||
Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism),
|
||||
Extension::Authentication(ref mechanism) => write!(f, "{} {}", "AUTH", mechanism),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ impl 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")),
|
||||
None => return Err(Error::ResponseParsing("Could not read server name")),
|
||||
};
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
@@ -77,7 +77,7 @@ impl ServerInfo {
|
||||
for line in response.message() {
|
||||
|
||||
let splitted: Vec<&str> = line.split_whitespace().collect();
|
||||
let _ = match splitted[0] {
|
||||
match splitted[0] {
|
||||
"8BITMIME" => {
|
||||
features.insert(Extension::EightBitMime);
|
||||
}
|
||||
@@ -88,13 +88,13 @@ impl ServerInfo {
|
||||
features.insert(Extension::StartTls);
|
||||
}
|
||||
"AUTH" => {
|
||||
for &mecanism in &splitted[1..] {
|
||||
match mecanism {
|
||||
for &mechanism in &splitted[1..] {
|
||||
match mechanism {
|
||||
"PLAIN" => {
|
||||
features.insert(Extension::Authentication(Mecanism::Plain));
|
||||
features.insert(Extension::Authentication(Mechanism::Plain));
|
||||
}
|
||||
"CRAM-MD5" => {
|
||||
features.insert(Extension::Authentication(Mecanism::CramMd5));
|
||||
features.insert(Extension::Authentication(Mechanism::CramMd5));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
@@ -116,8 +116,8 @@ impl ServerInfo {
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_auth_mecanism(&self, mecanism: Mecanism) -> bool {
|
||||
self.features.contains(&Extension::Authentication(mecanism))
|
||||
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
|
||||
self.features.contains(&Extension::Authentication(mechanism))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,15 +125,15 @@ impl ServerInfo {
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{ServerInfo, Extension};
|
||||
use transport::smtp::authentication::Mecanism;
|
||||
use transport::smtp::response::{Code, Response, Severity, Category};
|
||||
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(Mecanism::Plain)),
|
||||
assert_eq!(format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string());
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ mod test {
|
||||
"name with no supported features".to_string());
|
||||
|
||||
let mut plain = HashSet::new();
|
||||
assert!(plain.insert(Extension::Authentication(Mecanism::Plain)));
|
||||
assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
@@ -171,12 +171,9 @@ mod test {
|
||||
|
||||
#[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 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));
|
||||
@@ -190,11 +187,10 @@ mod test {
|
||||
|
||||
assert!(server_info.supports_feature(&Extension::EightBitMime));
|
||||
assert!(!server_info.supports_feature(&Extension::StartTls));
|
||||
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
|
||||
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
|
||||
|
||||
let response2 = Response::new(Code::new(Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
1),
|
||||
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(),
|
||||
@@ -202,8 +198,8 @@ mod test {
|
||||
|
||||
let mut features2 = HashSet::new();
|
||||
assert!(features2.insert(Extension::EightBitMime));
|
||||
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
|
||||
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Plain)));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5)));
|
||||
|
||||
let server_info2 = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
@@ -213,8 +209,8 @@ mod test {
|
||||
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_auth_mechanism(Mechanism::Plain));
|
||||
assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
|
||||
assert!(!server_info2.supports_feature(&Extension::StartTls));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,64 @@
|
||||
//! Sends an email using the client
|
||||
|
||||
use std::string::String;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
|
||||
use openssl::ssl::{SslMethod, SslContext};
|
||||
//! This transport sends emails using the SMTP protocol
|
||||
|
||||
use email::SendableEmail;
|
||||
use transport::smtp::extension::{Extension, ServerInfo};
|
||||
use transport::error::{EmailResult, Error};
|
||||
use transport::smtp::client::Client;
|
||||
use transport::smtp::authentication::Mecanism;
|
||||
|
||||
use openssl::ssl::{SslContext, SslMethod};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::string::String;
|
||||
use transport::EmailTransport;
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
use transport::smtp::client::Client;
|
||||
|
||||
use transport::smtp::error::{SmtpResult, Error};
|
||||
use transport::smtp::extension::{Extension, ServerInfo};
|
||||
|
||||
pub mod extension;
|
||||
pub mod authentication;
|
||||
pub mod response;
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
|
||||
// Registrated port numbers:
|
||||
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
// https://www.iana.
|
||||
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
|
||||
/// Default smtp port
|
||||
pub static SMTP_PORT: u16 = 25;
|
||||
pub const SMTP_PORT: u16 = 25;
|
||||
|
||||
/// Default submission port
|
||||
pub static SUBMISSION_PORT: u16 = 587;
|
||||
pub const SUBMISSION_PORT: u16 = 587;
|
||||
|
||||
// Useful strings and characters
|
||||
|
||||
/// The word separator for SMTP transactions
|
||||
pub static SP: &'static str = " ";
|
||||
pub const SP: &'static str = " ";
|
||||
|
||||
/// The line ending for SMTP transactions (carriage return + line feed)
|
||||
pub static CRLF: &'static str = "\r\n";
|
||||
pub const CRLF: &'static str = "\r\n";
|
||||
|
||||
/// Colon
|
||||
pub static COLON: &'static str = ":";
|
||||
pub const COLON: &'static str = ":";
|
||||
|
||||
/// The ending of message content
|
||||
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
|
||||
pub const MESSAGE_ENDING: &'static str = "\r\n.\r\n";
|
||||
|
||||
/// NUL unicode character
|
||||
pub static NUL: &'static str = "\0";
|
||||
pub const NUL: &'static str = "\0";
|
||||
|
||||
/// TLS security level
|
||||
#[derive(Debug)]
|
||||
pub enum SecurityLevel {
|
||||
/// Only send an email on encrypted connection
|
||||
/// Use a TLS wrapped connection
|
||||
///
|
||||
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
|
||||
EncryptedWrapper,
|
||||
/// Only send an email on encrypted connection (with STARTTLS)
|
||||
///
|
||||
/// Recommended mode, prevents MITM when used with verified certificates.
|
||||
AlwaysEncrypt,
|
||||
/// Use TLS when available
|
||||
/// Use TLS when available (with STARTTLS)
|
||||
///
|
||||
/// Default mode.
|
||||
Opportunistic,
|
||||
/// Never use TLS
|
||||
NeverEncrypt,
|
||||
@@ -68,24 +78,25 @@ pub struct SmtpTransportBuilder {
|
||||
credentials: Option<(String, String)>,
|
||||
/// Socket we are connecting to
|
||||
server_addr: SocketAddr,
|
||||
/// SSL contexyt to use
|
||||
/// SSL context to use
|
||||
ssl_context: SslContext,
|
||||
/// TLS security level
|
||||
security_level: SecurityLevel,
|
||||
/// Enable UTF8 mailboxes in enveloppe or headers
|
||||
/// Enable UTF8 mailboxes in envelope or headers
|
||||
smtp_utf8: bool,
|
||||
/// List of authentication mecanism, sorted by priority
|
||||
authentication_mecanisms: Vec<Mecanism>,
|
||||
/// Optional enforced authentication mechanism
|
||||
authentication_mechanism: Option<Mechanism>,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP SmtpTransport
|
||||
/// 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 {
|
||||
Some(addr) => {
|
||||
Ok(SmtpTransportBuilder {
|
||||
server_addr: addr,
|
||||
ssl_context: SslContext::new(SslMethod::Tlsv1).unwrap(),
|
||||
security_level: SecurityLevel::Opportunistic,
|
||||
@@ -94,8 +105,9 @@ impl SmtpTransportBuilder {
|
||||
connection_reuse_count_limit: 100,
|
||||
connection_reuse: false,
|
||||
hello_name: "localhost".to_string(),
|
||||
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
|
||||
}),
|
||||
authentication_mechanism: None,
|
||||
})
|
||||
}
|
||||
None => Err(From::from("Could nor resolve hostname")),
|
||||
}
|
||||
}
|
||||
@@ -111,13 +123,29 @@ impl SmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Require SSL/TLS using STARTTLS
|
||||
/// 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 SMTPS
|
||||
///
|
||||
/// 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
|
||||
@@ -147,15 +175,15 @@ impl SmtpTransportBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mecanisms
|
||||
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SmtpTransportBuilder {
|
||||
self.authentication_mecanisms = mecanisms;
|
||||
/// Set the authentication mechanisms
|
||||
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpTransportBuilder {
|
||||
self.authentication_mechanism = Some(mechanism);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `SmtpTransport`
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
SmtpTransport::new(self)
|
||||
}
|
||||
@@ -201,7 +229,7 @@ macro_rules! try_smtp (
|
||||
impl SmtpTransport {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `SmtpTransport`
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
|
||||
|
||||
let client = Client::new();
|
||||
@@ -229,7 +257,7 @@ impl SmtpTransport {
|
||||
}
|
||||
|
||||
/// Gets the EHLO response and updates server information
|
||||
pub fn get_ehlo(&mut self) -> EmailResult {
|
||||
pub fn get_ehlo(&mut self) -> SmtpResult {
|
||||
// Extended Hello
|
||||
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
|
||||
|
||||
@@ -242,24 +270,29 @@ impl SmtpTransport {
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailTransport for SmtpTransport {
|
||||
impl EmailTransport<SmtpResult> for SmtpTransport {
|
||||
/// Sends an email
|
||||
fn send(&mut self,
|
||||
to_addresses: Vec<String>,
|
||||
from_address: String,
|
||||
message: String,
|
||||
message_id: String)
|
||||
-> EmailResult {
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
|
||||
|
||||
// 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() {
|
||||
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a usable connection, test if the server answers and hello has been sent
|
||||
if self.state.connection_reuse_count == 0 {
|
||||
try!(self.client.connect(&self.client_info.server_addr));
|
||||
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);
|
||||
@@ -268,10 +301,12 @@ impl EmailTransport for SmtpTransport {
|
||||
|
||||
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::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),
|
||||
@@ -289,16 +324,31 @@ impl EmailTransport for SmtpTransport {
|
||||
|
||||
let mut found = false;
|
||||
|
||||
for mecanism in self.client_info.authentication_mecanisms.clone() {
|
||||
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
|
||||
// Compute accepted mechanism
|
||||
let accepted_mechanisms = match self.client_info.authentication_mechanism {
|
||||
Some(mechanism) => vec![mechanism],
|
||||
None => {
|
||||
if self.client.is_encrypted() {
|
||||
// If encrypted, allow all mechanisms, with a preference for the
|
||||
// simplest
|
||||
vec![Mechanism::Plain, Mechanism::CramMd5]
|
||||
} else {
|
||||
// If not encrypted, do not allow clear-text passwords
|
||||
vec![Mechanism::CramMd5]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for mechanism in accepted_mechanisms {
|
||||
if self.server_info.as_ref().unwrap().supports_auth_mechanism(mechanism) {
|
||||
found = true;
|
||||
try_smtp!(self.client.auth(mecanism, &username, &password), self);
|
||||
try_smtp!(self.client.auth(mechanism, &username, &password), self);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
info!("No supported authentication mecanisms available");
|
||||
info!("No supported authentication mechanisms available");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,7 +361,7 @@ impl EmailTransport for SmtpTransport {
|
||||
self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(&Extension::EightBitMime)) {
|
||||
.supports_feature(&Extension::SmtpUtfEight)) {
|
||||
(true, true) => Some("BODY=8BITMIME SMTPUTF8"),
|
||||
(true, false) => Some("BODY=8BITMIME"),
|
||||
(false, _) => None,
|
||||
@@ -323,7 +373,7 @@ impl EmailTransport for SmtpTransport {
|
||||
info!("{}: from=<{}>", message_id, from_address);
|
||||
|
||||
// Recipient
|
||||
for to_address in to_addresses.iter() {
|
||||
for to_address in &to_addresses {
|
||||
try_smtp!(self.client.rcpt(&to_address), self);
|
||||
// Log the rcpt command
|
||||
info!("{}: to=<{}>", message_id, to_address);
|
||||
@@ -337,7 +387,7 @@ impl EmailTransport for SmtpTransport {
|
||||
|
||||
if result.is_ok() {
|
||||
// Increment the connection reuse counter
|
||||
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
|
||||
self.state.connection_reuse_count += 1;
|
||||
|
||||
// Log the message
|
||||
info!("{}: conn_use={}, size={}, status=sent ({})",
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text message
|
||||
//! SMTP response, containing a mandatory return code and an optional text
|
||||
//! message
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::result;
|
||||
use self::Category::*;
|
||||
|
||||
use self::Severity::*;
|
||||
use self::Category::*;
|
||||
use transport::error::{EmailResult, Error};
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::result;
|
||||
use std::str::FromStr;
|
||||
use transport::smtp::error::{Error, SmtpResult};
|
||||
|
||||
|
||||
/// First digit indicates severity
|
||||
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
|
||||
@@ -29,7 +31,7 @@ impl FromStr for Severity {
|
||||
"3" => Ok(PositiveIntermediate),
|
||||
"4" => Ok(TransientNegativeCompletion),
|
||||
"5" => Ok(PermanentNegativeCompletion),
|
||||
_ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
|
||||
_ => Err(Error::ResponseParsing("First digit must be between 2 and 5")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +76,7 @@ impl FromStr for Category {
|
||||
"3" => Ok(Unspecified3),
|
||||
"4" => Ok(Unspecified4),
|
||||
"5" => Ok(MailSystem),
|
||||
_ => Err(Error::ResponseParsingError("Second digit must be between 0 and 5")),
|
||||
_ => Err(Error::ResponseParsing("Second digit must be between 0 and 5")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,15 +116,17 @@ impl FromStr for Code {
|
||||
match (s[0..1].parse::<Severity>(),
|
||||
s[1..2].parse::<Category>(),
|
||||
s[2..3].parse::<u8>()) {
|
||||
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {
|
||||
(Ok(severity), Ok(category), Ok(detail)) => {
|
||||
Ok(Code {
|
||||
severity: severity,
|
||||
category: category,
|
||||
detail: detail,
|
||||
}),
|
||||
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
|
||||
})
|
||||
}
|
||||
_ => Err(Error::ResponseParsing("Could not parse response code")),
|
||||
}
|
||||
} else {
|
||||
Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"))
|
||||
Err(Error::ResponseParsing("Wrong code length (should be 3 digit)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +148,7 @@ impl Code {
|
||||
}
|
||||
|
||||
/// Parses an SMTP response
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
#[derive(PartialEq,Eq,Clone,Debug,Default)]
|
||||
pub struct ResponseParser {
|
||||
/// Response code
|
||||
code: Option<Code>,
|
||||
@@ -154,25 +158,17 @@ pub struct ResponseParser {
|
||||
}
|
||||
|
||||
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)"));
|
||||
return Err(Error::ResponseParsing("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 \
|
||||
return Err(Error::ResponseParsing("Response code has changed during a \
|
||||
reponse"));
|
||||
}
|
||||
}
|
||||
@@ -181,27 +177,25 @@ impl ResponseParser {
|
||||
|
||||
if line.len() > 4 {
|
||||
self.message.push(line[4..].to_string());
|
||||
if line.as_bytes()[3] == '-' as u8 {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
Ok(line.as_bytes()[3] == b'-')
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a response from a `ResponseParser`
|
||||
pub fn response(self) -> EmailResult {
|
||||
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")),
|
||||
None => {
|
||||
Err(Error::ResponseParsing("Incomplete response, could not read response \
|
||||
code"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an SMTP reply, with separed code and message
|
||||
/// Contains an SMTP reply, with separated code and message
|
||||
///
|
||||
/// The text message is optional, only the code is mandatory
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
@@ -263,19 +257,21 @@ impl Response {
|
||||
|
||||
/// 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() {
|
||||
if self.message.is_empty() {
|
||||
None
|
||||
} else {
|
||||
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};
|
||||
use super::{Category, Code, Response, ResponseParser, Severity};
|
||||
|
||||
#[test]
|
||||
fn test_severity_from_str() {
|
||||
@@ -374,7 +370,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_response_parser() {
|
||||
let mut parser = ResponseParser::new();
|
||||
let mut parser = ResponseParser::default();
|
||||
|
||||
assert!(parser.read_line("250-me").unwrap());
|
||||
assert!(parser.read_line("250-8BITMIME").unwrap());
|
||||
|
||||
37
src/transport/stub/error.rs
Normal file
37
src/transport/stub/error.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Client(_) => "an unknown error occured",
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Client(string)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,25 @@
|
||||
//! TODO
|
||||
//! This transport is a stub that only logs the message, and always returns
|
||||
//! success
|
||||
|
||||
use transport::error::EmailResult;
|
||||
use transport::smtp::response::Response;
|
||||
use email::SendableEmail;
|
||||
use transport::EmailTransport;
|
||||
use transport::smtp::response::{Code, Category, Severity};
|
||||
|
||||
/// TODO
|
||||
pub mod error;
|
||||
|
||||
/// This transport does nothing except logging the message envelope
|
||||
pub struct StubEmailTransport;
|
||||
|
||||
impl EmailTransport for StubEmailTransport {
|
||||
fn send(&mut self,
|
||||
to_addresses: Vec<String>,
|
||||
from_address: String,
|
||||
message: String,
|
||||
message_id: String)
|
||||
-> EmailResult {
|
||||
/// SMTP result type
|
||||
pub type StubResult = Result<(), error::Error>;
|
||||
|
||||
let _ = message;
|
||||
info!("message '{}': from '{}' to '{:?}'",
|
||||
message_id,
|
||||
from_address,
|
||||
to_addresses);
|
||||
Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
|
||||
vec!["Ok: email logged".to_string()]))
|
||||
impl EmailTransport<StubResult> for StubEmailTransport {
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> StubResult {
|
||||
|
||||
info!("{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
email.from_address(),
|
||||
email.to_addresses());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 smtp_transport_simple() {
|
||||
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