Compare commits

...

145 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
Alexis Mousset
4efb560bc8 Merge pull request #23 from lettre/update-readme
docs(readme): Update readme for 0.5.0
2015-10-26 21:14:24 +01:00
Alexis Mousset
500c4fb39d Update readme for 0.5.0 2015-10-26 21:05:18 +01:00
Alexis Mousset
d488910010 Merge pull request #22 from lorenz/patch-2
Release version 0.5.0
2015-10-26 21:01:33 +01:00
Lorenz Brun
4155e44dbd Release new version
I'd like to use the new features you introduced in the last few days, especially `transport.send(mail)`. Also the docs are currently built for the `master` branch which means they currently show features not yet available in the Crate.
2015-10-26 20:30:57 +01:00
Alexis Mousset
401118ee68 Improved test for File transport 2015-10-26 00:17:46 +01:00
Alexis Mousset
e6dd9d5a46 Install complete openssl on windows 2015-10-25 23:40:59 +01:00
Alexis Mousset
c8187c4a7c Install packages for kcov 2015-10-25 17:59:56 +01:00
Alexis Mousset
8f211c88a8 Add OpenSSL path to windows build 2015-10-25 17:50:20 +01:00
Alexis Mousset
62df24c5b1 Add file transport 2015-10-25 17:47:07 +01:00
Alexis Mousset
7ac43b73c3 Remove Mailer struct 2015-10-25 15:29:40 +01:00
Alexis Mousset
3c91c065d6 Split integration tests 2015-10-25 12:52:56 +01:00
Alexis Mousset
9e30e7185e Use travis-cargo for benchmarks 2015-10-22 23:52:30 +02:00
Alexis Mousset
4da9e16bfc Add cache for release (used in benchmarks) 2015-10-22 23:51:01 +02:00
Alexis Mousset
2977eb0509 Travis build without sudo 2015-10-22 23:45:20 +02:00
Alexis Mousset
2884da8f90 Use smtp-sink for benchmarks 2015-10-22 23:37:04 +02:00
Alexis Mousset
31a7504d54 Add becnhmarks 2015-10-22 23:34:04 +02:00
Alexis Mousset
9a93feea96 Fix SMTPUTF8 test 2015-10-22 21:51:29 +02:00
Alexis Mousset
f102f321d3 Tests email with travis 2015-10-22 21:45:19 +02:00
Alexis Mousset
1ba47e473c Tests email with travis 2015-10-22 21:41:09 +02:00
Alexis Mousset
3acf21a316 Tests email with travis 2015-10-22 21:32:48 +02:00
Alexis Mousset
544894def9 Add integration testing on travis 2015-10-22 19:30:36 +02:00
Alexis Mousset
f74fb4f89c Add openssl to AppVeyor env 2015-10-22 18:32:00 +02:00
Alexis Mousset
085998c730 Add mingw to AppVeyor 2015-10-22 18:07:20 +02:00
Alexis Mousset
d3d7c4b44e Formatting with rustfmt 2015-10-22 16:53:13 +02:00
Alexis Mousset
88b9cfb847 Fix documentation link 2015-10-21 23:16:09 +02:00
Alexis Mousset
250ed7bcf4 Add SMTPUTF8 support 2015-10-21 23:02:14 +02:00
Alexis Mousset
63d9216df3 Change crate name 2015-10-15 00:03:07 +02:00
Alexis Mousset
54758ebde9 Rename rust-smtp to lettre, add multiple transports support 2015-10-14 23:44:25 +02:00
Alexis Mousset
bd67d80d3e Try to authenticate only once 2015-10-12 23:46:12 +02:00
Alexis Mousset
a91db14869 Configurable TLS security level 2015-10-12 22:47:53 +02:00
Alexis Mousset
b5c6663629 Update AppVeyor configuration 2015-10-12 02:50:53 +02:00
Alexis Mousset
4b4150ed99 Version 0.3.0 2015-10-12 02:20:39 +02:00
33 changed files with 2322 additions and 1180 deletions

View File

@@ -1,23 +1,24 @@
environment:
CARGO_TARGET: x86_64-pc-windows-gnu
matrix:
- TARGET: x86_64-pc-windows-msvc
- TARGET: i686-pc-windows-gnu
OPENSSL_INCLUDE_DIR: C:\OpenSSL\include
OPENSSL_LIB_DIR: C:\OpenSSL\lib
OPENSSL_LIBS: ssleay32:libeay32
matrix:
- TARGET: i686-pc-windows-gnu
BITS: 32
# - TARGET: x86_64-pc-windows-msvc
# BITS: 64
install:
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rustc-nightly-${env:TARGET}.tar.gz"
- ps: Start-FileDownload "https://static.rust-lang.org/cargo-dist/cargo-nightly-${env:CARGO_TARGET}.tar.gz"
- 7z x rustc-nightly-%TARGET%.tar.gz > nul
- 7z x rustc-nightly-%TARGET%.tar > nul
- 7z x cargo-nightly-%CARGO_TARGET%.tar.gz > nul
- 7z x cargo-nightly-%CARGO_TARGET%.tar > nul
- call "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" amd64
- set PATH=%PATH%;%cd%/rustc-nightly-%TARGET%/rustc/bin
- set PATH=%PATH%;%cd%/cargo-nightly-%CARGO_TARGET%/cargo/bin
- SET PATH=%PATH%;C:\MinGW\bin
- rustc -V
- cargo -V
- ps: Start-FileDownload "http://slproweb.com/download/Win${env:BITS}OpenSSL-1_0_2g.exe"
- Win%BITS%OpenSSL-1_0_2g.exe /SILENT /VERYSILENT /SP- /DIR="C:\OpenSSL"
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-beta-${env:TARGET}.exe"
- rust-beta-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
- SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
- SET PATH=%PATH%;C:\MinGW\bin
- rustc -V
- cargo -V
build: false
test_script:
- cargo test --verbose --no-default-features
- cargo build --verbose

View File

@@ -1,20 +1,49 @@
language: rust
sudo: required
rust:
- stable
- beta
- nightly
- stable
- beta
- nightly
matrix:
allow_failures:
- rust: nightly
sudo: false
cache:
apt: true
pip: true
directories:
- target/debug/deps
- target/debug/build
- target/release/deps
- target/release/build
install:
- pip install 'travis-cargo<0.2' --user
- export PATH=$HOME/.local/bin:$PATH
addons:
apt:
packages:
- postfix
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev
before_script:
- pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH
- smtp-sink 2525 1000&
script:
- |
travis-cargo build &&
travis-cargo test &&
travis-cargo doc
- travis-cargo build
- travis-cargo test
- travis-cargo doc
after_success:
- travis-cargo --only stable doc-upload
- travis-cargo --only stable coveralls
- ./.travis/doc.sh
- ./.travis/coverage.sh
- travis-cargo --only nightly bench
env:
global:

20
.travis/coverage.sh Executable file
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 = "smtp"
version = "0.3.0"
description = "Simple SMTP client"
name = "lettre"
version = "0.6.3"
description = "Email client"
readme = "README.md"
documentation = "http://amousset.me/rust-smtp/smtp/"
repository = "https://github.com/amousset/rust-smtp"
documentation = "https://lettre.github.io/lettre/"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <alexis.mousset@gmx.fr>"]
authors = ["Alexis Mousset <contact@amousset.me>"]
keywords = ["email", "smtp", "mailer"]
[dependencies]
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,22 +1,52 @@
rust-smtp [![Build Status](https://travis-ci.org/amousset/rust-smtp.svg?branch=master)](https://travis-ci.org/amousset/rust-smtp) [![Coverage Status](https://coveralls.io/repos/github/amousset/rust-smtp/badge.svg?branch=master)](https://coveralls.io/github/amousset/rust-smtp?branch=master) [![Crate](https://meritbadge.herokuapp.com/smtp)](https://crates.io/crates/smtp) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
=========
# lettre
[![Build Status](https://travis-ci.org/lettre/lettre.svg?branch=master)](https://travis-ci.org/lettre/lettre)
[![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 library implements a simple SMTP client.
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information.
This is an email library written in Rust.
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]
smtp = "0.2"
lettre = "0.6"
```
License
-------
## Testing
The tests require an open mail server listening locally on port 25.
## License
This program is distributed under the terms of the MIT license.
See LICENSE for details.
See [LICENSE](./LICENSE) for details.

44
benches/transport_smtp.rs Normal file
View File

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

View File

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

1
rustfmt.toml Normal file
View File

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

View File

@@ -1,109 +0,0 @@
//! Provides authentication mecanisms
use std::fmt::{Display, Formatter};
use std::fmt;
use serialize::base64::{self, ToBase64, FromBase64};
use serialize::hex::ToHex;
use crypto::hmac::Hmac;
use crypto::md5::Md5;
use crypto::mac::Mac;
use NUL;
use error::Error;
/// Represents authentication mecanisms
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
pub enum Mecanism {
/// PLAIN authentication mecanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
Plain,
/// CRAM-MD5 authentication mecanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5,
}
impl Display for Mecanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f,
"{}",
match *self {
Mecanism::Plain => "PLAIN",
Mecanism::CramMd5 => "CRAM-MD5",
})
}
}
impl Mecanism {
/// Does the mecanism supports initial response
pub fn supports_initial_response(&self) -> bool {
match *self {
Mecanism::Plain => true,
Mecanism::CramMd5 => false,
}
}
/// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases
pub fn response(&self,
username: &str,
password: &str,
challenge: Option<&str>)
-> Result<String, Error> {
match *self {
Mecanism::Plain => {
match challenge {
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")),
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
.as_bytes()
.to_base64(base64::STANDARD)),
}
}
Mecanism::CramMd5 => {
let encoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::ClientError("This mecanism does expect a challenge")),
};
let decoded_challenge = match encoded_challenge.from_base64() {
Ok(challenge) => challenge,
Err(error) => return Err(Error::ChallengeParsingError(error)),
};
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
hmac.input(&decoded_challenge);
Ok(format!("{} {}", username, hmac.result().code().to_hex())
.as_bytes()
.to_base64(base64::STANDARD))
}
}
}
}
#[cfg(test)]
mod test {
use super::Mecanism;
#[test]
fn test_plain() {
let mecanism = Mecanism::Plain;
assert_eq!(mecanism.response("username", "password", None).unwrap(),
"AHVzZXJuYW1lAHBhc3N3b3Jk");
assert!(mecanism.response("username", "password", Some("test")).is_err());
}
#[test]
fn test_cram_md5() {
let mecanism = Mecanism::CramMd5;
assert_eq!(mecanism.response("alice",
"wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
.unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
assert!(mecanism.response("alice", "wonderland", Some("tést")).is_err());
assert!(mecanism.response("alice", "wonderland", None).is_err());
}
}

View File

@@ -1,348 +0,0 @@
//! Simple email (very incomplete)
use std::fmt::{Display, Formatter, Result};
use email_format::{MimeMessage, Header, Mailbox};
use time::{now, Tm};
use uuid::Uuid;
/// Converts an adress or an address with an alias to a `Address`
pub trait ToHeader {
/// Converts to a `Header` struct
fn to_header(&self) -> Header;
}
impl ToHeader for Header {
fn to_header(&self) -> Header {
(*self).clone()
}
}
impl<'a> ToHeader for (&'a str, &'a str) {
fn to_header(&self) -> Header {
let (name, value) = *self;
Header::new(name.to_string(), value.to_string())
}
}
/// Converts an adress or an address with an alias to a `Mailbox`
pub trait ToMailbox {
/// Converts to a `Mailbox` struct
fn to_mailbox(&self) -> Mailbox;
}
impl ToMailbox for Mailbox {
fn to_mailbox(&self) -> Mailbox {
(*self).clone()
}
}
impl<'a> ToMailbox for &'a str {
fn to_mailbox(&self) -> Mailbox {
Mailbox::new(self.to_string())
}
}
impl<'a> ToMailbox for (&'a str, &'a str) {
fn to_mailbox(&self) -> Mailbox {
let (address, alias) = *self;
Mailbox::new_with_name(alias.to_string(), address.to_string())
}
}
/// Builds an `Email` structure
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct EmailBuilder {
/// Email content
content: Email,
/// Date issued
date_issued: bool,
}
/// 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: Option<String>,
/// Message-ID
message_id: Uuid,
}
impl Display for Email {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", self.message.as_string())
}
}
impl EmailBuilder {
/// Creates a new empty email
pub fn new() -> EmailBuilder {
let current_message = Uuid::new_v4();
let mut email = Email {
message: MimeMessage::new_blank_message(),
to: vec![],
from: None,
message_id: current_message,
};
match Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>", current_message)) {
Ok(header) => email.message.headers.insert(header),
Err(_) => (),
}
EmailBuilder {
content: email,
date_issued: false,
}
}
/// Sets the email body
pub fn body(mut self, body: &str) -> EmailBuilder {
self.content.message.body = body.to_string();
self
}
/// Add a generic header
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
self.insert_header(header);
self
}
fn insert_header<A: ToHeader>(&mut self, header: A) {
self.content.message.headers.insert(header.to_header());
}
/// Adds a `From` header and store the sender address
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox();
self.insert_header(("From", mailbox.to_string().as_ref()));
self.content.from = Some(mailbox.address);
self
}
/// Adds a `To` header and store 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.content.to.push(mailbox.address);
self
}
/// Adds a `Cc` header and store the recipient address
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
let mailbox = address.to_mailbox();
self.insert_header(("Cc", mailbox.to_string().as_ref()));
self.content.to.push(mailbox.address);
self
}
/// 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
}
/// 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.content.from = Some(mailbox.address);
self
}
/// Adds a `Subject` header
pub fn subject(mut self, subject: &str) -> EmailBuilder {
self.insert_header(("Subject", subject));
self
}
/// 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
}
/// Build the Email
pub fn build(mut self) -> Email {
if !self.date_issued {
self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
}
self.content.message.update_headers();
self.content
}
}
/// Email sendable by an SMTP client
pub trait SendableEmail {
/// From address
fn from_address(&self) -> Option<String>;
/// To addresses
fn to_addresses(&self) -> Option<Vec<String>>;
/// Message content
fn message(&self) -> Option<String>;
/// Message ID
fn message_id(&self) -> Option<String>;
}
/// Minimal email structure
pub struct SimpleSendableEmail {
/// From address
from: String,
/// To addresses
to: Vec<String>,
/// Message
message: String,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(from_address: &str, to_address: &str, message: &str) -> SimpleSendableEmail {
SimpleSendableEmail {
from: from_address.to_string(),
to: vec![to_address.to_string()],
message: message.to_string(),
}
}
}
impl SendableEmail for SimpleSendableEmail {
fn from_address(&self) -> Option<String> {
Some(self.from.clone())
}
fn to_addresses(&self) -> Option<Vec<String>> {
Some(self.to.clone())
}
fn message(&self) -> Option<String> {
Some(self.message.clone())
}
fn message_id(&self) -> Option<String> {
Some(format!("<{}@rust-smtp>", Uuid::new_v4()))
}
}
impl SendableEmail for Email {
/// Return the to addresses, and fails if it is not set
fn to_addresses(&self) -> Option<Vec<String>> {
if self.to.is_empty() {
None
} else {
Some(self.to.clone())
}
}
/// Return the from address, and fails if it is not set
fn from_address(&self) -> Option<String> {
match self.from {
Some(ref from_address) => Some(from_address.clone()),
None => None,
}
}
fn message(&self) -> Option<String> {
Some(format!("{}", self))
}
fn message_id(&self) -> Option<String> {
Some(format!("{}", self.message_id))
}
}
#[cfg(test)]
mod test {
use time::now;
use uuid::Uuid;
use email_format::{MimeMessage, Header};
use super::{SendableEmail, EmailBuilder, Email};
#[test]
fn test_email_display() {
let current_message = Uuid::new_v4();
let mut email = Email {
message: MimeMessage::new_blank_message(),
to: vec![],
from: None,
message_id: current_message,
};
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>",
current_message))
.unwrap());
email.message
.headers
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
.unwrap());
email.message.body = "body".to_string();
assert_eq!(format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
current_message));
assert_eq!(current_message.to_string(), email.message_id().unwrap());
}
#[test]
fn test_email_builder() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = email_builder.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.add_header(("X-test", "value"))
.build();
assert_eq!(format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: <user@localhost>\r\nFrom: \
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
<reply@localhost>\r\nSender: <sender@localhost>\r\nDate: \
{}\r\nSubject: Hello\r\nX-test: value\r\n\r\nHello World!\r\n",
email.message_id().unwrap(),
date_now.rfc822z()));
}
#[test]
fn test_email_sendable() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = email_builder.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.add_header(("X-test", "value"))
.build();
assert_eq!(email.from_address().unwrap(),
"sender@localhost".to_string());
assert_eq!(email.to_addresses().unwrap(),
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
assert_eq!(email.message().unwrap(), format!("{}", email));
}
}

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",
}
}
}

799
src/email/mod.rs Normal file
View File

@@ -0,0 +1,799 @@
//! Simple email representation
pub mod error;
use email::error::Error;
use email_format::{Header, Mailbox, Address, MimeMessage, MimeMultipartType};
use mime::Mime;
use std::fmt;
use std::fmt::{Display, Formatter};
use time::{Tm, now};
use uuid::Uuid;
/// 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;
}
impl ToHeader for Header {
fn to_header(&self) -> Header {
self.clone()
}
}
impl<'a> ToHeader for (&'a str, &'a str) {
fn to_header(&self) -> Header {
let (name, value) = *self;
Header::new(name.to_string(), value.to_string())
}
}
/// Converts an adress or an address with an alias to a `Mailbox`
pub trait ToMailbox {
/// Converts to a `Mailbox` struct
fn to_mailbox(&self) -> Mailbox;
}
impl ToMailbox for Mailbox {
fn to_mailbox(&self) -> Mailbox {
(*self).clone()
}
}
impl<'a> ToMailbox for &'a str {
fn to_mailbox(&self) -> Mailbox {
Mailbox::new(self.to_string())
}
}
impl<'a> ToMailbox for (&'a str, &'a str) {
fn to_mailbox(&self) -> Mailbox {
let (address, alias) = *self;
Mailbox::new_with_name(alias.to_string(), address.to_string())
}
}
/// 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: 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 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,
/// Envelope
envelope: Envelope,
/// Message-ID
message_id: Uuid,
}
impl Display for Email {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.message.as_string())
}
}
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: 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,
}
}
/// Sets the email body
pub fn body(mut self, body: &str) -> EmailBuilder {
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) {
self.message.add_header(header);
}
/// Adds a `From` header and stores the sender address
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
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.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 {
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.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 {
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 {
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.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.set_date(date);
self
}
/// 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;
}
/// 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();
if let Ok(header) = Header::new_with_value("Message-ID".to_string(),
format!("<{}.lettre@localhost>", message_id)) {
self.message.add_header(header)
}
Ok(Email {
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
fn from_address(&self) -> String;
/// To addresses
fn to_addresses(&self) -> Vec<String>;
/// Message content
fn message(&self) -> String;
/// Message ID
fn message_id(&self) -> String;
}
/// Minimal email structure
pub struct SimpleSendableEmail {
/// From address
from: String,
/// To addresses
to: Vec<String>,
/// Message
message: String,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(from_address: &str, to_address: Vec<String>, message: &str) -> SimpleSendableEmail {
SimpleSendableEmail {
from: from_address.to_string(),
to: to_address,
message: message.to_string(),
}
}
}
impl SendableEmail for SimpleSendableEmail {
fn from_address(&self) -> String {
self.from.clone()
}
fn to_addresses(&self) -> Vec<String> {
self.to.clone()
}
fn message(&self) -> String {
self.message.clone()
}
fn message_id(&self) -> String {
format!("{}", Uuid::new_v4())
}
}
impl SendableEmail for Email {
fn to_addresses(&self) -> Vec<String> {
self.envelope.to.clone()
}
fn from_address(&self) -> String {
self.envelope.from.clone()
}
fn message(&self) -> String {
format!("{}", self)
}
fn message_id(&self) -> String {
format!("{}", self.message_id)
}
}
#[cfg(test)]
mod test {
use email_format::{Header, MimeMessage};
use super::{Email, EmailBuilder, Envelope, SendableEmail};
use time::now;
use uuid::Uuid;
#[test]
fn test_email_display() {
let current_message = Uuid::new_v4();
let mut email = Email {
message: MimeMessage::new_blank_message(),
envelope: Envelope {
to: vec![],
from: "".to_string(),
},
message_id: current_message,
};
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>",
current_message))
.unwrap());
email.message
.headers
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
.unwrap());
email.message.body = "body".to_string();
assert_eq!(format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
current_message));
assert_eq!(current_message.to_string(), email.message_id());
}
#[test]
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();
let email = email_builder.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap();
assert_eq!(format!("{}", email),
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()));
}
#[test]
fn test_email_sendable() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = email_builder.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap();
assert_eq!(email.from_address(), "sender@localhost".to_string());
assert_eq!(email.to_addresses(),
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
assert_eq!(email.message(), format!("{}", email));
}
}

View File

@@ -1,133 +1,177 @@
//! # Rust SMTP client
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
//!
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
//! a work in progress. It is designed to efficiently send emails from an application to a
//! relay email server, as it relies as much as possible on the relay server for sanity and RFC
//! ## Overview
//!
//! This mailer is divided into:
//!
//! * An `email` part: builds the email message
//! * A `transport` part: contains the available transports for your emails. To be sendable, the
//! emails have to implement `SendableEmail`.
//!
//! ## Creating messages
//!
//! The `email` part builds email messages. For now, it does not support attachments.
//! An email is built using an `EmailBuilder`. The simplest email could be:
//!
//! ```rust
//! use lettre::email::EmailBuilder;
//!
//! // Create an email
//! let email = EmailBuilder::new()
//! // Addresses can be specified by the tuple (email, alias)
//! .to(("user@example.org", "Firstname Lastname"))
//! // ... or by an address only
//! .from("user@example.com")
//! .subject("Hi, Hello world")
//! .text("Hello world.")
//! .build();
//!
//! assert!(email.is_ok());
//! ```
//!
//! When the `build` method is called, the `EmailBuilder` will add the missing headers (like
//! `Message-ID` or `Date`) and check for missing necessary ones (like `From` or `To`). It will
//! then generate an `Email` that can be sent.
//!
//! The `text()` method will create a plain text email, while the `html()` method will create an
//! HTML email. You can use the `alternative()` method to provide both versions, using plain text
//! as fallback for the HTML version.
//!
//! Below is a more complete example, not using method chaining:
//!
//! ```rust
//! use lettre::email::EmailBuilder;
//!
//! let mut builder = EmailBuilder::new();
//! builder.add_to(("user@example.org", "Alias name"));
//! builder.add_cc(("user@example.net", "Alias name"));
//! builder.add_from("no-reply@example.com");
//! builder.add_from("no-reply@example.eu");
//! builder.set_sender("no-reply@example.com");
//! builder.set_subject("Hello world");
//! builder.set_alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.");
//! builder.add_reply_to("contact@example.com");
//! builder.add_header(("X-Custom-Header", "my header"));
//!
//! let email = builder.build();
//! assert!(email.is_ok());
//! ```
//!
//! See the `EmailBuilder` documentation for a complete list of methods.
//!
//! ## Sending messages
//!
//! The following sections describe the available transport methods to handle emails.
//!
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
//! the prefered way of sending emails.
//! * The `FileTransport` creates a file containing the email content to be sent. It can be used
//! for debugging or if you want to keep all sent emails.
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the
//! logs.
//!
//! ### SMTP transport
//!
//! This SMTP follows [RFC
//! 5321](https://tools.ietf.org/html/rfc5321), but is still
//! a work in progress. It is designed to efficiently send emails from an
//! application to a
//! relay email server, as it relies as much as possible on the relay server
//! for sanity and RFC
//! compliance checks.
//!
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and
//! CRAM-MD5 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//!
//! It will eventually implement the following extensions:
//!
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
//! ## Architecture
//!
//! This client is divided into three main parts:
//!
//! * client: a low level SMTP client providing all SMTP commands
//! * sender: a high level SMTP client providing an easy method to send emails
//! * email: generates the email to be sent with the sender
//!
//! ## Usage
//!
//! ### Simple example
//! #### Simple example
//!
//! This is the most basic example of usage:
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::EmailBuilder;
//! ```rust
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
//! use lettre::email::EmailBuilder;
//! use lettre::transport::EmailTransport;
//!
//! // Create an email
//! let email = EmailBuilder::new()
//! // 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();
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .unwrap();
//!
//! // Open a local connection on port 25
//! let mut sender = SenderBuilder::localhost().unwrap().build();
//! let mut mailer =
//! SmtpTransportBuilder::localhost().unwrap().build();
//! // Send the email
//! let result = sender.send(email);
//! let result = mailer.send(email);
//!
//! assert!(result.is_ok());
//! ```
//!
//! ### Complete example
//! #### Complete example
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::EmailBuilder;
//! use smtp::authentication::Mecanism;
//! use smtp::SUBMISSION_PORT;
//! use lettre::email::EmailBuilder;
//! use lettre::transport::smtp::{SecurityLevel, SmtpTransport,
//! SmtpTransportBuilder};
//! use lettre::transport::smtp::authentication::Mechanism;
//! use lettre::transport::smtp::SUBMISSION_PORT;
//! use lettre::transport::EmailTransport;
//!
//! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name"));
//! 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();
//! 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 sender = SenderBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap()
//! let mut mailer = SmtpTransportBuilder::new(("server.tld",
//! SUBMISSION_PORT)).unwrap()
//! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name("my.hostname.tld")
//! // Add credentials for authentication
//! .credentials("username", "password")
//! // Use TLS with STARTTLS, you can also specify a specific SSL context
//! // with `.ssl_context(context)`
//! .starttls()
//! // Configure accepted authetication mecanisms
//! .authentication_mecanisms(vec![Mecanism::CramMd5])
//! // Specify a TLS security level. You can also specify an SslContext with
//! // .ssl_context(SslContext::Ssl23)
//! .security_level(SecurityLevel::AlwaysEncrypt)
//! // Enable SMTPUTF8 if the server supports it
//! .smtp_utf8(true)
//! // Configure expected authentication mechanism
//! .authentication_mechanism(Mechanism::CramMd5)
//! // Enable connection reuse
//! .enable_connection_reuse(true).build();
//! .connection_reuse(true).build();
//!
//! let result_1 = sender.send(email.clone());
//! let result_1 = mailer.send(email.clone());
//! assert!(result_1.is_ok());
//!
//! // The second email will use the same connection
//! let result_2 = sender.send(email);
//! let result_2 = mailer.send(email);
//! assert!(result_2.is_ok());
//!
//! // Explicitely close the SMTP transaction as we enabled connection reuse
//! sender.close();
//! // Explicitly close the SMTP transaction as we enabled connection reuse
//! mailer.close();
//! ```
//!
//! ### Using the client directly
//! #### Lower level
//!
//! If you just want to send an email without using `Email` to provide headers:
//! You can also send commands, here is a simple email transaction without
//! error handling:
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::SimpleSendableEmail;
//!
//! // Create a minimal email
//! let email = SimpleSendableEmail::new(
//! "test@example.com",
//! "test@example.org",
//! "Hello world !"
//! );
//!
//! let mut sender = SenderBuilder::localhost().unwrap().build();
//! let result = sender.send(email);
//! assert!(result.is_ok());
//! ```
//!
//! ### Lower level
//!
//! You can also send commands, here is a simple email transaction without error handling:
//!
//! ```rust,no_run
//! use smtp::client::Client;
//! use smtp::SMTP_PORT;
//! use smtp::client::net::NetworkStream;
//! ```rust
//! use lettre::transport::smtp::SMTP_PORT;
//! use lettre::transport::smtp::client::Client;
//! use lettre::transport::smtp::client::net::NetworkStream;
//!
//! let mut email_client: Client<NetworkStream> = Client::new();
//! let _ = email_client.connect(&("localhost", SMTP_PORT));
//! let _ = email_client.connect(&("localhost", SMTP_PORT), None);
//! let _ = email_client.ehlo("my_hostname");
//! let _ = email_client.mail("user@example.com", None);
//! let _ = email_client.rcpt("user@example.org");
@@ -135,12 +179,83 @@
//! let _ = email_client.message("Test email");
//! let _ = email_client.quit();
//! ```
//!
//! ### Stub transport
//!
//! The stub transport only logs message envelope and drops the content. It can be useful for
//! testing purposes.
//!
//! ```rust
//! use lettre::transport::stub::StubEmailTransport;
//! use lettre::transport::EmailTransport;
//! use lettre::email::EmailBuilder;
//!
//! let email = EmailBuilder::new()
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .unwrap();
//!
//! let mut sender = StubEmailTransport;
//! let result = sender.send(email);
//! assert!(result.is_ok());
//! ```
//!
//! Will log the line:
//!
//! ```text
//! b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
//! ```
//!
//! ### File transport
//!
//! The file transport writes the emails to the given directory. The name of the file will be
//! `message_id.txt`.
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//!
//! ```rust
//! use std::env::temp_dir;
//!
//! use lettre::transport::file::FileEmailTransport;
//! use lettre::transport::EmailTransport;
//! use lettre::email::{EmailBuilder, SendableEmail};
//!
//! // Write to the local temp directory
//! let mut sender = FileEmailTransport::new(temp_dir());
//! let email = EmailBuilder::new()
//! .to("root@localhost")
//! .from("user@localhost")
//! .body("Hello World!")
//! .subject("Hello")
//! .build()
//! .unwrap();
//!
//! let result = sender.send(email);
//! assert!(result.is_ok());
//! ```
//! Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.txt`:
//!
//! ```text
//! b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
//! To: <root@localhost>
//! From: <user@localhost>
//! Subject: Hello
//! Date: Sat, 31 Oct 2015 13:42:19 +0100
//! Message-ID: <b7c211bc-9811-45ce-8cd9-68eab575d695.lettre@localhost>
//!
//! Hello World!
//! ```
#![deny(missing_docs)]
#![deny(missing_docs, unsafe_code, unstable_features)]
#[macro_use]
extern crate log;
extern crate rustc_serialize as serialize;
#[macro_use]
extern crate mime;
extern crate rustc_serialize;
extern crate crypto;
extern crate time;
extern crate uuid;
@@ -148,39 +263,5 @@ extern crate email as email_format;
extern crate bufstream;
extern crate openssl;
mod extension;
pub mod client;
pub mod sender;
pub mod response;
pub mod error;
pub mod authentication;
pub mod transport;
pub mod email;
// Registrated port numbers:
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port
pub static SMTP_PORT: u16 = 25;
/// Default smtps port
pub static SMTPS_PORT: u16 = 465;
/// Default submission port
pub static SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub static SP: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub static CRLF: &'static str = "\r\n";
/// Colon
pub static COLON: &'static str = ":";
/// The ending of message content
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub static NUL: &'static str = "\0";

View File

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

View File

@@ -0,0 +1,54 @@
//! Error and result type for file transport
use self::Error::*;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(&'static str),
/// IO error
Io(io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Client(_) => "an unknown error occured",
Io(_) => "an I/O error occured",
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
Io(ref err) => Some(&*err as &StdError),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
}
/// SMTP result type
pub type FileResult = Result<(), Error>;

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

@@ -0,0 +1,51 @@
//! This transport creates a file for each email, containing the envelope information and the email
//! itself.
use email::SendableEmail;
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use transport::EmailTransport;
use transport::file::error::FileResult;
pub mod error;
/// Writes the content and the envelope information to a file
pub struct FileEmailTransport {
path: PathBuf,
}
impl FileEmailTransport {
/// Creates a new transport to the given directory
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport {
let mut path_buf = PathBuf::new();
path_buf.push(path);
FileEmailTransport { path: path_buf }
}
}
impl EmailTransport<FileResult> for FileEmailTransport {
fn send<T: SendableEmail>(&mut self, email: T) -> FileResult {
let mut file = self.path.clone();
file.push(format!("{}.txt", email.message_id()));
let mut f = try!(File::create(file.as_path()));
let log_line = format!("{}: from=<{}> to=<{}>\n",
email.message_id(),
email.from_address(),
email.to_addresses().join("> to=<"));
try!(f.write_all(log_line.as_bytes()));
try!(f.write_all(email.message().clone().as_bytes()));
info!("{} status=<written>", log_line);
Ok(())
}
fn close(&mut self) {
()
}
}

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

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

View File

@@ -0,0 +1,111 @@
//! Provides authentication mechanisms
use crypto::hmac::Hmac;
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::smtp::error::Error;
/// Represents authentication mechanisms
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
pub enum Mechanism {
/// PLAIN authentication mechanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
Plain,
/// CRAM-MD5 authentication mechanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5,
}
impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f,
"{}",
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::CramMd5 => "CRAM-MD5",
})
}
}
impl Mechanism {
/// Does the mechanism supports initial response
pub fn supports_initial_response(&self) -> bool {
match *self {
Mechanism::Plain => true,
Mechanism::CramMd5 => false,
}
}
/// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases
pub fn response(&self,
username: &str,
password: &str,
challenge: Option<&str>)
-> Result<String, Error> {
match *self {
Mechanism::Plain => {
match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => {
Ok(format!("{}{}{}{}", NUL, username, NUL, password)
.as_bytes()
.to_base64(base64::STANDARD))
}
}
}
Mechanism::CramMd5 => {
let encoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
let decoded_challenge = match encoded_challenge.from_base64() {
Ok(challenge) => challenge,
Err(error) => return Err(Error::ChallengeParsing(error)),
};
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
hmac.input(&decoded_challenge);
Ok(format!("{} {}", username, hmac.result().code().to_hex())
.as_bytes()
.to_base64(base64::STANDARD))
}
}
}
}
#[cfg(test)]
mod test {
use super::Mechanism;
#[test]
fn test_plain() {
let mechanism = Mechanism::Plain;
assert_eq!(mechanism.response("username", "password", None).unwrap(),
"AHVzZXJuYW1lAHBhc3N3b3Jk");
assert!(mechanism.response("username", "password", Some("test")).is_err());
}
#[test]
fn test_cram_md5() {
let mechanism = Mechanism::CramMd5;
assert_eq!(mechanism.response("alice",
"wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
.unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
assert!(mechanism.response("alice", "wonderland", Some("tést")).is_err());
assert!(mechanism.response("alice", "wonderland", None).is_err());
}
}

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 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 response::ResponseParser;
use authentication::Mecanism;
use error::{Error, SmtpResult};
use client::net::{Connector, NetworkStream};
use {CRLF, MESSAGE_ENDING};
use transport::smtp::error::{SmtpResult, Error};
use transport::smtp::response::ResponseParser;
pub mod net;
@@ -22,11 +22,11 @@ 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(".") {
format!(".{}", string)
} else {
string.to_string()
}
if string.starts_with('.') {
format!(".{}", string)
} else {
string.to_string()
}
.replace("\r.", "\r..")
.replace("\n.", "\n..")
}
@@ -44,6 +44,7 @@ fn remove_crlf(string: &str) -> String {
}
/// Structure that implements the SMTP client
#[derive(Debug)]
pub struct Client<S: Write + Read = NetworkStream> {
/// TCP stream between client and server
/// Value is None before connection
@@ -56,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`
@@ -65,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();
@@ -79,16 +80,25 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
/// Upgrades the underlying connection to SSL/TLS
pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> {
//let current_stream = self.stream.clone();
if self.stream.is_some() {
self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context)
} else {
Ok(())
match self.stream {
Some(ref mut stream) => stream.get_mut().upgrade_tls(ssl_context),
None => Ok(()),
}
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
match self.stream {
Some(ref stream) => stream.get_ref().is_encrypted(),
None => false,
}
}
/// Connects to the configured server
pub fn connect<A: ToSocketAddrs>(&mut self, addr: &A) -> SmtpResult {
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()
}
@@ -173,26 +185,26 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
self.command("RSET")
}
/// Sends an AUTH command with the given mecanism
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
/// Sends an AUTH command with the given mechanism
pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> 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,
password,
Some(&encoded_challenge)));
let cram_response = try!(mechanism.response(username,
password,
Some(&encoded_challenge)));
self.command(&format!("{}", cram_response))
self.command(&cram_response.clone())
}
}
@@ -223,7 +235,7 @@ impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
/// Gets the SMTP response
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,19 +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 {
@@ -21,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) {
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
},
NetworkStream::Ssl(stream) => NetworkStream::Ssl(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)),
}
}
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,
}
}
}
@@ -50,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 response::{Severity, Response};
use serialize::base64::FromBase64Error;
use rustc_serialize::base64::FromBase64Error;
use self::Error::*;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use transport::smtp::response::{Response, Severity};
/// An enum of all error kinds.
#[derive(Debug)]
@@ -15,21 +15,21 @@ pub enum Error {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
TransientError(Response),
Transient(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
PermanentError(Response),
Permanent(Response),
/// Error parsing a response
ResponseParsingError(&'static str),
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsingError(FromBase64Error),
ChallengeParsing(FromBase64Error),
/// Internal client error
ClientError(&'static str),
Client(&'static str),
/// DNS resolution error
ResolutionError,
Resolution,
/// IO error
IoError(io::Error),
Io(io::Error),
}
impl Display for Error {
@@ -41,19 +41,19 @@ impl Display for Error {
impl StdError for Error {
fn description(&self) -> &str {
match *self {
TransientError(_) => "a transient error occured during the SMTP transaction",
PermanentError(_) => "a permanent error occured during the SMTP transaction",
ResponseParsingError(_) => "an error occured while parsing an SMTP response",
ChallengeParsingError(_) => "an error occured while parsing a CRAM-MD5 challenge",
ResolutionError => "Could no resolve hostname",
ClientError(_) => "an unknown error occured",
IoError(_) => "an I/O error occured",
Transient(_) => "a transient error occured during the SMTP transaction",
Permanent(_) => "a permanent error occured during the SMTP transaction",
ResponseParsing(_) => "an error occured while parsing an SMTP response",
ChallengeParsing(_) => "an error occured while parsing a CRAM-MD5 challenge",
Resolution => "could not resolve hostname",
Client(_) => "an unknown error occured",
Io(_) => "an I/O error occured",
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
IoError(ref err) => Some(&*err as &StdError),
Io(ref err) => Some(&*err as &StdError),
_ => None,
}
}
@@ -61,30 +61,25 @@ impl StdError for Error {
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
IoError(err)
Io(err)
}
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.severity() {
Severity::TransientNegativeCompletion => TransientError(response),
Severity::PermanentNegativeCompletion => PermanentError(response),
_ => ClientError("Unknown error code"),
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
}
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
ClientError(string)
Client(string)
}
}
/// SMTP result type
pub type SmtpResult = Result<Response, Error>;
#[cfg(test)]
mod test {
// TODO
}

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 response::Response;
use error::Error;
use authentication::Mecanism;
use transport::smtp::error::Error;
use transport::smtp::response::Response;
/// 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 authentication::Mecanism;
use 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,20 +187,19 @@ 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),
vec!["me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]);
let response2 =
Response::new(Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
vec!["me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]);
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
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));
}
}

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

@@ -0,0 +1,419 @@
//! This transport sends emails using the SMTP protocol
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 const SMTP_PORT: u16 = 25;
/// Default submission port
pub const SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub const SP: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub const CRLF: &'static str = "\r\n";
/// Colon
pub const COLON: &'static str = ":";
/// The ending of message content
pub const MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub const NUL: &'static str = "\0";
/// TLS security level
#[derive(Debug)]
pub enum SecurityLevel {
/// Use a TLS wrapped connection
///
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
EncryptedWrapper,
/// Only send an email on encrypted connection (with STARTTLS)
///
/// Recommended mode, prevents MITM when used with verified certificates.
AlwaysEncrypt,
/// Use TLS when available (with STARTTLS)
///
/// Default mode.
Opportunistic,
/// Never use TLS
NeverEncrypt,
}
/// Contains client configuration
pub struct SmtpTransportBuilder {
/// Maximum connection reuse
///
/// Zero means no limitation
connection_reuse_count_limit: u16,
/// Enable connection reuse
connection_reuse: bool,
/// Name sent during HELO or EHLO
hello_name: String,
/// Credentials
credentials: Option<(String, String)>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// SSL context to use
ssl_context: SslContext,
/// TLS security level
security_level: SecurityLevel,
/// Enable UTF8 mailboxes in envelope or headers
smtp_utf8: bool,
/// Optional enforced authentication mechanism
authentication_mechanism: Option<Mechanism>,
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder {
/// Creates a new local SMTP client
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() {
Some(addr) => {
Ok(SmtpTransportBuilder {
server_addr: addr,
ssl_context: SslContext::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_mechanism: None,
})
}
None => Err(From::from("Could nor resolve hostname")),
}
}
/// Creates a new local SMTP client to port 25
pub fn localhost() -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(("localhost", SMTP_PORT))
}
/// Use STARTTLS with a specific context
pub fn ssl_context(mut self, ssl_context: SslContext) -> SmtpTransportBuilder {
self.ssl_context = ssl_context;
self
}
/// Set the security level for SSL/TLS
pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder {
self.security_level = level;
self
}
/// Require SSL/TLS using STARTTLS
///
/// Incompatible with `ssl_wrapper()``
pub fn encrypt(mut self) -> SmtpTransportBuilder {
self.security_level = SecurityLevel::AlwaysEncrypt;
self
}
/// Require SSL/TLS using 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
}
/// Set the name used during HELO or EHLO
pub fn hello_name(mut self, name: &str) -> SmtpTransportBuilder {
self.hello_name = name.to_string();
self
}
/// Enable connection reuse
pub fn connection_reuse(mut self, enable: bool) -> SmtpTransportBuilder {
self.connection_reuse = enable;
self
}
/// Set the maximum number of emails sent using one connection
pub fn connection_reuse_count_limit(mut self, limit: u16) -> SmtpTransportBuilder {
self.connection_reuse_count_limit = limit;
self
}
/// Set the client credentials
pub fn credentials(mut self, username: &str, password: &str) -> SmtpTransportBuilder {
self.credentials = Some((username.to_string(), password.to_string()));
self
}
/// Set the authentication mechanisms
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpTransportBuilder {
self.authentication_mechanism = Some(mechanism);
self
}
/// Build the SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn build(self) -> SmtpTransport {
SmtpTransport::new(self)
}
}
/// Represents the state of a client
#[derive(Debug)]
struct State {
/// Panic state
pub panic: bool,
/// Connection reuse counter
pub connection_reuse_count: u16,
}
/// Structure that implements the high level SMTP client
pub struct SmtpTransport {
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
/// SmtpTransport variable states
state: State,
/// Information about the client
client_info: SmtpTransportBuilder,
/// Low level client
client: Client,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.reset();
}
return Err(From::from(err))
},
}
})
);
impl SmtpTransport {
/// Creates a new SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
let client = Client::new();
SmtpTransport {
client: client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
/// Reset the client state
fn reset(&mut self) {
// Close the SMTP transaction if needed
self.close();
// Reset the client state
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
}
/// Gets the EHLO response and updates server information
pub fn get_ehlo(&mut self) -> SmtpResult {
// Extended Hello
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
Ok(ehlo_response)
}
}
impl EmailTransport<SmtpResult> for SmtpTransport {
/// Sends an email
fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
// Extract email information
let message_id = email.message_id();
let from_address = email.from_address();
let to_addresses = email.to_addresses();
let message = email.message();
// Check if the connection is still available
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
self.reset();
}
if self.state.connection_reuse_count == 0 {
try!(self.client.connect(&self.client_info.server_addr,
match &self.client_info.security_level {
&SecurityLevel::EncryptedWrapper => {
Some(&self.client_info.ssl_context)
}
_ => None,
}));
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
try!(self.get_ehlo());
match (&self.client_info.security_level,
self.server_info.as_ref().unwrap().supports_feature(&Extension::StartTls)) {
(&SecurityLevel::AlwaysEncrypt, false) => {
return Err(From::from("Could not encrypt connection, aborting"))
}
(&SecurityLevel::Opportunistic, false) => (),
(&SecurityLevel::NeverEncrypt, _) => (),
(&SecurityLevel::EncryptedWrapper, _) => (),
(_, true) => {
try_smtp!(self.client.starttls(), self);
try_smtp!(self.client.upgrade_tls_stream(&self.client_info.ssl_context),
self);
debug!("connection encrypted");
// Send EHLO again
try!(self.get_ehlo());
}
}
if self.client_info.credentials.is_some() {
let (username, password) = self.client_info.credentials.clone().unwrap();
let mut found = false;
// 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(mechanism, &username, &password), self);
break;
}
}
if !found {
info!("No supported authentication mechanisms available");
}
}
}
// Mail
let mail_options = match (self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::EightBitMime),
self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::SmtpUtfEight)) {
(true, true) => Some("BODY=8BITMIME SMTPUTF8"),
(true, false) => Some("BODY=8BITMIME"),
(false, _) => None,
};
try_smtp!(self.client.mail(&from_address, mail_options), self);
// Log the mail command
info!("{}: from=<{}>", message_id, from_address);
// Recipient
for to_address in &to_addresses {
try_smtp!(self.client.rcpt(&to_address), self);
// Log the rcpt command
info!("{}: to=<{}>", message_id, to_address);
}
// Data
try_smtp!(self.client.data(), self);
// Message content
let result = self.client.message(&message);
if result.is_ok() {
// Increment the connection reuse counter
self.state.connection_reuse_count += 1;
// Log the message
info!("{}: conn_use={}, size={}, status=sent ({})",
message_id,
self.state.connection_reuse_count,
message.len(),
result.as_ref()
.ok()
.unwrap()
.message()
.iter()
.next()
.unwrap_or(&"no response".to_string()));
}
// Test if we can reuse the existing connection
if (!self.client_info.connection_reuse) ||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
self.reset();
}
result
}
/// Closes the inner connection
fn close(&mut self) {
self.client.close();
}
}

