Compare commits

...

113 Commits

Author SHA1 Message Date
Alexis Mousset
9d61e6ec1f 0.6.3 release 2020-10-21 21:06:20 +02:00
Pyry Kontio
7f8c13910d Bump openssl major version: 0.9 -> 0.10. Leaving the range to continue including 0.9, so this is a patch-level bump. Bump lettre patch version to 0.6.3. 2020-10-21 21:03:03 +02:00
Alexis Mousset
364e40f5d9 Bump to v0.6.2 2017-02-18 18:28:36 +01:00
Alexis Mousset
cb08bc5527 feat(all): Update uuid crate to 0.4 2017-02-18 18:24:25 +01:00
Alexis Mousset
6c7c3ba9fa feat(all): Update env_logger 2017-02-18 18:24:13 +01:00
Zack Mullaly
53e79c0ac4 feat(transport): Upgrade to OpenSSL ^0.9 2017-02-18 18:23:21 +01:00
Alexis Mousset
53f9bada4c chore(all): Bump to v0.6.1 2016-10-19 23:19:32 +02:00
Alexis Mousset
ce7d55ffa8 Merge pull request #96 from amousset/master
docs(all): Add complete documentation information to README
2016-10-19 22:32:07 +02:00
Alexis Mousset
a6ea43a842 Merge branch 'master' into master 2016-10-19 22:24:52 +02:00
Alexis Mousset
eac29768ae docs(all): Add complete documentation information to README 2016-10-19 22:23:43 +02:00
Alexis Mousset
d944aed9d3 Merge pull request #93 from amousset/master
docs(all): Force building tests before coverage computing
2016-10-19 02:33:33 +02:00
Alexis Mousset
67318ac759 docs(all): Force building tests before coverage computing 2016-10-19 02:26:25 +02:00
Alexis Mousset
90999bfc24 Merge pull request #92 from amousset/improve-doc-build
docs(all): Fix token name
2016-10-19 02:05:36 +02:00
Alexis Mousset
7635830399 Merge branch 'master' into improve-doc-build 2016-10-19 01:58:05 +02:00
Alexis Mousset
4fbd06e18b docs(all): Fix token name 2016-10-19 01:56:53 +02:00
Alexis Mousset
5247e2c2aa Merge pull request #91 from amousset/improve-doc-build
docs(all): Build seperate docs for each release
2016-10-19 01:43:20 +02:00
Alexis Mousset
8e21de8de3 docs(all): Build seperate docs for each release 2016-10-19 01:37:51 +02:00
Alexis Mousset
d976f48b7b Merge pull request #86 from ConnyOnny/master
fix(email): address-list for "To", "From" etc.
2016-10-18 00:23:47 +02:00
Constantin Berhard
9ed51a2d3d fix(email): address-list for "To", "From" etc.
Recipients etc. are accumulated by the EmailBuilder and put into the
email as one header with an address-list instead of multiple headers.
This is required by RFC 5322. Also a new test for this behaviour was
added.

fixes #85
2016-10-16 20:32:23 +02:00
Alexis Mousset
ff72bcf5ef Merge pull request #82 from amousset/openssl-0.8
style(all): Fix doc and Cargo.toml openssl line
2016-09-01 00:49:56 +02:00
Alexis Mousset
4d934685a8 Merge branch 'master' into openssl-0.8 2016-09-01 00:40:24 +02:00
Alexis Mousset
a616c0d4c0 style(all): Fix doc and Cargo.toml openssl line 2016-09-01 00:39:04 +02:00
Alexis Mousset
b938e2d757 Merge pull request #81 from amousset/openssl-0.8
feat(transport-smtp): Use rust-openssl 0.8
2016-09-01 00:23:50 +02:00
Alexis Mousset
b4603b4fbc feat(transport-smtp): Use rust-openssl 0.8 2016-09-01 00:15:40 +02:00
Alexis Mousset
495c21b776 Merge pull request #80 from amousset/master
feat(transport): Add an Error and Result type for each transport
2016-08-31 21:43:05 +02:00
Alexis Mousset
bc874fa8f4 feat(transport): Add an Error and Result type for each transport 2016-08-31 21:33:02 +02:00
Alexis Mousset
86c45f13c3 Merge pull request #77 from amousset/master
Improve Email management.
2016-08-02 01:24:24 +02:00
Alexis Mousset
46ae195ba6 Merge branch 'master' into master 2016-08-02 01:15:41 +02:00
Alexis Mousset
75a85831ab feat(email): Improve Email management
Add a new layer called SimpleEmail, useful for some transport (like Web APIs).
2016-08-02 01:14:22 +02:00
Alexis Mousset
95e9f31141 Merge pull request #76 from tshepang/patch-1
docs(smtp): typo fixes
2016-06-27 09:04:11 +02:00
Tshepang Lekhonkhobe
79ac38c84d docs(smtp): typo fixes 2016-06-26 21:46:54 +02:00
Alexis Mousset
e2b4a964e7 Merge pull request #74 from amousset/test-travis
feat(all) Fix doc build
2016-05-26 22:56:25 +02:00
Alexis Mousset
f5f4c026bc Merge branch 'master' into test-travis 2016-05-26 22:46:49 +02:00
Alexis Mousset
efc42f5f22 feat(all) Fix doc build 2016-05-26 22:45:53 +02:00
Alexis Mousset
d201a56571 Merge pull request #73 from amousset/test-travis
feat(all): Test doc upload with travis
2016-05-26 22:26:55 +02:00
Alexis Mousset
f57279bdb7 feat(all): Test doc upload with travis 2016-05-26 21:07:43 +02:00
Alexis Mousset
d967bbb126 Merge pull request #71 from amousset/master
feat(transport-smtp): Add a debug log for name resolution
2016-05-23 22:35:51 +02:00
Alexis Mousset
2553b32196 feat(transport-smtp): Add a debug log for name resolution 2016-05-23 22:27:13 +02:00
Alexis Mousset
3f6fc56b3e Merge pull request #70 from amousset/use-clippy
style(all): improve the code thanks to rust-clippy
2016-05-17 01:01:53 +02:00
Alexis Mousset
97da0c0869 style(all): improve the code thanks to rust-clippy 2016-05-17 00:54:11 +02:00
Alexis Mousset
0acc57d36d Merge pull request #69 from amousset/rustfmt-2
style(all): run rustfmt 0.5
2016-05-14 12:25:13 +02:00
Alexis Mousset
d1bef702d6 style(all): run rustfmt 0.5 2016-05-14 12:17:09 +02:00
Alexis Mousset
d20d4f038b Merge pull request #67 from amousset/single-authentication-mechanism
feat(transport-smtp): Change default authentication mecanism default handling
2016-05-13 10:38:13 +02:00
Alexis Mousset
b7039a7a69 feat(transport-smtp): Change default authentication mecanism default handling
Change the default authentication mechanism selection check if the
connection is encrypted, and only test PLAIN when it is the case.
Also make the .authentication_mechnaism only take one mechanism, as
a user will specify it he wants to ensure one particular method will
be used.