View File

@@ -1,12 +1,14 @@
//! SMTP response, containing a mandatory return code and an optional text message
//! SMTP response, containing a mandatory return code and an optional text
//! message
use std::str::FromStr;
use std::fmt::{Display, Formatter, Result};
use std::result;
use self::Category::*;
use self::Severity::*;
use self::Category::*;
use error::{SmtpResult, Error};
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::FromStr;
use transport::smtp::error::{Error, SmtpResult};
/// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
@@ -29,7 +31,7 @@ impl FromStr for Severity {
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
_ => Err(Error::ResponseParsing("First digit must be between 2 and 5")),
}
}
}
@@ -74,7 +76,7 @@ impl FromStr for Category {
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"5" => Ok(MailSystem),
_ => Err(Error::ResponseParsingError("Second digit must be between 0 and 5")),
_ => Err(Error::ResponseParsing("Second digit must be between 0 and 5")),
}
}
}
@@ -114,15 +116,17 @@ impl FromStr for Code {
match (s[0..1].parse::<Severity>(),
s[1..2].parse::<Category>(),
s[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {
severity: severity,
category: category,
detail: detail,
}),
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
(Ok(severity), Ok(category), Ok(detail)) => {
Ok(Code {
severity: severity,
category: category,
detail: detail,
})
}
_ => Err(Error::ResponseParsing("Could not parse response code")),
}
} else {
Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"))
Err(Error::ResponseParsing("Wrong code length (should be 3 digit)"))
}
}
}
@@ -144,7 +148,7 @@ impl Code {
}
/// Parses an SMTP response
#[derive(PartialEq,Eq,Clone,Debug)]
#[derive(PartialEq,Eq,Clone,Debug,Default)]
pub struct ResponseParser {
/// Response code
code: Option<Code>,
@@ -154,25 +158,17 @@ pub struct ResponseParser {
}
impl ResponseParser {
/// Creates a new parser
pub fn new() -> ResponseParser {
ResponseParser {
code: None,
message: vec![],
}
}
/// Parses a line and return a `bool` indicating if there are more lines to come
pub fn read_line(&mut self, line: &str) -> result::Result<bool, Error> {
if line.len() < 3 {
return Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"));
return Err(Error::ResponseParsing("Wrong code length (should be 3 digit)"));
}
match self.code {
Some(ref code) => {
if code.code() != line[0..3] {
return Err(Error::ResponseParsingError("Response code has changed during a \
return Err(Error::ResponseParsing("Response code has changed during a \
reponse"));
}
}
@@ -181,11 +177,7 @@ 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)
}
@@ -195,13 +187,15 @@ impl ResponseParser {
pub fn response(self) -> SmtpResult {
match self.code {
Some(code) => Ok(Response::new(code, self.message)),
None => Err(Error::ResponseParsingError("Incomplete response, could not read \
response code")),
None => {
Err(Error::ResponseParsing("Incomplete response, could not read response \
code"))
}
}
}
}
/// Contains an SMTP reply, with separed code and message
/// Contains an SMTP reply, with separated code and message
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq,Eq,Clone,Debug)]
@@ -263,19 +257,21 @@ impl Response {
/// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<String> {
match self.message.is_empty() {
true => None,
false => match self.message[0].split_whitespace().next() {
if self.message.is_empty() {
None
} else {
match self.message[0].split_whitespace().next() {
Some(word) => Some(word.to_string()),
None => None,
},
}
}
}
}
#[cfg(test)]
mod test {
use super::{Severity, Category, Response, ResponseParser, Code};
use super::{Category, Code, Response, ResponseParser, Severity};
#[test]
fn test_severity_from_str() {
@@ -374,7 +370,7 @@ mod test {
#[test]
fn test_response_parser() {
let mut parser = ResponseParser::new();
let mut parser = ResponseParser::default();
assert!(parser.read_line("250-me").unwrap());
assert!(parser.read_line("250-8BITMIME").unwrap());
@@ -407,7 +403,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.is_positive());
.is_positive());
assert!(!Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
@@ -416,7 +412,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.is_positive());
.is_positive());
}
#[test]
@@ -518,7 +514,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(241));
.has_code(241));
assert!(!Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
@@ -527,7 +523,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(251));
.has_code(251));
}
#[test]

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

28
src/transport/stub/mod.rs Normal file
View File

@@ -0,0 +1,28 @@
//! This transport is a stub that only logs the message, and always returns
//! success
use email::SendableEmail;
use transport::EmailTransport;
pub mod error;
/// This transport does nothing except logging the message envelope
pub struct StubEmailTransport;
/// 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(())
}
fn close(&mut self) {
()
}
}

View File

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

37
tests/transport_file.rs Normal file
View File

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

19
tests/transport_smtp.rs Normal file
View File

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

19
tests/transport_stub.rs Normal file
View File

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