Closes #65
2016-05-10 00:12:39 +02:00
Alexis Mousset
8fda23435c Merge pull request #64 from craftytrickster/improvements
Improvements
2016-05-07 08:02:34 +02:00
David Raifaizen
4f4a1436ae refactor(client): Replacing manual unwrap with match statement 2016-05-06 22:56:49 -04:00
David Raifaizen
a8324795e5 style(all): Minor typo/grammar fixes 2016-05-06 22:56:33 -04:00
Alexis Mousset
f347dcc97b Merge pull request #63 from amousset/add-changelog
fix(email): Add required MIME-Version header
2016-05-06 08:59:03 +02:00
Alexis Mousset
df69c8b7c2 Merge branch 'master' into add-changelog 2016-05-06 08:51:06 +02:00
Alexis Mousset
fd20e90bb5 fix(email): Add required MIME-Version header
Add a MIME-Version header to all messages.
2016-05-06 00:33:31 +02:00
Alexis Mousset
1da35ec17f Merge pull request #61 from amousset/add-changelog
docs(all): Add a changelog file
2016-05-05 23:54:46 +02:00
Alexis Mousset
69f29cfb45 Merge branch 'master' into add-changelog 2016-05-05 23:41:12 +02:00
Alexis Mousset
e047aedee1 docs(all): Add a changelog file
Add a CHANGELOG file starting from 0.6.
2016-05-05 23:27:34 +02:00
Alexis Mousset
d14236be4d Merge pull request #60 from amousset/fix-contributing
docs(all): Fix contributing doc formatting
2016-05-05 22:28:45 +02:00
Alexis Mousset
c814300f96 docs(all): Fix contributing doc formatting
Fix the formatting of the titles.
2016-05-05 22:21:45 +02:00
Alexis Mousset
3171dec6e9 Merge pull request #59 from amousset/contributing-doc
docs(all): Add a contributing documentation
2016-05-05 21:40:35 +02:00
Alexis Mousset
e9b658d868 docs(all): Add a contributing documentation
Adds a CONTRIBUTING.md describing the commit format.
Commit message formatting is now enforced by Gitcop.
2016-05-05 21:30:09 +02:00
Alexis Mousset
72b8bc3866 Merge pull request #57 from amousset/fix-nightly
Add documentation for multipart messages
2016-05-05 19:22:41 +02:00
Alexis Mousset
f3742adc9f Add documentation for multipart messages 2016-05-05 19:22:07 +02:00
Alexis Mousset
c5fec283f7 Merge pull request #56 from amousset/fix-nightly
Improve multipart support
2016-05-05 16:34:35 +02:00
Alexis Mousset
2d9ad22102 Improve multipart support 2016-05-05 16:32:42 +02:00
Alexis Mousset
d0eab7a09f Merge pull request #55 from amousset/fix-nightly
Do not use default type parameter in impl
2016-05-04 00:03:01 +02:00
Alexis Mousset
4f6e6185fc Do not use default type parameter in impl 2016-05-04 00:01:13 +02:00
Alexis Mousset
256624f3d8 Merge pull request #54 from amousset/multipart
Add initial multipart support
2016-05-03 23:55:17 +02:00
Alexis Mousset
74de004e6c Add initial multipart support 2016-05-03 23:51:30 +02:00
Alexis Mousset
b7d81016e1 Merge pull request #53 from amousset/update-rependencies
Update uuid crate from 0.1 to 0.2
2016-05-01 19:10:52 +02:00
Alexis Mousset
6bd2b364ec Update uuid crate from 0.1 to 0.2 2016-05-01 19:09:59 +02:00
Alexis Mousset
bc764aec5a Merge pull request #52 from amousset/improve-doc-4
Doc formatting
2016-03-27 22:47:15 +02:00
Alexis Mousset
d18eec4d1b Doc formatting 2016-03-27 21:40:20 +02:00
Alexis Mousset
ec68ca2ca8 Merge pull request #51 from amousset/improve-doc-4
Improve doc
2016-03-27 21:38:35 +02:00
Alexis Mousset
c9076fef63 Improve doc 2016-03-27 21:37:33 +02:00
Alexis Mousset
c770dc6205 Merge pull request #50 from amousset/improve-doc-4
Improve doc
2016-03-27 21:07:08 +02:00
Alexis Mousset
da8c733939 Improve doc 2016-03-27 21:06:18 +02:00
Alexis Mousset
c21bdaff43 Merge pull request #47 from amousset/small-fixes
Little refactoring
2016-03-25 22:23:57 +01:00
Alexis Mousset
dab8b111d3 Little refactoring 2016-03-25 21:54:15 +01:00
Alexis Mousset
521681c0f7 Merge pull request #46 from amousset/appveyor
Add an AppVeyor status
2016-03-20 02:10:59 +01:00
Alexis Mousset
2e3f82b98a Add an AppVeyor status 2016-03-20 02:10:25 +01:00
Alexis Mousset
2816e8ee73 Merge pull request #45 from amousset/appveyor
Update appveyor file
2016-03-20 01:55:15 +01:00
Alexis Mousset
4441be6b7b Update appveyor file 2016-03-20 01:54:16 +01:00
Alexis Mousset
b992ca9694 Merge pull request #44 from amousset/appveyor
Update appveyor file
2016-03-20 01:49:42 +01:00
Alexis Mousset
d7e100692b Update appveyor file 2016-03-20 01:48:11 +01:00
Alexis Mousset
2c979a6fbd Merge pull request #43 from amousset/update
rustfmt pass
2016-03-20 01:35:31 +01:00
Alexis Mousset
f8c883f58e rustfmt pass 2016-03-20 01:27:58 +01:00
Alexis Mousset
af20cfa8ff Merge pull request #41 from amousset/master
Improve doc
2016-01-16 12:22:40 +01:00
Alexis Mousset
7a9f9111a5 Improve doc 2016-01-16 12:13:06 +01:00
Alexis Mousset
89c0be219d Merge pull request #39 from lorenz/patch-3
Allow multiple To addresses in SimpleSendableMail
2015-12-18 22:57:31 +01:00
Lorenz Brun
6ee7fdb3d1 Allow multiple To addresses in SimpleSendableMail
Because the struct internally stores a `Vec<String>` it would be nice to be able to construct messages with multiple To addresses.
This is in its current form a breaking API change, so feel free to change the way it's implemented.
2015-12-05 00:10:24 +01:00
Alexis Mousset
b7ac3a897f Merge pull request #38 from cloudvlts/master
Update 'openssl' to 0.7
2015-11-30 10:47:23 +01:00
Darius Clark
c436716277 Update 'openssl' to 0.7 2015-11-29 20:51:49 -05:00
Alexis Mousset
eabdb960b0 Merge pull request #37 from amousset/master
Fix doc build
2015-11-07 08:47:13 +01:00
Alexis Mousset
59ba9e84dc Fix doc build 2015-11-07 08:46:51 +01:00
Alexis Mousset
e569c030bc Merge pull request #36 from amousset/master
Fix doc build
2015-11-07 08:40:35 +01:00
Alexis Mousset
72aea756fa Fix doc build 2015-11-07 08:40:02 +01:00
Alexis Mousset
655ae6d2ff Update .travis.yml 2015-11-03 22:20:33 +01:00
Alexis Mousset
150536d242 Update .travis.yml 2015-11-03 22:00:17 +01:00
Alexis Mousset
7d707fab25 Update .travis.yml 2015-11-03 21:49:03 +01:00
Alexis Mousset
4ec34987f8 Merge pull request #35 from amousset/improve-doc
Improve documentation
2015-11-03 21:48:02 +01:00
Alexis Mousset
7f3680f125 Improve documentation 2015-11-03 21:47:06 +01:00
Alexis Mousset
67566c2152 Merge pull request #34 from amousset/improve-doc
Improve documentation
2015-11-03 21:22:26 +01:00
Alexis Mousset
d863a7677e Improve documentation 2015-11-03 21:18:18 +01:00
Alexis Mousset
c1fe40479b Improve documentation 2015-11-03 21:08:56 +01:00
Alexis Mousset
8d03545062 Merge pull request #32 from amousset/improve-doc
Improve documentation
2015-10-31 21:41:47 +01:00
Alexis Mousset
489a6e892e Improve documentation 2015-10-31 21:23:06 +01:00
Alexis Mousset
b3fe1e0f65 Merge pull request #31 from amousset/fix-benchmarks
Fix benchmarks
2015-10-30 19:15:32 +01:00
Alexis Mousset
3612ffca7a Fix becnhmarks 2015-10-30 19:08:11 +01:00
Alexis Mousset
7940ad6c15 Merge pull request #30 from amousset/comply-with-rfc1214
Comply with RFC1214
2015-10-30 17:29:01 +01:00
Alexis Mousset
5ffb169bc9 Comply with RFC1214 2015-10-29 23:26:20 +01:00
Alexis Mousset
ea0bb256cd Merge pull request #29 from amousset/add-smtps-support
feature(smtp): Add SMTPS support
2015-10-27 22:47:16 +01:00
Alexis Mousset
9f177047f8 Add SMTPS support 2015-10-27 22:38:45 +01:00
Alexis Mousset
48eb859804 Merge pull request #26 from norcalli/master
Renames and fixes typo from mecanism to mechanism
2015-10-27 14:34:47 +01:00
Ashkan Kiani
8d9877233d Renames and fixes typo from mecanism to mechanism 2015-10-27 06:20:04 -07:00
Alexis Mousset
09f61a9fc9 Merge pull request #24 from amousset/rustfmt
style(formatting): Run rustfmt
2015-10-26 22:58:09 +01:00
Alexis Mousset
40e749a04a style(formatting): Run rustfmt 2015-10-26 22:51:07 +01:00
28 changed files with 1474 additions and 579 deletions

View File

@@ -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: x86_64-pc-windows-gnu
- TARGET: i686-pc-windows-gnu
BITS: 32
# - TARGET: x86_64-pc-windows-msvc
# BITS: 64
install:
- ps: Start-FileDownload 'http://slproweb.com/download/Win64OpenSSL-1_0_2d.exe'
- ps: Start-Process "Win64OpenSSL-1_0_2d.exe" -ArgumentList "/silent /verysilent /sp- /suppressmsgboxes" -Wait
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-nightly-${env:TARGET}.exe" -FileName "rust-nightly.exe"
- ps: .\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:
- env OPENSSL_LIB_DIR=C:/OpenSSL-Win64 OPENSSL_INCLUDE_DIR=C:/OpenSSL-Win64/include cargo build --verbose
- cargo build --verbose

View File

@@ -1,9 +1,13 @@
language: rust
rust:
- stable
- beta
- nightly
- stable
- beta
- nightly
matrix:
allow_failures:
- rust: nightly
sudo: false
@@ -16,7 +20,9 @@ cache:
- target/release/deps
- target/release/build
install: pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH
install:
- pip install 'travis-cargo<0.2' --user
- export PATH=$HOME/.local/bin:$PATH
addons:
apt:
@@ -27,17 +33,17 @@ addons:
- libdw-dev
before_script:
- smtp-sink 2525 1000&
- 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 nightly bench
- travis-cargo --only stable doc-upload
- travis-cargo --only stable coveralls --no-sudo
- ./.travis/doc.sh
- ./.travis/coverage.sh
- travis-cargo --only nightly bench
env:
global:

20
.travis/coverage.sh Executable file
View 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
View 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

43
CHANGELOG.md Normal file
View File

@@ -0,0 +1,43 @@
### v0.6.3 (2022-1--21)
* **transport**: Allow using openssl 0.10
### v0.6.2 (2017-02-18)
#### Features
* **all**
* Update uuid crate to 0.4
* Update env-logger crate to 0.4
* Update openssl crate to 0.9
### 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
View 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.

View File

@@ -1,27 +1,28 @@
[package]
name = "lettre"
version = "0.5.0"
version = "0.6.3"
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"
bufstream = "^0.1"
email = "^0.0"
log = "^0.3"
mime = "^0.2"
openssl = "> 0.9, < 0.11"
rustc-serialize = "^0.3"
rust-crypto = "^0.2"
time = "^0.1"
uuid = { version = "^0.4", features = ["v4"] }
[dev-dependencies]
env_logger = "0.3"
env_logger = "^0.4"
[features]
unstable = []

View File

@@ -1,27 +1,52 @@
lettre [![Build Status](https://travis-ci.org/lettre/lettre.svg?branch=master)](https://travis-ci.org/lettre/lettre) [![Coverage Status](https://coveralls.io/repos/lettre/lettre/badge.svg?branch=master&service=github)](https://coveralls.io/github/lettre/lettre?branch=master) [![Crate](https://meritbadge.herokuapp.com/lettre)](https://crates.io/crates/lettre) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
=========
# lettre
[![Build Status](https://travis-ci.org/lettre/lettre.svg?branch=master)](https://travis-ci.org/lettre/lettre)
[![Build status](https://ci.appveyor.com/api/projects/status/mpwglemugjtkps2d/branch/master?svg=true)](https://ci.appveyor.com/project/amousset/lettre/branch/master)
[![Coverage Status](https://coveralls.io/repos/github/lettre/lettre/badge.svg?branch=master)](https://coveralls.io/github/lettre/lettre?branch=master)
[![Crate](https://img.shields.io/crates/v/lettre.svg)](https://crates.io/crates/lettre)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Gitter](https://badges.gitter.im/lettre/lettre.svg)](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.3](https://lettre.github.io/lettre/v0.6.3/lettre/)
* [v0.6.2](https://lettre.github.io/lettre/v0.6.2/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.5"
lettre = "0.6"
```
Testing
-------
## Testing
The tests require a mail server listening locally on port 25.
The tests require an open mail server listening locally on port 25.
License
-------
## License
This program is distributed under the terms of the MIT license.
See LICENSE for details.
See [LICENSE](./LICENSE) for details.

View File

@@ -5,13 +5,11 @@ extern crate test;
use lettre::transport::smtp::SmtpTransportBuilder;
use lettre::transport::EmailTransport;
use lettre::mailer::Mailer;
use lettre::email::EmailBuilder;
#[bench]
fn bench_simple_send(b: &mut test::Bencher) {
let sender = SmtpTransportBuilder::new("127.0.0.1:2525").unwrap().build();
let mut mailer = Mailer::new(sender);
let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525").unwrap().build();
b.iter(|| {
let email = EmailBuilder::new()
.to("root@localhost")
@@ -20,18 +18,17 @@ fn bench_simple_send(b: &mut test::Bencher) {
.subject("Hello")
.build()
.unwrap();
let result = mailer.send(email);
let result = sender.send(email);
assert!(result.is_ok());
});
}
#[bench]
fn bench_reuse_send(b: &mut test::Bencher) {
let sender = SmtpTransportBuilder::new("127.0.0.1:2525")
let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525")
.unwrap()
.connection_reuse(true)
.build();
let mut mailer = Mailer::new(sender);
b.iter(|| {
let email = EmailBuilder::new()
.to("root@localhost")
@@ -40,8 +37,8 @@ fn bench_reuse_send(b: &mut test::Bencher) {
.subject("Hello")
.build()
.unwrap();
let result = mailer.send(email);
let result = sender.send(email);
assert!(result.is_ok());
});
mailer.close()
sender.close()
}

View File

@@ -1,20 +0,0 @@
extern crate lettre;
use lettre::transport::file::FileEmailTransport;
use lettre::transport::EmailTransport;
use lettre::email::EmailBuilder;
fn main() {
let mut sender = FileEmailTransport::new("/tmp/");
let email = EmailBuilder::new()
.to("root@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build()
.unwrap();
let result = sender.send(email);
println!("{:?}", result);
assert!(result.is_ok());
}

1
rustfmt.toml Normal file
View File

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

31
src/email/error.rs Normal file
View 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",
}
}
}

View File

@@ -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();

View File

@@ -1,6 +1,78 @@
//! # 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
//! ## 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
@@ -12,21 +84,11 @@
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and
//! CRAM-MD5 mecanisms
//! 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:
//!
@@ -35,15 +97,13 @@
//! use lettre::email::EmailBuilder;
//! use lettre::transport::EmailTransport;
//!
//! // 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 =
@@ -54,28 +114,23 @@
//! 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::authentication::Mechanism;
//! use lettre::transport::smtp::SUBMISSION_PORT;
//! use lettre::transport::EmailTransport;
//!
//! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name"));
//! 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 = SmtpTransportBuilder::new(("server.tld",
@@ -87,10 +142,10 @@
//! // 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();
//!
@@ -101,33 +156,11 @@
//! 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
//!
//! If you just want to send an email without using `Email` to provide headers:
//!
//! ```rust
//! use lettre::email::SimpleSendableEmail;
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
//! use lettre::transport::EmailTransport;
//!
//! // Create a minimal email
//! let email = SimpleSendableEmail::new(
//! "test@example.com",
//! "test@example.org",
//! "Hello world !"
//! );
//!
//! let mut mailer =
//! SmtpTransportBuilder::localhost().unwrap().build();
//! let result = mailer.send(email);
//! assert!(result.is_ok());
//! ```
//!
//! ### Lower level
//! #### Lower level
//!
//! You can also send commands, here is a simple email transaction without
//! error handling:
@@ -138,7 +171,7 @@
//! 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");
@@ -146,11 +179,82 @@
//! 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;
#[macro_use]
extern crate mime;
extern crate rustc_serialize;
extern crate crypto;
extern crate time;

View 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>;

View File

@@ -1,17 +1,17 @@
//! TODO
//! This transport creates a file for each email, containing the envelope information and the email
//! itself.
use std::path::{Path, PathBuf};
use std::io::prelude::*;
use std::fs::File;
use transport::error::EmailResult;
use transport::smtp::response::Response;
use transport::EmailTransport;
use transport::smtp::response::{Code, Category, Severity};
use email::SendableEmail;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use transport::EmailTransport;
use transport::file::error::FileResult;
/// TODO
pub mod error;
/// Writes the content and the envelope information to a file
pub struct FileEmailTransport {
path: PathBuf,
}
@@ -25,8 +25,8 @@ impl FileEmailTransport {
}
}
impl EmailTransport for FileEmailTransport {
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
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()));
@@ -38,13 +38,11 @@ impl EmailTransport for FileEmailTransport {
email.to_addresses().join("> to=<"));
try!(f.write_all(log_line.as_bytes()));
try!(f.write_all(format!("{}", email.message()).as_bytes()));
try!(f.write_all(email.message().clone().as_bytes()));
info!("{} status=<written>", log_line);
Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
vec![format!("Ok: email written to {}",
file.to_str().unwrap_or("non-UTF-8 path"))]))
Ok(())
}
fn close(&mut self) {

View File

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

View File

@@ -1,45 +1,45 @@
//! Provides authentication mecanisms
//! Provides authentication mechanisms
use std::fmt::{Display, Formatter};
use std::fmt;
use rustc_serialize::base64::{self, ToBase64, FromBase64};
use rustc_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());
}
}

View File

@@ -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,15 +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<()> {
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);
@@ -101,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()
}
@@ -113,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)),
@@ -131,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"),
@@ -159,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"));
}
@@ -221,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));
@@ -237,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() {

View File

@@ -1,20 +1,20 @@
//! A trait to represent a stream
use std::io;
use std::io::{Read, Write, ErrorKind};
use std::net::SocketAddr;
use std::net::TcpStream;
use openssl::ssl::{Ssl, SslContext, SslStream};
use std::fmt;
use std::fmt::{Debug, Formatter};
use openssl::ssl::{SslContext, SslStream};
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 +22,46 @@ impl Connector for NetworkStream {
let tcp_stream = try!(TcpStream::connect(addr));
match ssl_context {
Some(context) => match SslStream::new(&context, tcp_stream) {
Ok(stream) => Ok(NetworkStream::Ssl(stream)),
Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
},
Some(context) => {
match Ssl::new(context) {
Ok(ssl) => {
ssl.connect(tcp_stream)
.map(|s| NetworkStream::Ssl(s))
.map_err(|e| io::Error::new(ErrorKind::Other, e))
}
Err(e) => Err(io::Error::new(ErrorKind::Other, e)),
}
}
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 Ssl::new(ssl_context) {
Ok(ssl) => {
match ssl.connect(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),
}
}
Err(e) => return Err(io::Error::new(ErrorKind::Other, e)),
}
}
NetworkStream::Ssl(_) => return Ok(()),
};
Ok(())
}
fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Plain(_) => false,
NetworkStream::Ssl(_) => true,
}
}
}
@@ -51,16 +74,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(_)")

View File

@@ -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 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,25 +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>;
pub type SmtpResult = Result<Response, Error>;

View File

@@ -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));
}
}

View File

@@ -1,55 +1,64 @@
//! Sends an email using the client
//! This transport sends emails using the SMTP protocol
use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use openssl::ssl::{SslMethod, SslContext};
use transport::smtp::extension::{Extension, ServerInfo};
use transport::error::{EmailResult, Error};
use transport::smtp::client::Client;
use transport::smtp::authentication::Mecanism;
use transport::EmailTransport;
use email::SendableEmail;
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
/// 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,
@@ -69,34 +78,36 @@ 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(),
ssl_context: SslContext::builder(SslMethod::tls()).unwrap().build(),
security_level: SecurityLevel::Opportunistic,
smtp_utf8: false,
credentials: None,
connection_reuse_count_limit: 100,
connection_reuse: false,
hello_name: "localhost".to_string(),
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
}),
authentication_mechanism: None,
})
}
None => Err(From::from("Could nor resolve hostname")),
}
}
@@ -112,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
@@ -148,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)
}
@@ -202,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();
@@ -230,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);
@@ -243,9 +270,9 @@ impl SmtpTransport {
}
}
impl EmailTransport for SmtpTransport {
impl EmailTransport<SmtpResult> for SmtpTransport {
/// Sends an email
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
// Extract email information
let message_id = email.message_id();
@@ -254,16 +281,18 @@ impl EmailTransport for SmtpTransport {
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);
@@ -272,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),
@@ -293,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");
}
}
}
@@ -327,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);
@@ -341,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 ({})",

View File

@@ -1,13 +1,14 @@
//! 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)]
@@ -30,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")),
}
}
}
@@ -75,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")),
}
}
}
@@ -115,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)"))
}
}
}
@@ -145,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>,
@@ -155,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"));
}
}
@@ -182,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)]
@@ -264,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() {
@@ -375,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());

View 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)
}
}

View File

@@ -1,24 +1,25 @@
//! This transport is a stub that only logs the message, and always returns
//! succes
//! success
use transport::error::EmailResult;
use transport::smtp::response::Response;
use transport::EmailTransport;
use transport::smtp::response::{Code, Category, Severity};
use email::SendableEmail;
use transport::EmailTransport;
/// This transport does nothing exept logging the message enveloppe
pub mod error;
/// This transport does nothing except logging the message envelope
pub struct StubEmailTransport;
impl EmailTransport for StubEmailTransport {
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
/// SMTP result type
pub type StubResult = Result<(), error::Error>;
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(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
vec!["Ok: email logged".to_string()]))
Ok(())
}
fn close(&mut self) {

View File

@@ -7,7 +7,7 @@ use std::io::Read;
use lettre::transport::file::FileEmailTransport;
use lettre::transport::EmailTransport;
use lettre::email::{SendableEmail, EmailBuilder};
use lettre::email::{EmailBuilder, SendableEmail};
#[test]
fn file_transport() {
@@ -28,7 +28,10 @@ fn file_transport() {
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()));
assert_eq!(buffer,
format!("{}: from=<user@localhost> to=<root@localhost>\n{}",
message_id,
email.message()));
remove_file(file).unwrap();
}

View File

@@ -5,7 +5,7 @@ use lettre::transport::EmailTransport;
use lettre::email::EmailBuilder;
#[test]
fn simple_sender() {
fn smtp_transport_simple() {
let mut sender = SmtpTransportBuilder::localhost().unwrap().build();
let email = EmailBuilder::new()
.to("root@localhost")