Compare commits

..

115 Commits

Author SHA1 Message Date
Alexis Mousset
a468cb3ab8 chore(all): 0.9.3 release 2020-04-19 10:56:46 +02:00
Alexis Mousset
9b591ff932 chore(all): Fix warnings 2020-04-19 10:24:23 +02:00
Alexis Mousset
eff4e1693f chore(builder): Update email to 0.0.21 2020-04-19 09:56:31 +02:00
Alexis Mousset
75ab05229a feat(all): v0.9.2 release 2019-06-11 19:58:32 +02:00
Alexis Mousset
393ef8dcd1 Simplify header formatting and fix nightly build (fixes #340) 2019-06-11 19:48:01 +02:00
Alexis Mousset
ceb57edfdd Remove failure crate usage (fixes #331) 2019-05-06 09:36:10 +02:00
Alexis Mousset
cf8f934c56 fix(docs): Broken title syntax in SMTP docs 2019-05-05 21:01:36 +02:00
Alexis Mousset
df949f837e fix(all): Properly override favicon in docs theme 2019-05-05 20:49:12 +02:00
Alexis Mousset
0a3d51dc25 fix(docs): Use doc root and set custom favicon 2019-05-05 20:15:45 +02:00
Alexis Mousset
4828cf4e92 feat(all): Require rust 1.32 2019-05-05 20:07:38 +02:00
Alexis Mousset
c33de49fbb feat(all): Move to mdBook for the docs 2019-05-05 19:45:51 +02:00
Alexis Mousset
4f470a2c3f feat(all): 0.9.1 release 2019-05-05 18:20:30 +02:00
Alexis Mousset
a0c8fb947c fix(email): Re-export mime crate 2019-05-05 18:16:05 +02:00
Alexis Mousset
101189882a Prepare 0.9 release 2019-03-17 13:49:45 +01:00
Alexis Mousset
be50862d55 Fix clippy and formatting 2019-03-17 13:26:14 +01:00
Alexis Mousset
2d4320ea45 Fix clippy and formatting 2019-03-17 13:21:14 +01:00
Alexis Mousset
0444e7833b feat(email): Uptae dev-dependencies 2019-03-17 12:55:25 +01:00
Alexis Mousset
6997ab7ce4 Merge pull request #316 from marmistrz/SmtpClient
Mention SmtpClient::new_simple in the docs for SmtpClient::new
2019-03-17 11:38:12 +00:00
Alexis Mousset
139eb9e67e Merge pull request #317 from stammw/master
feat(transport-smtp): SMTP connection pool implementation with r2d2
2019-03-17 11:37:45 +00:00
Alexis Mousset
917d34210d Merge pull request #319 from architekton/gmail_example
docs(transport-smtp): Gmail transport simple example
2019-03-17 11:37:03 +00:00
Alexis Mousset
ed2730a0a7 Merge pull request #320 from TyPR124/get-from-from-envelope
Get 'from' from envelope
2019-03-17 11:36:43 +00:00
Alexis Mousset
81d174d7ed Merge pull request #321 from funkill/original_email_crate
Using orginal email dependency
2019-03-17 11:36:23 +00:00
Alexis Mousset
bdb4f78bd3 Merge branch 'master' into original_email_crate 2019-03-17 11:36:15 +00:00
Alexis Mousset
4a76fbb46c Merge pull request #324 from cynede/base64
Update base64 to ^0.10
2019-03-17 11:35:27 +00:00
Alexis Mousset
52535c4554 Merge pull request #328 from Eijebong/unfork
Use the original rust-email instead of the lettre fork
2019-03-17 11:35:17 +00:00
Alexis Mousset
5918803778 Merge pull request #329 from leo-lb/master
fix(smtp-transport): Client::read_response infinite loop
2019-03-17 11:34:43 +00:00
leo-lb
72f3cd8f12 fix(smtp-transport): Client::read_response infinite loop
I have encountered an issue on Gmail where the server returned an error,
and the code was stuck here looping indefinitely.
This commit fixes the issue.
2019-03-12 01:44:04 +01:00
Bastien Orivel
78ba8007cd fix(lettre_email): Use the original rust-email
The fork is 10 commits behind the original repo, I don't see a point in
using it as the rest of the history is the same
2019-02-24 14:30:41 +01:00
Cynede
e202eafb7f feat(transport): Update base64 to ^0.10 2019-02-11 18:46:24 +04:00
funkill2
adbd50a6ce fix(email): Use original "email" crate 2019-01-05 02:07:04 +03:00
Tyler Ruckinger
058fa694f0 fix issue, inserting 'from' from envelope into message headers.
add test case to expect failure when there really is no 'from'
2018-12-28 20:13:17 -05:00
Tyler Ruckinger
f64721702f Add test case 2018-12-28 19:47:48 -05:00
Architekton
a8d8e2ac00 docs(transport-smtp): Gmail transport simple example 2018-11-13 12:50:18 +11:00
Jean-Christophe BEGUE
434654e9af feat(transport-smtp): SMTP connection pool implementation with r2d2 2018-11-09 16:43:44 +01:00
Marcin Mielniczuk
4a77f587c3 docs(transport-smtp): Improve SmtpClient docs.
Mention SmtpClient::new_simple in the docs for SmtpClient::new
2018-10-16 01:44:17 +02:00
Alexis Mousset
c988b1760a Merge pull request #311 from stammw/master
check email validity before creating any new EmailAddress #308
2018-10-03 20:32:23 +02:00
Jean-Christophe BEGUE
e08d4e3ee5 feat(email): check validity for EmailAddress #308 2018-09-22 10:06:45 +02:00
Alexis Mousset
fc91bb6ee8 feat(email): Add In-Reply-To and References headers 2018-09-20 21:45:08 +02:00
Alexis Mousset
ee31bbe9e3 fix(email): Do not include Bcc addresses in headers 2018-09-20 20:20:20 +02:00
Alexis Mousset
0b92881b48 feat(email): Update uuid to 0.7 2018-09-20 20:12:15 +02:00
Alexis Mousset
8bb97e62ca Update nom to 0.4 2018-09-20 20:09:06 +02:00
Alexis Mousset
8ba1c3a3f7 Merge pull request #303 from tyranron/upgrade-to-native-tls-0-2
feat(transport-smtp): Upgrade to 0.2 version of native-tls crate
2018-09-20 19:59:52 +02:00
tyranron
644b1e59b0 feat(transport-smtp): Upgrade to 0.2 version of native-tls crate 2018-07-08 10:40:13 +03:00
Alexis Mousset
e225afbec2 Merge pull request #297 from amousset/code-cleanup
style(transport): Improve code style
2018-05-31 15:49:20 +02:00
Alexis Mousset
22555f0620 style(transport): Improve code style 2018-05-31 15:49:05 +02:00
Alexis Mousset
c09e3ff8cd Merge pull request #296 from amousset/failure
feat(all): Start using the failure crate for errors
2018-05-31 13:15:14 +02:00
Alexis Mousset
c10fe3db84 feat(all): Start using the failure crate for errors 2018-05-31 13:08:02 +02:00
Alexis Mousset
9d14630552 Merge pull request #295 from amousset/submissions
feat(transport): Use submissions port by default
2018-05-31 09:34:42 +02:00
Alexis Mousset
186ad29424 feat(transport): Use submissions port by default 2018-05-31 09:17:08 +02:00
Alexis Mousset
dce8d53310 Merge pull request #293 from amousset/fix-vec-attachment
feat(email): Improve attachment methods
2018-05-31 09:02:14 +02:00
Alexis Mousset
c9c82495ce feat(email): Improve attachment methods 2018-05-31 00:21:14 +02:00
Alexis Mousset
283d45824e Merge pull request #291 from madmaxio/master
Added attachment from a vector of bytes
2018-05-29 23:50:42 +02:00
Alexis Mousset
0ee089fc37 Merge pull request #292 from amousset/clean
style(all): Run rustfmt and clippy
2018-05-28 23:55:28 +02:00
Alexis Mousset
7f545301e1 style(all): Run rustfmt and clippy 2018-05-28 23:46:05 +02:00
Баранов Максим Игоревич
ce932c15d6 Added reference to bytes_vec 2018-05-24 10:31:34 +03:00
Maximb
afc23de20f Added attach_from_vec function to email builder 2018-05-23 14:22:46 +00:00
Alexis Mousset
c52c28ab80 Merge pull request #287 from astonbitecode/master
fix (transport): Sendmail transport: Fix to-list manipulation
2018-05-22 20:56:23 +02:00
aston
4f9067f258 fix (transport): Sendmail transport: Fix to-list manipulation
Use the `to_addresses` Vec as a separate `args` entry.
2018-05-22 10:58:39 +03:00
Alexis Mousset
964b9dc00b Merge pull request #286 from amousset/fix-mime-readme
docs(all): README should not use external crates
2018-05-20 00:06:02 +02:00
Alexis Mousset
866c804ef3 docs(all): README should not use external crates 2018-05-20 00:03:28 +02:00
Alexis Mousset
a7d35325ed Merge pull request #285 from amousset/readme-08
docs(all): README should be compatible with latest stable version
2018-05-19 23:59:41 +02:00
Alexis Mousset
319be26031 docs(all): README should be compatible with latest stable version 2018-05-19 23:53:29 +02:00
Alexis Mousset
3a0b6e1a31 Merge pull request #282 from amousset/add-xoauth2
feat(transport): Initial support for XOAUTH2
2018-05-15 01:18:06 +02:00
Alexis Mousset
ed7c16452c feat(transport): Initial support for XOAUTH2 2018-05-15 01:04:16 +02:00
Alexis Mousset
2e2f614517 Merge pull request #281 from amousset/remove-crammd5
feat(transport): Remove support for CRAM-MD5
2018-05-15 00:53:33 +02:00
Alexis Mousset
bc09aa2185 feat(transport): Remove support for CRAM-MD5
It is obsolete and may give a false sense of security.
We may add a better mechanism later.
2018-05-15 00:35:21 +02:00
Alexis Mousset
bab4519baa Merge pull request #278 from amousset/tls12
feat(transport): Remove TLS 1.1 in accepted protocols by default
2018-05-06 11:39:33 +02:00
Alexis Mousset
1f1359502e Merge pull request #279 from amousset/disable-crammd5
feat(transport): Disable the CRAM-MD5 AUTH feature dy default
2018-05-06 11:39:11 +02:00
Alexis Mousset
5423f53bad feat(transport): Disable the CRAM-MD5 AUTH feature dy default 2018-05-06 11:28:42 +02:00
Alexis Mousset
4b48bdbd9a feat(transport): Remove TLS 1.1 in accepted protocols by default (only allow TLS 1.2) 2018-05-06 11:24:17 +02:00
Alexis Mousset
31442e96d0 Update issue templates 2018-05-05 01:51:42 +02:00
Alexis Mousset
70720d7cdd Add 0.8.2 to changelog 2018-05-04 00:52:04 +02:00
Alexis Mousset
706ed8b4fd Merge branch 'devel' 2018-05-04 00:27:04 +02:00
Alexis Mousset
da63de72fc Build doc from v0.8.x branch 2018-05-04 00:07:54 +02:00
dimlev
d71b560077 fix(transport): Write timeout is not set in smtp transport 2018-05-03 08:52:36 +02:00
Alexis Mousset
917ecbc477 Rename SmtpTransportBuilder to SmtpClient 2018-04-30 01:01:59 +02:00
Alexis Mousset
4a61357205 Rework internal structs 2018-04-29 11:28:41 +02:00
Alexis Mousset
e7e0f3485d feat(transport): Use md-5 and hmac instead of rust-crypto
RustCrypto is not supported anymore, and this avoids
compiling useless code.
2018-04-28 15:04:49 +02:00
Alexis Mousset
33d20c61d1 Merge pull request #269 from amousset/remove-builders
feat(email): Remove non-chaining builder methods
2018-04-27 22:49:44 +02:00
Alexis Mousset
1baf8a9516 feat(email): Remove non-chaining builder methods 2018-04-27 22:40:50 +02:00
Alexis Mousset
5bb4f4f8e7 Merge pull request #267 from amousset/remove-rust-crypto
feat(transport): Use md-5 and hmac instead of rust-crypto
2018-04-17 22:25:59 +02:00
Alexis Mousset
2e56bd6a82 feat(transport): Use md-5 and hmac instead of rust-crypto
RustCrypto is not supported anymore, and this avoids
compiling useless code.
2018-04-17 22:15:50 +02:00
Alexis Mousset
3159981e4a Merge pull request #266 from amousset/result-type-transport
feat(transport): Use an associated type for result type of EmailTrans…
2018-04-17 00:29:35 +02:00
Alexis Mousset
a0c95f748e feat(transport): Use an associated type for result type of EmailTransport 2018-04-17 00:21:43 +02:00
Alexis Mousset
f949dd53ed Update book.json 2018-04-15 19:12:07 +02:00
Alexis Mousset
d990ab4de3 Merge pull request #265 from amousset/clean-buildscripts
feat(all): Add set -xe option to build scripts
2018-04-15 18:57:20 +02:00
Alexis Mousset
c489a0bdc2 feat(all): Add set -xe option to build scripts 2018-04-15 18:49:03 +02:00
Alexis Mousset
9dd08ad4c2 Update .travis.yml 2018-04-15 18:33:00 +02:00
Alexis Mousset
4313207896 Update .travis.yml 2018-04-15 18:05:20 +02:00
Alexis Mousset
944a236aa7 Update .travis.yml 2018-04-15 17:46:40 +02:00
Alexis Mousset
ddd80f5dcd Update .travis.yml 2018-04-15 16:00:14 +02:00
Alexis Mousset
530b595424 Update .travis.yml 2018-04-15 15:54:47 +02:00
Alexis Mousset
f985cf7559 Merge pull request #264 from amousset/use-deploy-script
feat(all): Move post-success scripts to separate files
2018-04-15 15:46:25 +02:00
Alexis Mousset
7b6ac2e677 feat(all): Move post-success scripts to separate files 2018-04-15 15:39:59 +02:00
Alexis Mousset
6797d3d3f5 Merge pull request #263 from amousset/typos
style(all): Fix typos
2018-04-15 13:45:42 +02:00
Alexis Mousset
bffe2978d2 style(all): Fix typos 2018-04-15 13:37:29 +02:00
Alexis Mousset
4d86840bc9 Update .travis.yml 2018-04-15 13:10:29 +02:00
Alexis Mousset
a7e5493aad Update .travis.yml 2018-04-15 13:04:27 +02:00
Alexis Mousset
d6828f5150 Merge pull request #262 from amousset/deploy-website-travis
feat(all): Add website upload to travis build script
2018-04-15 12:54:10 +02:00
Alexis Mousset
0b850e3b2f feat(all): Add website upload to travis build script 2018-04-15 12:52:49 +02:00
Alexis Mousset
57be14112a Remove doc redirect to lettre.at 2018-04-14 16:23:22 +02:00
Alexis Mousset
928fd413a4 Delete CNAME 2018-04-14 16:14:25 +02:00
Alexis Mousset
2d196599c7 Merge pull request #260 from amousset/code-coverage
feat(all): Add codecov upload in travis
2018-04-14 15:56:43 +02:00
Alexis Mousset
ad2fef9bbc feat(all): Add codecov upload in travis 2018-04-14 15:56:08 +02:00
Alexis Mousset
6577aa17d9 Merge pull request #259 from amousset/update-readme
feat(all): Update README to put useful links at the top
2018-04-14 15:18:16 +02:00
Alexis Mousset
34fc101f31 feat(all): Update README to put useful links at the top 2018-04-14 15:10:58 +02:00
Alexis Mousset
91f0cfa27c Merge pull request #258 from amousset/update-changelog
feat(all): Update changelog with 0.8.1
2018-04-14 14:44:20 +02:00
Alexis Mousset
78bd429310 feat(all): Update changelog with 0.8.1 2018-04-14 14:31:01 +02:00
Alexis Mousset
54e3dd3e41 Merge pull request #257 from amousset/update-badges
feat(all): Update badges in README and Cargo.toml
2018-04-14 14:27:32 +02:00
Alexis Mousset
36381eb345 feat(all): Update badges in README and Cargo.toml 2018-04-14 14:15:35 +02:00
Alexis Mousset
8d92dbb0c2 Create CNAME 2018-04-14 13:56:47 +02:00
Alexis Mousset
c4b1100bdb Merge pull request #256 from amousset/move-doc-gitbook
feat(all): Move docs from hugo to gitbook and move generated html to …
2018-04-14 13:24:36 +02:00
Alexis Mousset
b5e2c67dbd feat(all): Move docs from hugo to gitbook
Also move generated html to the lettre.github.io repo
2018-04-14 13:16:26 +02:00
Alexis Mousset
bf3bb78534 Merge pull request #254 from amousset/rename-master-to-0-9
feat(all): Change master version to 0.9.0-pre
2018-04-13 21:23:03 +02:00
Alexis Mousset
a914d9990c feat(all): Change master version to 0.9.0-pre 2018-04-13 20:59:23 +02:00
54 changed files with 1398 additions and 1629 deletions

View File

@@ -4,7 +4,7 @@ set -xe
cd website cd website
make clean && make make clean && make
echo "lettre.at" > _book/CNAME echo "lettre.at" > _book/html/CNAME
sudo pip install ghp-import sudo pip install ghp-import
ghp-import -n _book ghp-import -n _book/html
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages

21
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@@ -0,0 +1,21 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Code allowing to reproduce the bug.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- Lettre version
- OS
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -3,7 +3,7 @@ rust:
- stable - stable
- beta - beta
- nightly - nightly
- 1.20.0 - 1.32.0
matrix: matrix:
allow_failures: allow_failures:
- rust: nightly - rust: nightly
@@ -19,13 +19,11 @@ addons:
- gcc - gcc
- binutils-dev - binutils-dev
- libiberty-dev - libiberty-dev
- npm
before_script: before_script:
- smtp-sink 2525 1000& - smtp-sink 2525 1000&
- sudo chgrp -R postdrop /var/spool/postfix/maildrop - sudo chgrp -R postdrop /var/spool/postfix/maildrop
- sudo npm set strict-ssl false && sudo npm install -g gitbook-cli
script: script:
- cargo test --verbose --all - cargo test --verbose --all --all-features
after_success: after_success:
- ./.build-scripts/codecov.sh - ./.build-scripts/codecov.sh
- '[ "$TRAVIS_BRANCH" = "v0.8.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh' - '[ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_BRANCH" = "v0.9.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'

View File

@@ -1,11 +1,60 @@
<a name="v0.8.4"></a> <a name="v0.9.3"></a>
### v0.8.4 (2020-11-11) ### v0.9.3 (2020-04-19)
#### Bug Fixes #### Bug Fixes
* **transport** * **all:**
* Fix compilation warnings ([9b591ff](https://github.com/lettre/lettre/commit/9b591ff932e35947f793aaaeec0e3f06e8818449))
* **SECURITY**: Prevent argument injection in sendmail transport * **email**
* Update `rust-email` to 0.0.21 ([eff4e169](https://github.com/lettre/lettre/commit/eff4e1693f5e65430b851707fdfd18046bc796e3))
<a name="v0.9.2"></a>
### v0.9.2 (2019-06-11)
#### Bug Fixes
* **email:**
* Fix compilation with Rust 1.36+ ([393ef8d](https://github.com/lettre/lettre/commit/393ef8dcd1b1c6a6119d0666d5f09b12f50f6b4b))
<a name="v0.9.1"></a>
### v0.9.1 (2019-05-05)
#### Features
* **email:**
* Re-export mime crate ([a0c8fb9](https://github.com/lettre/lettre/commit/a0c8fb9))
<a name="v0.9.0"></a>
### v0.9.0 (2019-03-17)
#### Bug Fixes
* **email:**
* Inserting 'from' from envelope into message headers ([058fa69](https://github.com/lettre/lettre/commit/058fa69))
* Do not include Bcc addresses in headers ([ee31bbe](https://github.com/lettre/lettre/commit/ee31bbe))
* **transport:**
* Write timeout is not set in smtp transport ([d71b560](https://github.com/lettre/lettre/commit/d71b560))
* Client::read_response infinite loop ([72f3cd8](https://github.com/lettre/lettre/commit/72f3cd8))
#### Features
* **all:**
* Update dependencies
* Start using the failure crate for errors ([c10fe3d](https://github.com/lettre/lettre/commit/c10fe3d))
* **transport:**
* Remove TLS 1.1 in accepted protocols by default (only allow TLS 1.2) ([4b48bdb](https://github.com/lettre/lettre/commit/4b48bdb))
* Initial support for XOAUTH2 ([ed7c164](https://github.com/lettre/lettre/commit/ed7c164))
* Remove support for CRAM-MD5 ([bc09aa2](https://github.com/lettre/lettre/commit/bc09aa2))
* SMTP connection pool implementation with r2d2 ([434654e](https://github.com/lettre/lettre/commit/434654e))
* Use md-5 and hmac instead of rust-crypto ([e7e0f34](https://github.com/lettre/lettre/commit/e7e0f34))
* Gmail transport simple example ([a8d8e2a](https://github.com/lettre/lettre/commit/a8d8e2a))
* **email:**
* Add In-Reply-To and References headers ([fc91bb6](https://github.com/lettre/lettre/commit/fc91bb6))
* Remove non-chaining builder methods ([1baf8a9](https://github.com/lettre/lettre/commit/1baf8a9))
<a name="v0.8.2"></a> <a name="v0.8.2"></a>
### v0.8.2 (2018-05-03) ### v0.8.2 (2018-05-03)

View File

@@ -34,13 +34,13 @@ Lettre provides the following features:
## Example ## Example
This library requires Rust 1.20 or newer. This library requires Rust 1.32 or newer.
To use this library, add the following to your `Cargo.toml`: To use this library, add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
lettre = "0.8" lettre = "0.9"
lettre_email = "0.8" lettre_email = "0.9"
``` ```
```rust,no_run ```rust,no_run

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre" name = "lettre"
version = "0.8.4" # remember to update html_root_url version = "0.9.3" # remember to update html_root_url
description = "Email client" description = "Email client"
readme = "README.md" readme = "README.md"
homepage = "http://lettre.at" homepage = "http://lettre.at"
@@ -22,29 +22,32 @@ is-it-maintained-open-issues = { repository = "lettre/lettre" }
log = "^0.4" log = "^0.4"
nom = { version = "^4.0", optional = true } nom = { version = "^4.0", optional = true }
bufstream = { version = "^0.1", optional = true } bufstream = { version = "^0.1", optional = true }
native-tls = { version = "^0.1", optional = true } native-tls = { version = "^0.2", optional = true }
base64 = { version = "^0.9", optional = true } base64 = { version = "^0.10", optional = true }
hex = { version = "^0.3", optional = true }
hostname = { version = "^0.1", optional = true } hostname = { version = "^0.1", optional = true }
md-5 = { version = "^0.7", optional = true }
hmac = { version = "^0.6", optional = true }
serde = { version = "^1.0", optional = true } serde = { version = "^1.0", optional = true }
serde_json = { version = "^1.0", optional = true } serde_json = { version = "^1.0", optional = true }
serde_derive = { version = "^1.0", optional = true } serde_derive = { version = "^1.0", optional = true }
fast_chemail = "^0.9"
r2d2 = { version = "^0.8", optional = true}
[dev-dependencies] [dev-dependencies]
env_logger = "^0.5" env_logger = "^0.6"
glob = "0.2" glob = "0.3"
[features] [features]
default = ["file-transport", "crammd5-auth", "smtp-transport", "sendmail-transport"] default = ["file-transport", "smtp-transport", "sendmail-transport"]
unstable = [] unstable = []
serde-impls = ["serde", "serde_derive"] serde-impls = ["serde", "serde_derive"]
file-transport = ["serde-impls", "serde_json"] file-transport = ["serde-impls", "serde_json"]
crammd5-auth = ["md-5", "hmac", "hex"]
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"] smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
sendmail-transport = [] sendmail-transport = []
connection-pool = [ "r2d2" ]
[[example]] [[example]]
name = "smtp" name = "smtp"
required-features = ["smtp-transport"] required-features = ["smtp-transport"]
[[example]]
name = "smtp_gmail"
required-features = ["smtp-transport"]

View File

@@ -3,9 +3,9 @@
extern crate lettre; extern crate lettre;
extern crate test; extern crate test;
use lettre::{ClientSecurity, SmtpTransport};
use lettre::{EmailAddress, EmailTransport, SimpleSendableEmail};
use lettre::smtp::ConnectionReuseParameters; use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, Envelope, SmtpTransport};
use lettre::{EmailAddress, SendableEmail, Transport};
#[bench] #[bench]
fn bench_simple_send(b: &mut test::Bencher) { fn bench_simple_send(b: &mut test::Bencher) {
@@ -13,13 +13,16 @@ fn bench_simple_send(b: &mut test::Bencher) {
.unwrap() .unwrap()
.build(); .build();
b.iter(|| { b.iter(|| {
let email = SimpleSendableEmail::new( let email = SendableEmail::new(
EmailAddress::new("user@localhost".to_string()), Envelope::new(
vec![EmailAddress::new("root@localhost".to_string())], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(), "id".to_string(),
"Hello world".to_string(), "Hello ß☺ example".to_string().into_bytes(),
); );
let result = sender.send(&email); let result = sender.send(email);
assert!(result.is_ok()); assert!(result.is_ok());
}); });
} }
@@ -31,13 +34,16 @@ fn bench_reuse_send(b: &mut test::Bencher) {
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited) .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.build(); .build();
b.iter(|| { b.iter(|| {
let email = SimpleSendableEmail::new( let email = SendableEmail::new(
EmailAddress::new("user@localhost".to_string()), Envelope::new(
vec![EmailAddress::new("root@localhost".to_string())], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(), "id".to_string(),
"Hello world".to_string(), "Hello ß☺ example".to_string().into_bytes(),
); );
let result = sender.send(&email); let result = sender.send(email);
assert!(result.is_ok()); assert!(result.is_ok());
}); });
sender.close() sender.close()

View File

@@ -1,24 +1,25 @@
extern crate env_logger; extern crate env_logger;
extern crate lettre; extern crate lettre;
use lettre::{EmailTransport, SimpleSendableEmail, SmtpTransport}; use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
fn main() { fn main() {
env_logger::init(); env_logger::init();
let email = SimpleSendableEmail::new( let email = SendableEmail::new(
"user@localhost".to_string(), Envelope::new(
&["root@localhost".to_string()], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
"my-message-id".to_string(), vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"Hello ß☺ example".to_string(), )
).unwrap(); .unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
// Open a local connection on port 25 // Open a local connection on port 25
let mut mailer = SmtpTransport::builder_unencrypted_localhost() let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
.unwrap()
.build();
// Send the email // Send the email
let result = mailer.send(&email); let result = mailer.send(email);
if result.is_ok() { if result.is_ok() {
println!("Email sent"); println!("Email sent");

View File

@@ -0,0 +1,38 @@
extern crate lettre;
use lettre::smtp::authentication::Credentials;
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("from@gmail.com".to_string()).unwrap()),
vec![EmailAddress::new("to@example.com".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello example".to_string().into_bytes(),
);
let creds = Credentials::new(
"example_username".to_string(),
"example_password".to_string(),
);
// Open a remote connection to gmail
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
.unwrap()
.credentials(creds)
.transport();
// Send the email
let result = mailer.send(email);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

35
lettre/src/error.rs Normal file
View File

@@ -0,0 +1,35 @@
use self::Error::*;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// Error type for email content
#[derive(Debug, Clone, Copy)]
pub enum Error {
/// Missing from in envelope
MissingFrom,
/// Missing to in envelope
MissingTo,
/// Invalid email
InvalidEmailAddress,
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str(&match *self {
MissingFrom => "missing source address, invalid envelope".to_owned(),
MissingTo => "missing destination address, invalid envelope".to_owned(),
InvalidEmailAddress => "invalid email address".to_owned(),
})
}
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
None
}
}
/// Email result type
pub type EmailResult<T> = Result<T, Error>;

View File

@@ -2,9 +2,11 @@
use self::Error::*; use self::Error::*;
use serde_json; use serde_json;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io; use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds. /// An enum of all error kinds.
#[derive(Debug)] #[derive(Debug)]
@@ -19,20 +21,16 @@ pub enum Error {
impl Display for Error { impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description()) match *self {
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
JsonSerialization(ref err) => err.fmt(fmt),
}
} }
} }
impl StdError for Error { impl StdError for Error {
fn description(&self) -> &str { fn cause(&self) -> Option<&dyn StdError> {
match *self {
Client(err) => err,
Io(ref err) => err.description(),
JsonSerialization(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
match *self { match *self {
Io(ref err) => Some(&*err), Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err), JsonSerialization(ref err) => Some(&*err),
@@ -43,19 +41,19 @@ impl StdError for Error {
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(err: io::Error) -> Error { fn from(err: io::Error) -> Error {
Io(err) Error::Io(err)
} }
} }
impl From<serde_json::Error> for Error { impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error { fn from(err: serde_json::Error) -> Error {
JsonSerialization(err) Error::JsonSerialization(err)
} }
} }
impl From<&'static str> for Error { impl From<&'static str> for Error {
fn from(string: &'static str) -> Error { fn from(string: &'static str) -> Error {
Client(string) Error::Client(string)
} }
} }

View File

@@ -3,52 +3,58 @@
//! It can be useful for testing purposes, or if you want to keep track of sent messages. //! It can be useful for testing purposes, or if you want to keep track of sent messages.
//! //!
use EmailTransport;
use SendableEmail;
use SimpleSendableEmail;
use file::error::FileResult; use file::error::FileResult;
use serde_json; use serde_json;
use std::fs::File; use std::fs::File;
use std::io::Read;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use Envelope;
use SendableEmail;
use Transport;
pub mod error; pub mod error;
/// Writes the content and the envelope information to a file /// Writes the content and the envelope information to a file
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct FileEmailTransport { pub struct FileTransport {
path: PathBuf, path: PathBuf,
} }
impl FileEmailTransport { impl FileTransport {
/// Creates a new transport to the given directory /// Creates a new transport to the given directory
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport { pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
let mut path_buf = PathBuf::new(); FileTransport {
path_buf.push(path); path: PathBuf::from(path.as_ref()),
FileEmailTransport { path: path_buf } }
} }
} }
impl<'a, T: Read + 'a> EmailTransport<'a, T, FileResult> for FileEmailTransport { #[derive(PartialEq, Eq, Clone, Debug)]
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> FileResult { #[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
struct SerializableEmail {
envelope: Envelope,
message_id: String,
message: Vec<u8>,
}
impl<'a> Transport<'a> for FileTransport {
type Result = FileResult;
fn send(&mut self, email: SendableEmail) -> FileResult {
let message_id = email.message_id().to_string();
let envelope = email.envelope().clone();
let mut file = self.path.clone(); let mut file = self.path.clone();
file.push(format!("{}.txt", email.message_id())); file.push(format!("{}.json", message_id));
let mut f = File::create(file.as_path())?; let serialized = serde_json::to_string(&SerializableEmail {
envelope,
let mut message_content = String::new(); message_id,
let _ = email.message().read_to_string(&mut message_content); message: email.message_to_string()?.as_bytes().to_vec(),
})?;
let simple_email = SimpleSendableEmail::new_with_envelope(
email.envelope().clone(),
email.message_id().to_string(),
message_content,
);
f.write_all(serde_json::to_string(&simple_email)?.as_bytes())?;
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
Ok(()) Ok(())
} }
} }

View File

@@ -1,102 +1,80 @@
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports. //! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
//! //!
//! This mailer contains the available transports for your emails. To be sendable, the //! This mailer contains the available transports for your emails.
//! emails have to implement `SendableEmail`.
//! //!
#![doc(html_root_url = "https://docs.rs/lettre/0.8.4")] #![doc(html_root_url = "https://docs.rs/lettre/0.9.3")]
#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts, #![deny(
trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces, missing_copy_implementations,
unused_qualifications)] trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces
)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
extern crate base64; extern crate base64;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
extern crate bufstream; extern crate bufstream;
#[cfg(feature = "crammd5-auth")]
extern crate hex;
#[cfg(feature = "crammd5-auth")]
extern crate hmac;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
extern crate hostname; extern crate hostname;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
#[cfg(feature = "crammd5-auth")]
extern crate md5;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
extern crate native_tls; extern crate native_tls;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
#[macro_use] #[macro_use]
extern crate nom; extern crate nom;
#[cfg(feature = "serde-impls")] #[cfg(feature = "serde-impls")]
extern crate serde;
#[cfg(feature = "serde-impls")]
#[macro_use] #[macro_use]
extern crate serde_derive; extern crate serde_derive;
extern crate fast_chemail;
#[cfg(feature = "connection-pool")]
extern crate r2d2;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
extern crate serde_json; extern crate serde_json;
#[cfg(feature = "smtp-transport")] pub mod error;
pub mod smtp;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
pub mod stub;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
pub mod file; pub mod file;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
#[cfg(feature = "smtp-transport")]
pub mod smtp;
pub mod stub;
use error::EmailResult;
use error::Error;
use fast_chemail::is_valid_email;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
pub use file::FileEmailTransport; pub use file::FileTransport;
#[cfg(feature = "sendmail-transport")] #[cfg(feature = "sendmail-transport")]
pub use sendmail::SendmailTransport; pub use sendmail::SendmailTransport;
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
pub use smtp::{ClientSecurity, SmtpTransport};
#[cfg(feature = "smtp-transport")]
pub use smtp::client::net::ClientTlsParameters; pub use smtp::client::net::ClientTlsParameters;
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
pub use smtp::r2d2::SmtpConnectionManager;
#[cfg(feature = "smtp-transport")]
pub use smtp::{ClientSecurity, SmtpClient, SmtpTransport};
use std::ffi::OsStr;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::io;
use std::io::Cursor;
use std::io::Read; use std::io::Read;
use std::error::Error as StdError;
use std::str::FromStr; use std::str::FromStr;
/// Error type for email content
#[derive(Debug, Clone, Copy)]
pub enum Error {
/// Missing from in envelope
MissingFrom,
/// Missing to in envelope
MissingTo,
/// Invalid email
InvalidEmailAddress,
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Error::MissingFrom => "missing source address, invalid envelope",
Error::MissingTo => "missing destination address, invalid envelope",
Error::InvalidEmailAddress => "invalid email address",
}
}
fn cause(&self) -> Option<&StdError> {
None
}
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
/// Email result type
pub type EmailResult<T> = Result<T, Error>;
/// Email address /// Email address
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EmailAddress(String); pub struct EmailAddress(String);
impl EmailAddress { impl EmailAddress {
/// Creates a new `EmailAddress`. For now it makes no validation.
pub fn new(address: String) -> EmailResult<EmailAddress> { pub fn new(address: String) -> EmailResult<EmailAddress> {
// TODO make some basic sanity checks if !is_valid_email(&address) && !address.ends_with("localhost") {
return Err(Error::InvalidEmailAddress);
}
Ok(EmailAddress(address)) Ok(EmailAddress(address))
} }
} }
@@ -115,6 +93,18 @@ impl Display for EmailAddress {
} }
} }
impl AsRef<str> for EmailAddress {
fn as_ref(&self) -> &str {
&self.0
}
}
impl AsRef<OsStr> for EmailAddress {
fn as_ref(&self) -> &OsStr {
&self.0.as_ref()
}
}
/// Simple email envelope representation /// Simple email envelope representation
/// ///
/// We only accept mailboxes, and do not support source routes (as per RFC). /// We only accept mailboxes, and do not support source routes (as per RFC).
@@ -150,130 +140,74 @@ impl Envelope {
pub fn from(&self) -> Option<&EmailAddress> { pub fn from(&self) -> Option<&EmailAddress> {
self.reverse_path.as_ref() self.reverse_path.as_ref()
} }
}
/// Creates a new builder pub enum Message {
pub fn builder() -> EnvelopeBuilder { Reader(Box<dyn Read + Send>),
EnvelopeBuilder::new() Bytes(Cursor<Vec<u8>>),
}
impl Read for Message {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
Message::Reader(ref mut rdr) => rdr.read(buf),
Message::Bytes(ref mut rdr) => rdr.read(buf),
}
} }
} }
/// Simple email envelope representation /// Sendable email structure
#[derive(PartialEq, Eq, Clone, Debug, Default)] pub struct SendableEmail {
pub struct EnvelopeBuilder { envelope: Envelope,
/// The envelope recipients' addresses message_id: String,
to: Vec<EmailAddress>, message: Message,
/// The envelope sender address
from: Option<EmailAddress>,
} }
impl EnvelopeBuilder { impl SendableEmail {
/// Constructs an envelope with no recipients and an empty sender pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
pub fn new() -> Self { SendableEmail {
EnvelopeBuilder { envelope,
to: vec![], message_id,
from: None, message: Message::Bytes(Cursor::new(message)),
} }
} }
/// Adds a recipient pub fn new_with_reader(
pub fn to<S: Into<EmailAddress>>(mut self, address: S) -> Self { envelope: Envelope,
self.add_to(address); message_id: String,
self message: Box<dyn Read + Send>,
) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Reader(message),
}
} }
/// Adds a recipient pub fn envelope(&self) -> &Envelope {
pub fn add_to<S: Into<EmailAddress>>(&mut self, address: S) { &self.envelope
self.to.push(address.into());
} }
/// Sets the sender pub fn message_id(&self) -> &str {
pub fn from<S: Into<EmailAddress>>(mut self, address: S) -> Self { &self.message_id
self.set_from(address);
self
} }
/// Sets the sender pub fn message(self) -> Message {
pub fn set_from<S: Into<EmailAddress>>(&mut self, address: S) { self.message
self.from = Some(address.into());
} }
/// Build the envelope pub fn message_to_string(mut self) -> Result<String, io::Error> {
pub fn build(self) -> EmailResult<Envelope> { let mut message_content = String::new();
Envelope::new(self.from, self.to) self.message.read_to_string(&mut message_content)?;
Ok(message_content)
} }
} }
/// Email sendable by an SMTP client
pub trait SendableEmail<'a, T: Read + 'a> {
/// Envelope
fn envelope(&self) -> Envelope;
/// Message ID, used for logging
fn message_id(&self) -> String;
/// Message content
fn message(&'a self) -> Box<T>;
}
/// Transport method for emails /// Transport method for emails
pub trait EmailTransport<'a, U: Read + 'a, V> { pub trait Transport<'a> {
/// Result type for the transport
type Result;
/// Sends the email /// Sends the email
fn send<T: SendableEmail<'a, U> + 'a>(&mut self, email: &'a T) -> V; fn send(&mut self, email: SendableEmail) -> Self::Result;
}
/// Minimal email structure
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct SimpleSendableEmail {
/// Envelope
envelope: Envelope,
/// Message ID
message_id: String,
/// Message content
message: Vec<u8>,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(
from_address: String,
to_addresses: &[String],
message_id: String,
message: String,
) -> EmailResult<SimpleSendableEmail> {
let to: Result<Vec<EmailAddress>, Error> = to_addresses
.iter()
.map(|x| EmailAddress::new(x.clone()))
.collect();
Ok(SimpleSendableEmail::new_with_envelope(
Envelope::new(Some(EmailAddress::new(from_address)?), to?)?,
message_id,
message,
))
}
/// Returns a new email from a valid envelope
pub fn new_with_envelope(
envelope: Envelope,
message_id: String,
message: String,
) -> SimpleSendableEmail {
SimpleSendableEmail {
envelope,
message_id,
message: message.into_bytes(),
}
}
}
impl<'a> SendableEmail<'a, &'a [u8]> for SimpleSendableEmail {
fn envelope(&self) -> Envelope {
self.envelope.clone()
}
fn message_id(&self) -> String {
self.message_id.clone()
}
fn message(&'a self) -> Box<&[u8]> {
Box::new(self.message.as_slice())
}
} }

View File

@@ -1,9 +1,11 @@
//! Error and result type for sendmail transport //! Error and result type for sendmail transport
use self::Error::*; use self::Error::*;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io; use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds. /// An enum of all error kinds.
#[derive(Debug)] #[derive(Debug)]
@@ -16,19 +18,15 @@ pub enum Error {
impl Display for Error { impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description()) match *self {
Client(ref err) => err.fmt(fmt),
Io(ref err) => err.fmt(fmt),
}
} }
} }
impl StdError for Error { impl StdError for Error {
fn description(&self) -> &str { fn cause(&self) -> Option<&dyn StdError> {
match *self {
Client(err) => err,
Io(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
match *self { match *self {
Io(ref err) => Some(&*err), Io(ref err) => Some(&*err),
_ => None, _ => None,
@@ -38,13 +36,13 @@ impl StdError for Error {
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(err: io::Error) -> Error { fn from(err: io::Error) -> Error {
Io(err) Error::Io(err)
} }
} }
impl From<&'static str> for Error { impl From<&'static str> for Error {
fn from(string: &'static str) -> Error { fn from(string: &'static str) -> Error {
Client(string) Error::Client(string)
} }
} }

View File

@@ -1,11 +1,12 @@
//! The sendmail transport sends the email using the local sendmail command. //! The sendmail transport sends the email using the local sendmail command.
//! //!
use {EmailTransport, SendableEmail};
use sendmail::error::SendmailResult; use sendmail::error::SendmailResult;
use std::io::Read;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::Read;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use SendableEmail;
use Transport;
pub mod error; pub mod error;
@@ -32,23 +33,24 @@ impl SendmailTransport {
} }
} }
impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTransport { impl<'a> Transport<'a> for SendmailTransport {
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SendmailResult { type Result = SendmailResult;
let envelope = email.envelope();
fn send(&mut self, email: SendableEmail) -> SendmailResult {
let message_id = email.message_id().to_string();
// Spawn the sendmail command // Spawn the sendmail command
let to_addresses: Vec<String> = envelope.to().iter().map(|x| x.to_string()).collect();
let mut process = Command::new(&self.command) let mut process = Command::new(&self.command)
.args(&[ .arg("-i")
"-i", .arg("-f")
"-f", .arg(
&match envelope.from() { email
Some(address) => address.to_string(), .envelope()
None => "\"\"".to_string(), .from()
}, .map(|x| x.as_ref())
"--", .unwrap_or("\"\""),
&to_addresses.join(" "), )
]) .args(email.envelope.to())
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.spawn()?; .spawn()?;
@@ -56,26 +58,21 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTranspo
let mut message_content = String::new(); let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content); let _ = email.message().read_to_string(&mut message_content);
match process process
.stdin .stdin
.as_mut() .as_mut()
.unwrap() .unwrap()
.write_all(message_content.as_bytes()) .write_all(message_content.as_bytes())?;
{
Ok(_) => (),
Err(error) => return Err(From::from(error)),
}
info!("Wrote message to stdin"); info!("Wrote {} message to stdin", message_id);
if let Ok(output) = process.wait_with_output() { let output = process.wait_with_output()?;
if output.status.success() {
Ok(()) if output.status.success() {
} else { Ok(())
Err(From::from("The message could not be sent"))
}
} else { } else {
Err(From::from("The sendmail process stopped")) // TODO display stderr
Err(error::Error::Client("The message could not be sent"))
} }
} }
} }

View File

@@ -1,31 +1,13 @@
//! Provides authentication mechanisms //! Provides limited SASL authentication mechanisms
#[cfg(feature = "crammd5-auth")]
use md5::Md5;
#[cfg(feature = "crammd5-auth")]
use hmac::{Hmac, Mac};
#[cfg(feature = "crammd5-auth")]
use hex;
use smtp::NUL;
use smtp::error::Error; use smtp::error::Error;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
/// Accepted authentication mechanisms on an encrypted connection /// Accepted authentication mechanisms on an encrypted connection
/// Trying LOGIN last as it is deprecated. /// Trying LOGIN last as it is deprecated.
#[cfg(feature = "crammd5-auth")]
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] =
&[Mechanism::Plain, Mechanism::CramMd5, Mechanism::Login];
/// Accepted authentication mechanisms on an encrypted connection
/// Trying LOGIN last as it is deprecated.
#[cfg(not(feature = "crammd5-auth"))]
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login]; pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Accepted authentication mechanisms on an unencrypted connection /// Accepted authentication mechanisms on an unencrypted connection
#[cfg(feature = "crammd5-auth")]
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::CramMd5];
/// Accepted authentication mechanisms on an unencrypted connection
/// When CRAMMD5 support is not enabled, no mechanisms are allowed.
#[cfg(not(feature = "crammd5-auth"))]
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[]; pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
/// Convertable to user credentials /// Convertable to user credentials
@@ -51,14 +33,17 @@ impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
#[derive(PartialEq, Eq, Clone, Hash, Debug)] #[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Credentials { pub struct Credentials {
username: String, authentication_identity: String,
password: String, secret: String,
} }
impl Credentials { impl Credentials {
/// Create a `Credentials` struct from username and password /// Create a `Credentials` struct from username and password
pub fn new(username: String, password: String) -> Credentials { pub fn new(username: String, password: String) -> Credentials {
Credentials { username, password } Credentials {
authentication_identity: username,
secret: password,
}
} }
} }
@@ -73,10 +58,9 @@ pub enum Mechanism {
/// Obsolete but needed for some providers (like office365) /// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt /// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login, Login,
/// CRAM-MD5 authentication mechanism /// Non-standard XOAUTH2 mechanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195 /// https://developers.google.com/gmail/imap/xoauth2-protocol
#[cfg(feature = "crammd5-auth")] Xoauth2,
CramMd5,
} }
impl Display for Mechanism { impl Display for Mechanism {
@@ -87,8 +71,7 @@ impl Display for Mechanism {
match *self { match *self {
Mechanism::Plain => "PLAIN", Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN", Mechanism::Login => "LOGIN",
#[cfg(feature = "crammd5-auth")] Mechanism::Xoauth2 => "XOAUTH2",
Mechanism::CramMd5 => "CRAM-MD5",
} }
) )
} }
@@ -96,64 +79,49 @@ impl Display for Mechanism {
impl Mechanism { impl Mechanism {
/// Does the mechanism supports initial response /// Does the mechanism supports initial response
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] pub fn supports_initial_response(self) -> bool {
pub fn supports_initial_response(&self) -> bool { match self {
match *self { Mechanism::Plain | Mechanism::Xoauth2 => true,
Mechanism::Plain => true,
Mechanism::Login => false, Mechanism::Login => false,
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => false,
} }
} }
/// Returns the string to send to the server, using the provided username, password and /// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases /// challenge in some cases
pub fn response( pub fn response(
&self, self,
credentials: &Credentials, credentials: &Credentials,
challenge: Option<&str>, challenge: Option<&str>,
) -> Result<String, Error> { ) -> Result<String, Error> {
match *self { match self {
Mechanism::Plain => match challenge { Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")), Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!( None => Ok(format!(
"{}{}{}{}", "\u{0}{}\u{0}{}",
NUL, credentials.username, NUL, credentials.password credentials.authentication_identity, credentials.secret
)), )),
}, },
Mechanism::Login => { Mechanism::Login => {
let decoded_challenge = match challenge { let decoded_challenge =
Some(challenge) => challenge, challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) { if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(credentials.username.to_string()); return Ok(credentials.authentication_identity.to_string());
} }
if vec!["Password", "Password:"].contains(&decoded_challenge) { if vec!["Password", "Password:"].contains(&decoded_challenge) {
return Ok(credentials.password.to_string()); return Ok(credentials.secret.to_string());
} }
Err(Error::Client("Unrecognized challenge")) Err(Error::Client("Unrecognized challenge"))
} }
#[cfg(feature = "crammd5-auth")] Mechanism::Xoauth2 => match challenge {
Mechanism::CramMd5 => { Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
let decoded_challenge = match challenge { None => Ok(format!(
Some(challenge) => challenge, "user={}\x01auth=Bearer {}\x01\x01",
None => return Err(Error::Client("This mechanism does expect a challenge")), credentials.authentication_identity, credentials.secret
}; )),
},
let mut hmac: Hmac<Md5> = Hmac::new_varkey(credentials.password.as_bytes())
.expect("md5 should support variable key size");
hmac.input(decoded_challenge.as_bytes());
Ok(format!(
"{} {}",
credentials.username,
hex::encode(hmac.result().code())
))
}
} }
} }
} }
@@ -193,21 +161,18 @@ mod test {
} }
#[test] #[test]
#[cfg(feature = "crammd5-auth")] fn test_xoauth2() {
fn test_cram_md5() { let mechanism = Mechanism::Xoauth2;
let mechanism = Mechanism::CramMd5;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string()); let credentials = Credentials::new(
"username".to_string(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
);
assert_eq!( assert_eq!(
mechanism mechanism.response(&credentials, None).unwrap(),
.response( "user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
&credentials,
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")
)
.unwrap(),
"alice a540ebe4ef2304070bbc3c456c1f64c0"
); );
assert!(mechanism.response(&credentials, None).is_err()); assert!(mechanism.response(&credentials, Some("test")).is_err());
} }
} }

View File

@@ -2,7 +2,6 @@
use bufstream::BufStream; use bufstream::BufStream;
use nom::ErrorKind as NomErrorKind; use nom::ErrorKind as NomErrorKind;
use smtp::{CRLF, MESSAGE_ENDING};
use smtp::authentication::{Credentials, Mechanism}; use smtp::authentication::{Credentials, Mechanism};
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout}; use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
use smtp::commands::*; use smtp::commands::*;
@@ -14,8 +13,8 @@ use std::net::ToSocketAddrs;
use std::string::String; use std::string::String;
use std::time::Duration; use std::time::Duration;
pub mod net;
pub mod mock; pub mod mock;
pub mod net;
/// The codec used for transparency /// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)] #[derive(Default, Clone, Copy, Debug)]
@@ -72,12 +71,12 @@ impl ClientCodec {
/// Returns the string replacing all the CRLF with "\<CRLF\>" /// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays /// Used for debug displays
fn escape_crlf(string: &str) -> String { fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CRLF>") string.replace("\r\n", "<CRLF>")
} }
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Client<S: Write + Read = NetworkStream> { pub struct InnerClient<S: Write + Read = NetworkStream> {
/// TCP stream between client and server /// TCP stream between client and server
/// Value is None before connection /// Value is None before connection
stream: Option<BufStream<S>>, stream: Option<BufStream<S>>,
@@ -89,17 +88,17 @@ macro_rules! return_err (
}) })
); );
#[cfg_attr(feature = "cargo-clippy", allow(new_without_default_derive))] #[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
impl<S: Write + Read> Client<S> { impl<S: Write + Read> InnerClient<S> {
/// Creates a new SMTP client /// Creates a new SMTP client
/// ///
/// It does not connects to the server, but only creates the `Client` /// It does not connects to the server, but only creates the `Client`
pub fn new() -> Client<S> { pub fn new() -> InnerClient<S> {
Client { stream: None } InnerClient { stream: None }
} }
} }
impl<S: Connector + Write + Read + Timeout + Debug> Client<S> { impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
/// Closes the SMTP transaction if possible /// Closes the SMTP transaction if possible
pub fn close(&mut self) { pub fn close(&mut self) {
let _ = self.command(QuitCommand); let _ = self.command(QuitCommand);
@@ -166,9 +165,9 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
} }
/// Checks if the server is connected using the NOOP SMTP command /// Checks if the server is connected using the NOOP SMTP command
#[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] #[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
pub fn is_connected(&mut self) -> bool { pub fn is_connected(&mut self) -> bool {
self.command(NoopCommand).is_ok() self.stream.is_some() && self.command(NoopCommand).is_ok()
} }
/// Sends an AUTH command with the given mechanism, and handles challenge if needed /// Sends an AUTH command with the given mechanism, and handles challenge if needed
@@ -194,10 +193,11 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
} }
/// Sends the message content /// Sends the message content
pub fn message<T: Read>(&mut self, mut message: Box<T>) -> SmtpResult { pub fn message(&mut self, message: Box<dyn Read>) -> SmtpResult {
let mut out_buf: Vec<u8> = vec![]; let mut out_buf: Vec<u8> = vec![];
let mut codec = ClientCodec::new(); let mut codec = ClientCodec::new();
let mut message_reader = BufReader::new(message.as_mut());
let mut message_reader = BufReader::new(message);
loop { loop {
out_buf.clear(); out_buf.clear();
@@ -218,7 +218,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
self.write(out_buf.as_slice())?; self.write(out_buf.as_slice())?;
} }
self.write(MESSAGE_ENDING.as_bytes())?; self.write(b"\r\n.\r\n")?;
self.read_response() self.read_response()
} }
@@ -254,7 +254,13 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
break; break;
} }
// TODO read more than one line // TODO read more than one line
self.stream.as_mut().unwrap().read_line(&mut raw_response)?; let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
// EOF is reached
if read_count == 0 {
break;
}
response = raw_response.parse::<Response>(); response = raw_response.parse::<Response>();
} }

View File

@@ -24,8 +24,8 @@ impl ClientTlsParameters {
} }
/// Accepted protocols by default. /// Accepted protocols by default.
/// This removes TLS 1.0 compared to tls-native defaults. /// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv11, Protocol::Tlsv12]; pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv12];
#[derive(Debug)] #[derive(Debug)]
/// Represents the different types of underlying network streams /// Represents the different types of underlying network streams
@@ -117,7 +117,7 @@ impl Connector for NetworkStream {
} }
} }
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] #[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> { fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
*self = match *self { *self = match *self {
NetworkStream::Tcp(ref mut stream) => match tls_parameters NetworkStream::Tcp(ref mut stream) => match tls_parameters
@@ -134,7 +134,7 @@ impl Connector for NetworkStream {
Ok(()) Ok(())
} }
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] #[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
fn is_encrypted(&self) -> bool { fn is_encrypted(&self) -> bool {
match *self { match *self {
NetworkStream::Tcp(_) => false, NetworkStream::Tcp(_) => false,

View File

@@ -1,14 +1,15 @@
#![cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
//! SMTP commands //! SMTP commands
use EmailAddress;
use base64; use base64;
use smtp::CRLF;
use smtp::authentication::{Credentials, Mechanism}; use smtp::authentication::{Credentials, Mechanism};
use smtp::error::Error; use smtp::error::Error;
use smtp::extension::{MailParameter, RcptParameter};
use smtp::extension::ClientId; use smtp::extension::ClientId;
use smtp::extension::{MailParameter, RcptParameter};
use smtp::response::Response; use smtp::response::Response;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Display, Formatter};
use EmailAddress;
/// EHLO command /// EHLO command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
@@ -19,8 +20,7 @@ pub struct EhloCommand {
impl Display for EhloCommand { impl Display for EhloCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EHLO {}", self.client_id)?; write!(f, "EHLO {}\r\n", self.client_id)
f.write_str(CRLF)
} }
} }
@@ -38,8 +38,7 @@ pub struct StarttlsCommand;
impl Display for StarttlsCommand { impl Display for StarttlsCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("STARTTLS")?; f.write_str("STARTTLS\r\n")
f.write_str(CRLF)
} }
} }
@@ -56,15 +55,12 @@ impl Display for MailCommand {
write!( write!(
f, f,
"MAIL FROM:<{}>", "MAIL FROM:<{}>",
match self.sender { self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
Some(ref address) => address.to_string(),
None => "".to_string(),
}
)?; )?;
for parameter in &self.parameters { for parameter in &self.parameters {
write!(f, " {}", parameter)?; write!(f, " {}", parameter)?;
} }
f.write_str(CRLF) f.write_str("\r\n")
} }
} }
@@ -89,7 +85,7 @@ impl Display for RcptCommand {
for parameter in &self.parameters { for parameter in &self.parameters {
write!(f, " {}", parameter)?; write!(f, " {}", parameter)?;
} }
f.write_str(CRLF) f.write_str("\r\n")
} }
} }
@@ -110,8 +106,7 @@ pub struct DataCommand;
impl Display for DataCommand { impl Display for DataCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("DATA")?; f.write_str("DATA\r\n")
f.write_str(CRLF)
} }
} }
@@ -122,8 +117,7 @@ pub struct QuitCommand;
impl Display for QuitCommand { impl Display for QuitCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("QUIT")?; f.write_str("QUIT\r\n")
f.write_str(CRLF)
} }
} }
@@ -134,8 +128,7 @@ pub struct NoopCommand;
impl Display for NoopCommand { impl Display for NoopCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NOOP")?; f.write_str("NOOP\r\n")
f.write_str(CRLF)
} }
} }
@@ -152,7 +145,7 @@ impl Display for HelpCommand {
if self.argument.is_some() { if self.argument.is_some() {
write!(f, " {}", self.argument.as_ref().unwrap())?; write!(f, " {}", self.argument.as_ref().unwrap())?;
} }
f.write_str(CRLF) f.write_str("\r\n")
} }
} }
@@ -172,8 +165,8 @@ pub struct VrfyCommand {
impl Display for VrfyCommand { impl Display for VrfyCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "VRFY {}", self.argument)?; #[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
f.write_str(CRLF) write!(f, "VRFY {}\r\n", self.argument)
} }
} }
@@ -193,8 +186,7 @@ pub struct ExpnCommand {
impl Display for ExpnCommand { impl Display for ExpnCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EXPN {}", self.argument)?; write!(f, "EXPN {}\r\n", self.argument)
f.write_str(CRLF)
} }
} }
@@ -212,8 +204,7 @@ pub struct RsetCommand;
impl Display for RsetCommand { impl Display for RsetCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("RSET")?; f.write_str("RSET\r\n")
f.write_str(CRLF)
} }
} }
@@ -229,24 +220,20 @@ pub struct AuthCommand {
impl Display for AuthCommand { impl Display for AuthCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = if self.response.is_some() { let encoded_response = self
Some(base64::encode_config( .response
self.response.as_ref().unwrap().as_bytes(), .as_ref()
base64::STANDARD, .map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
))
} else {
None
};
if self.mechanism.supports_initial_response() { if self.mechanism.supports_initial_response() {
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap(),)?; write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
} else { } else {
match encoded_response { match encoded_response {
Some(response) => f.write_str(&response)?, Some(response) => f.write_str(&response)?,
None => write!(f, "AUTH {}", self.mechanism)?, None => write!(f, "AUTH {}", self.mechanism)?,
} }
} }
f.write_str(CRLF) f.write_str("\r\n")
} }
} }
@@ -258,7 +245,7 @@ impl AuthCommand {
challenge: Option<String>, challenge: Option<String>,
) -> Result<AuthCommand, Error> { ) -> Result<AuthCommand, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() { let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?) Some(mechanism.response(&credentials, challenge.as_deref())?)
} else { } else {
None None
}; };
@@ -281,21 +268,12 @@ impl AuthCommand {
return Err(Error::ResponseParsing("Expecting a challenge")); return Err(Error::ResponseParsing("Expecting a challenge"));
} }
let encoded_challenge = match response.first_word() { let encoded_challenge = response
Some(challenge) => challenge.to_string(), .first_word()
None => return Err(Error::ResponseParsing("Could not read auth challenge")), .ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
};
debug!("auth encoded challenge: {}", encoded_challenge); debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = match base64::decode(&encoded_challenge) { let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
Ok(challenge) => match String::from_utf8(challenge) {
Ok(value) => value,
Err(error) => return Err(Error::Utf8Parsing(error)),
},
Err(error) => return Err(Error::ChallengeParsing(error)),
};
debug!("auth decoded challenge: {}", decoded_challenge); debug!("auth decoded challenge: {}", decoded_challenge);
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?); let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
@@ -313,8 +291,6 @@ impl AuthCommand {
mod test { mod test {
use super::*; use super::*;
use smtp::extension::MailBodyParameter; use smtp::extension::MailBodyParameter;
#[cfg(feature = "crammd5-auth")]
use smtp::response::{Category, Code, Detail, Severity};
#[test] #[test]
fn test_display() { fn test_display() {
@@ -391,18 +367,6 @@ mod test {
), ),
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n" "AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
); );
#[cfg(feature = "crammd5-auth")]
assert_eq!(
format!(
"{}",
AuthCommand::new(
Mechanism::CramMd5,
credentials.clone(),
Some("test".to_string()),
).unwrap()
),
"dXNlciAzMTYxY2NmZDdmMjNlMzJiYmMzZTQ4NjdmYzk0YjE4Nw==\r\n"
);
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",
@@ -410,24 +374,5 @@ mod test {
), ),
"AUTH LOGIN\r\n" "AUTH LOGIN\r\n"
); );
#[cfg(feature = "crammd5-auth")]
assert_eq!(
format!(
"{}",
AuthCommand::new_from_response(
Mechanism::CramMd5,
credentials.clone(),
&Response::new(
Code::new(
Severity::PositiveIntermediate,
Category::Unspecified3,
Detail::Four,
),
vec!["dGVzdAo=".to_string()],
),
).unwrap()
),
"dXNlciA1NTIzNThiMzExOWFjOWNkYzM2YWRiN2MxNWRmMWJkNw==\r\n"
);
} }
} }

View File

@@ -42,42 +42,36 @@ pub enum Error {
impl Display for Error { impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description()) match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "transient error during SMTP transaction",
}),
Permanent(ref err) => fmt.write_str(match err.first_line() {
Some(line) => line,
None => "permanent error during SMTP transaction",
}),
ResponseParsing(err) => fmt.write_str(err),
ChallengeParsing(ref err) => err.fmt(fmt),
Utf8Parsing(ref err) => err.fmt(fmt),
Resolution => fmt.write_str("could not resolve hostname"),
Client(err) => fmt.write_str(err),
Io(ref err) => err.fmt(fmt),
Tls(ref err) => err.fmt(fmt),
Parsing(ref err) => fmt.write_str(err.description()),
}
} }
} }
impl StdError for Error { impl StdError for Error {
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] fn cause(&self) -> Option<&dyn StdError> {
fn description(&self) -> &str {
match *self {
// Try to display the first line of the server's response that usually
// contains a short humanly readable error message
Transient(ref err) => match err.first_line() {
Some(line) => line,
None => "undetailed transient error during SMTP transaction",
},
Permanent(ref err) => match err.first_line() {
Some(line) => line,
None => "undetailed permanent error during SMTP transaction",
},
ResponseParsing(err) => err,
ChallengeParsing(ref err) => err.description(),
Utf8Parsing(ref err) => err.description(),
Resolution => "could not resolve hostname",
Client(err) => err,
Io(ref err) => err.description(),
Tls(ref err) => err.description(),
Parsing(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
match *self { match *self {
ChallengeParsing(ref err) => Some(&*err), ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err), Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err), Io(ref err) => Some(&*err),
Tls(ref err) => Some(&*err), Tls(ref err) => Some(&*err),
Parsing(_) => None,
_ => None, _ => None,
} }
} }
@@ -101,6 +95,18 @@ impl From<nom::ErrorKind> for Error {
} }
} }
impl From<DecodeError> for Error {
fn from(err: DecodeError) -> Error {
ChallengeParsing(err)
}
}
impl From<FromUtf8Error> for Error {
fn from(err: FromUtf8Error) -> Error {
Utf8Parsing(err)
}
}
impl From<Response> for Error { impl From<Response> for Error {
fn from(response: Response) -> Error { fn from(response: Response) -> Error {
match response.code.severity { match response.code.severity {

View File

@@ -10,8 +10,8 @@ use std::fmt::{self, Display, Formatter};
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use std::result::Result; use std::result::Result;
/// Default ehlo clinet id /// Default client id
pub const DEFAULT_EHLO_HOSTNAME: &str = "localhost"; pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
/// Client identifier, the parameter to `EHLO` /// Client identifier, the parameter to `EHLO`
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
@@ -44,10 +44,7 @@ impl ClientId {
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be /// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
/// found /// found
pub fn hostname() -> ClientId { pub fn hostname() -> ClientId {
ClientId::Domain(match get_hostname() { ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
Some(name) => name,
None => DEFAULT_EHLO_HOSTNAME.to_string(),
})
} }
} }
@@ -137,21 +134,22 @@ impl ServerInfo {
"STARTTLS" => { "STARTTLS" => {
features.insert(Extension::StartTls); features.insert(Extension::StartTls);
} }
"AUTH" => for &mechanism in &split[1..] { "AUTH" => {
match mechanism { for &mechanism in &split[1..] {
"PLAIN" => { match mechanism {
features.insert(Extension::Authentication(Mechanism::Plain)); "PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
}
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
"XOAUTH2" => {
features.insert(Extension::Authentication(Mechanism::Xoauth2));
}
_ => (),
} }
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
#[cfg(feature = "crammd5-auth")]
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mechanism::CramMd5));
}
_ => (),
} }
}, }
_ => (), _ => (),
}; };
} }
@@ -357,8 +355,6 @@ mod test {
assert!(server_info.supports_feature(Extension::EightBitMime)); assert!(server_info.supports_feature(Extension::EightBitMime));
assert!(!server_info.supports_feature(Extension::StartTls)); assert!(!server_info.supports_feature(Extension::StartTls));
#[cfg(feature = "crammd5-auth")]
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
let response2 = Response::new( let response2 = Response::new(
Code::new( Code::new(
@@ -368,7 +364,7 @@ mod test {
), ),
vec![ vec![
"me".to_string(), "me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(), "AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"8BITMIME".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "SIZE 42".to_string(),
], ],
@@ -377,8 +373,7 @@ mod test {
let mut features2 = HashSet::new(); let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime)); assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),)); assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
#[cfg(feature = "crammd5-auth")] assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5),));
let server_info2 = ServerInfo { let server_info2 = ServerInfo {
name: "me".to_string(), name: "me".to_string(),
@@ -389,8 +384,6 @@ mod test {
assert!(server_info2.supports_feature(Extension::EightBitMime)); assert!(server_info2.supports_feature(Extension::EightBitMime));
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain)); assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
#[cfg(feature = "crammd5-auth")]
assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
assert!(!server_info2.supports_feature(Extension::StartTls)); assert!(!server_info2.supports_feature(Extension::StartTls));
} }
} }

View File

@@ -8,33 +8,33 @@
//! It implements the following extensions: //! It implements the following extensions:
//! //!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152)) //! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and //! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! CRAM-MD5 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487)) //! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531)) //! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//! //!
use EmailTransport;
use SendableEmail;
use native_tls::TlsConnector; use native_tls::TlsConnector;
use smtp::authentication::{Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, use smtp::authentication::{
DEFAULT_UNENCRYPTED_MECHANISMS}; Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
use smtp::client::Client; };
use smtp::client::net::ClientTlsParameters; use smtp::client::net::ClientTlsParameters;
use smtp::client::net::DEFAULT_TLS_PROTOCOLS; use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
use smtp::client::InnerClient;
use smtp::commands::*; use smtp::commands::*;
use smtp::error::{Error, SmtpResult}; use smtp::error::{Error, SmtpResult};
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo}; use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
use std::io::Read;
use std::net::{SocketAddr, ToSocketAddrs}; use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration; use std::time::Duration;
use {SendableEmail, Transport};
pub mod extension;
pub mod commands;
pub mod authentication; pub mod authentication;
pub mod response;
pub mod client; pub mod client;
pub mod commands;
pub mod error; pub mod error;
pub mod extension;
#[cfg(feature = "connection-pool")]
pub mod r2d2;
pub mod response;
pub mod util; pub mod util;
// Registered port numbers: // Registered port numbers:
@@ -43,39 +43,22 @@ pub mod util;
/// Default smtp port /// Default smtp port
pub const SMTP_PORT: u16 = 25; pub const SMTP_PORT: u16 = 25;
/// Default submission port /// Default submission port
pub const SUBMISSION_PORT: u16 = 587; pub const SUBMISSION_PORT: u16 = 587;
/// Default submission over TLS port
// Useful strings and characters pub const SUBMISSIONS_PORT: u16 = 465;
/// The word separator for SMTP transactions
pub const SP: &str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub const CRLF: &str = "\r\n";
/// Colon
pub const COLON: &str = ":";
/// The ending of message content
pub const MESSAGE_ENDING: &str = "\r\n.\r\n";
/// NUL unicode character
pub const NUL: &str = "\0";
/// How to apply TLS to a client connection /// How to apply TLS to a client connection
#[derive(Clone)] #[derive(Clone)]
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub enum ClientSecurity { pub enum ClientSecurity {
/// Insecure connection /// Insecure connection only (for testing purposes)
None, None,
/// Use `STARTTLS` when available /// Start with insecure connection and use `STARTTLS` when available
Opportunistic(ClientTlsParameters), Opportunistic(ClientTlsParameters),
/// Always use `STARTTLS` /// Start with insecure connection and require `STARTTLS`
Required(ClientTlsParameters), Required(ClientTlsParameters),
/// Use TLS wrapped connection without negotiation /// Use TLS wrapped connection
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
Wrapper(ClientTlsParameters), Wrapper(ClientTlsParameters),
} }
@@ -93,7 +76,8 @@ pub enum ConnectionReuseParameters {
/// Contains client configuration /// Contains client configuration
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct SmtpTransportBuilder { #[derive(Clone)]
pub struct SmtpClient {
/// Enable connection reuse /// Enable connection reuse
connection_reuse: ConnectionReuseParameters, connection_reuse: ConnectionReuseParameters,
/// Name sent during EHLO /// Name sent during EHLO
@@ -114,7 +98,7 @@ pub struct SmtpTransportBuilder {
} }
/// Builder for the SMTP `SmtpTransport` /// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder { impl SmtpClient {
/// Creates a new SMTP client /// Creates a new SMTP client
/// ///
/// Defaults are: /// Defaults are:
@@ -123,14 +107,13 @@ impl SmtpTransportBuilder {
/// * No authentication /// * No authentication
/// * No SMTPUTF8 support /// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands /// * A 60 seconds timeout for smtp commands
pub fn new<A: ToSocketAddrs>( ///
addr: A, /// Consider using [`SmtpClient::new_simple`] instead, if possible.
security: ClientSecurity, pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = addr.to_socket_addrs()?; let mut addresses = addr.to_socket_addrs()?;
match addresses.next() { match addresses.next() {
Some(addr) => Ok(SmtpTransportBuilder { Some(addr) => Ok(SmtpClient {
server_addr: addr, server_addr: addr,
security, security,
smtp_utf8: false, smtp_utf8: false,
@@ -144,41 +127,59 @@ impl SmtpTransportBuilder {
} }
} }
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submissions port, using the provided domain
/// to validate TLS certificates.
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new(
(domain, SUBMISSIONS_PORT),
ClientSecurity::Wrapper(tls_parameters),
)
}
/// Creates a new local SMTP client to port 25
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
}
/// Enable SMTPUTF8 if the server supports it /// Enable SMTPUTF8 if the server supports it
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder { pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
self.smtp_utf8 = enabled; self.smtp_utf8 = enabled;
self self
} }
/// Set the name used during EHLO /// Set the name used during EHLO
pub fn hello_name(mut self, name: ClientId) -> SmtpTransportBuilder { pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
self.hello_name = name; self.hello_name = name;
self self
} }
/// Enable connection reuse /// Enable connection reuse
pub fn connection_reuse( pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
mut self,
parameters: ConnectionReuseParameters,
) -> SmtpTransportBuilder {
self.connection_reuse = parameters; self.connection_reuse = parameters;
self self
} }
/// Set the client credentials /// Set the client credentials
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpTransportBuilder { pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
self.credentials = Some(credentials.into()); self.credentials = Some(credentials.into());
self self
} }
/// Set the authentication mechanism to use /// Set the authentication mechanism to use
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpTransportBuilder { pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
self.authentication_mechanism = Some(mechanism); self.authentication_mechanism = Some(mechanism);
self self
} }
/// Set the timeout duration /// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpTransportBuilder { pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
self.timeout = timeout; self.timeout = timeout;
self self
} }
@@ -186,7 +187,7 @@ impl SmtpTransportBuilder {
/// Build the SMTP client /// Build the SMTP client
/// ///
/// It does not connect to the server, but only creates the `SmtpTransport` /// It does not connect to the server, but only creates the `SmtpTransport`
pub fn build(self) -> SmtpTransport { pub fn transport(self) -> SmtpTransport {
SmtpTransport::new(self) SmtpTransport::new(self)
} }
} }
@@ -209,9 +210,9 @@ pub struct SmtpTransport {
/// SmtpTransport variable states /// SmtpTransport variable states
state: State, state: State,
/// Information about the client /// Information about the client
client_info: SmtpTransportBuilder, client_info: SmtpClient,
/// Low level client /// Low level client
client: Client, client: InnerClient,
} }
macro_rules! try_smtp ( macro_rules! try_smtp (
@@ -230,40 +231,11 @@ macro_rules! try_smtp (
); );
impl<'a> SmtpTransport { impl<'a> SmtpTransport {
/// Simple and secure transport, should be used when possible.
/// Creates an encrypted transport over submission port, using the provided domain
/// to validate TLS certificates.
pub fn simple_builder(domain: &str) -> Result<SmtpTransportBuilder, Error> {
let mut tls_builder = TlsConnector::builder()?;
tls_builder.supported_protocols(DEFAULT_TLS_PROTOCOLS)?;
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpTransportBuilder::new(
(domain, SUBMISSION_PORT),
ClientSecurity::Required(tls_parameters),
)
}
/// Creates a new configurable builder
pub fn builder<A: ToSocketAddrs>(
addr: A,
security: ClientSecurity,
) -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(addr, security)
}
/// Creates a new local SMTP client to port 25
pub fn builder_unencrypted_localhost() -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(("localhost", SMTP_PORT), ClientSecurity::None)
}
/// Creates a new SMTP client /// Creates a new SMTP client
/// ///
/// It does not connect to the server, but only creates the `SmtpTransport` /// It does not connect to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport { pub fn new(builder: SmtpClient) -> SmtpTransport {
let client = Client::new(); let client = InnerClient::new();
SmtpTransport { SmtpTransport {
client, client,
@@ -276,6 +248,99 @@ impl<'a> SmtpTransport {
} }
} }
fn connect(&mut self) -> Result<(), Error> {
// Check if the connection is still available
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
self.close();
}
if self.state.connection_reuse_count > 0 {
info!(
"connection already established to {}",
self.client_info.server_addr
);
return Ok(());
}
self.client.connect(
&self.client_info.server_addr,
match self.client_info.security {
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
},
)?;
self.client.set_timeout(self.client_info.timeout)?;
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
self.ehlo()?;
match (
&self.client_info.security.clone(),
self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::StartTls),
) {
(&ClientSecurity::Required(_), false) => {
return Err(From::from("Could not encrypt connection, aborting"));
}
(&ClientSecurity::Opportunistic(_), false) => (),
(&ClientSecurity::None, _) => (),
(&ClientSecurity::Wrapper(_), _) => (),
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
| (&ClientSecurity::Required(ref tls_parameters), true) => {
try_smtp!(self.client.command(StarttlsCommand), self);
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
debug!("connection encrypted");
// Send EHLO again
self.ehlo()?;
}
}
if self.client_info.credentials.is_some() {
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() {
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
} else {
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
}
}
};
for mechanism in accepted_mechanisms {
if self
.server_info
.as_ref()
.unwrap()
.supports_auth_mechanism(mechanism)
{
found = true;
try_smtp!(
self.client
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
self
);
break;
}
}
if !found {
info!("No supported authentication mechanisms available");
}
}
Ok(())
}
/// Gets the EHLO response and updates server information /// Gets the EHLO response and updates server information
fn ehlo(&mut self) -> SmtpResult { fn ehlo(&mut self) -> SmtpResult {
// Extended Hello // Extended Hello
@@ -306,101 +371,26 @@ impl<'a> SmtpTransport {
} }
} }
impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport { impl<'a> Transport<'a> for SmtpTransport {
type Result = SmtpResult;
/// Sends an email /// Sends an email
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms, cyclomatic_complexity))] #[cfg_attr(
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SmtpResult { feature = "cargo-clippy",
// Extract email information allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
let message_id = email.message_id(); )]
let envelope = email.envelope(); fn send(&mut self, email: SendableEmail) -> SmtpResult {
let message_id = email.message_id().to_string();
// Check if the connection is still available if !self.client.is_connected() {
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) { self.connect()?;
self.close();
}
if self.state.connection_reuse_count == 0 {
self.client.connect(
&self.client_info.server_addr,
match self.client_info.security {
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
},
)?;
self.client.set_timeout(self.client_info.timeout)?;
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
self.ehlo()?;
match (
&self.client_info.security.clone(),
self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::StartTls),
) {
(&ClientSecurity::Required(_), false) => {
return Err(From::from("Could not encrypt connection, aborting"))
}
(&ClientSecurity::Opportunistic(_), false) => (),
(&ClientSecurity::None, _) => (),
(&ClientSecurity::Wrapper(_), _) => (),
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
| (&ClientSecurity::Required(ref tls_parameters), true) => {
try_smtp!(self.client.command(StarttlsCommand), self);
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
debug!("connection encrypted");
// Send EHLO again
self.ehlo()?;
}
}
if self.client_info.credentials.is_some() {
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() {
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
} else {
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
}
}
};
for mechanism in accepted_mechanisms {
if self.server_info
.as_ref()
.unwrap()
.supports_auth_mechanism(mechanism)
{
found = true;
try_smtp!(
self.client
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
self
);
break;
}
}
if !found {
info!("No supported authentication mechanisms available");
}
}
} }
// Mail // Mail
let mut mail_options = vec![]; let mut mail_options = vec![];
if self.server_info if self
.server_info
.as_ref() .as_ref()
.unwrap() .unwrap()
.supports_feature(Extension::EightBitMime) .supports_feature(Extension::EightBitMime)
@@ -408,17 +398,21 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
if self.server_info if self
.server_info
.as_ref() .as_ref()
.unwrap() .unwrap()
.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8 .supports_feature(Extension::SmtpUtfEight)
&& self.client_info.smtp_utf8
{ {
mail_options.push(MailParameter::SmtpUtfEight); mail_options.push(MailParameter::SmtpUtfEight);
} }
try_smtp!( try_smtp!(
self.client self.client.command(MailCommand::new(
.command(MailCommand::new(envelope.from().cloned(), mail_options,)), email.envelope().from().cloned(),
mail_options,
)),
self self
); );
@@ -426,17 +420,17 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
info!( info!(
"{}: from=<{}>", "{}: from=<{}>",
message_id, message_id,
match envelope.from() { match email.envelope().from() {
Some(address) => address.to_string(), Some(address) => address.to_string(),
None => "".to_string(), None => "".to_string(),
} }
); );
// Recipient // Recipient
for to_address in envelope.to() { for to_address in email.envelope().to() {
try_smtp!( try_smtp!(
self.client self.client
.command(RcptCommand::new(to_address.clone(), vec![]),), .command(RcptCommand::new(to_address.clone(), vec![])),
self self
); );
// Log the rcpt command // Log the rcpt command
@@ -447,7 +441,7 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
try_smtp!(self.client.command(DataCommand), self); try_smtp!(self.client.command(DataCommand), self);
// Message content // Message content
let result = self.client.message(email.message()); let result = self.client.message(Box::new(email.message()));
if result.is_ok() { if result.is_ok() {
// Increment the connection reuse counter // Increment the connection reuse counter

38
lettre/src/smtp/r2d2.rs Normal file
View File

@@ -0,0 +1,38 @@
use r2d2::ManageConnection;
use smtp::error::Error;
use smtp::{ConnectionReuseParameters, SmtpClient, SmtpTransport};
pub struct SmtpConnectionManager {
transport_builder: SmtpClient,
}
impl SmtpConnectionManager {
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
Ok(SmtpConnectionManager {
transport_builder: transport_builder
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
})
}
}
impl ManageConnection for SmtpConnectionManager {
type Connection = SmtpTransport;
type Error = Error;
fn connect(&self) -> Result<Self::Connection, Error> {
let mut transport = SmtpTransport::new(self.transport_builder.clone());
transport.connect()?;
Ok(transport)
}
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
if conn.client.is_connected() {
return Ok(());
}
Err(Error::Client("is not connected anymore"))
}
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
conn.state.panic
}
}

View File

@@ -4,7 +4,7 @@
use nom::{crlf, ErrorKind as NomErrorKind}; use nom::{crlf, ErrorKind as NomErrorKind};
use std::fmt::{Display, Formatter, Result}; use std::fmt::{Display, Formatter, Result};
use std::result; use std::result;
use std::str::{FromStr, from_utf8}; use std::str::{from_utf8, FromStr};
/// First digit indicates severity /// First digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)] #[derive(PartialEq, Eq, Copy, Clone, Debug)]
@@ -331,20 +331,19 @@ mod test {
#[test] #[test]
fn test_response_is_positive() { fn test_response_is_positive() {
assert!( assert!(Response::new(
Response::new( Code {
Code { severity: Severity::PositiveCompletion,
severity: Severity::PositiveCompletion, category: Category::MailSystem,
category: Category::MailSystem, detail: Detail::Zero,
detail: Detail::Zero, },
}, vec![
vec![ "me".to_string(),
"me".to_string(), "8BITMIME".to_string(),
"8BITMIME".to_string(), "SIZE 42".to_string(),
"SIZE 42".to_string(), ],
], )
).is_positive() .is_positive());
);
assert!(!Response::new( assert!(!Response::new(
Code { Code {
severity: Severity::TransientNegativeCompletion, severity: Severity::TransientNegativeCompletion,
@@ -356,25 +355,25 @@ mod test {
"8BITMIME".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "SIZE 42".to_string(),
], ],
).is_positive()); )
.is_positive());
} }
#[test] #[test]
fn test_response_has_code() { fn test_response_has_code() {
assert!( assert!(Response::new(
Response::new( Code {
Code { severity: Severity::TransientNegativeCompletion,
severity: Severity::TransientNegativeCompletion, category: Category::MailSystem,
category: Category::MailSystem, detail: Detail::One,
detail: Detail::One, },
}, vec![
vec![ "me".to_string(),
"me".to_string(), "8BITMIME".to_string(),
"8BITMIME".to_string(), "SIZE 42".to_string(),
"SIZE 42".to_string(), ],
], )
).has_code(451) .has_code(451));
);
assert!(!Response::new( assert!(!Response::new(
Code { Code {
severity: Severity::TransientNegativeCompletion, severity: Severity::TransientNegativeCompletion,
@@ -386,7 +385,8 @@ mod test {
"8BITMIME".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "SIZE 42".to_string(),
], ],
).has_code(251)); )
.has_code(251));
} }
#[test] #[test]
@@ -403,7 +403,8 @@ mod test {
"8BITMIME".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "SIZE 42".to_string(),
], ],
).first_word(), )
.first_word(),
Some("me") Some("me")
); );
assert_eq!( assert_eq!(
@@ -418,7 +419,8 @@ mod test {
"8BITMIME".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "SIZE 42".to_string(),
], ],
).first_word(), )
.first_word(),
Some("me") Some("me")
); );
assert_eq!( assert_eq!(
@@ -429,7 +431,8 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![], vec![],
).first_word(), )
.first_word(),
None None
); );
assert_eq!( assert_eq!(
@@ -440,7 +443,8 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_string()],
).first_word(), )
.first_word(),
None None
); );
assert_eq!( assert_eq!(
@@ -451,7 +455,8 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_string()],
).first_word(), )
.first_word(),
None None
); );
assert_eq!( assert_eq!(
@@ -462,7 +467,8 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec!["".to_string()], vec!["".to_string()],
).first_word(), )
.first_word(),
None None
); );
} }
@@ -481,7 +487,8 @@ mod test {
"8BITMIME".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "SIZE 42".to_string(),
], ],
).first_line(), )
.first_line(),
Some("me") Some("me")
); );
assert_eq!( assert_eq!(
@@ -496,7 +503,8 @@ mod test {
"8BITMIME".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "SIZE 42".to_string(),
], ],
).first_line(), )
.first_line(),
Some("me mo") Some("me mo")
); );
assert_eq!( assert_eq!(
@@ -507,7 +515,8 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![], vec![],
).first_line(), )
.first_line(),
None None
); );
assert_eq!( assert_eq!(
@@ -518,7 +527,8 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_string()],
).first_line(), )
.first_line(),
Some(" ") Some(" ")
); );
assert_eq!( assert_eq!(
@@ -529,7 +539,8 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec![" ".to_string()], vec![" ".to_string()],
).first_line(), )
.first_line(),
Some(" ") Some(" ")
); );
assert_eq!( assert_eq!(
@@ -540,7 +551,8 @@ mod test {
detail: Detail::One, detail: Detail::One,
}, },
vec!["".to_string()], vec!["".to_string()],
).first_line(), )
.first_line(),
Some("") Some("")
); );
} }

View File

@@ -2,42 +2,42 @@
//! testing purposes. //! testing purposes.
//! //!
use EmailTransport;
use SendableEmail; use SendableEmail;
use std::io::Read; use Transport;
/// This transport logs the message envelope and returns the given response /// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct StubEmailTransport { pub struct StubTransport {
response: StubResult, response: StubResult,
} }
impl StubEmailTransport { impl StubTransport {
/// Creates a new transport that always returns the given response /// Creates a new transport that always returns the given response
pub fn new(response: StubResult) -> StubEmailTransport { pub fn new(response: StubResult) -> StubTransport {
StubEmailTransport { response } StubTransport { response }
} }
/// Creates a new transport that always returns a success response /// Creates a new transport that always returns a success response
pub fn new_positive() -> StubEmailTransport { pub fn new_positive() -> StubTransport {
StubEmailTransport { response: Ok(()) } StubTransport { response: Ok(()) }
} }
} }
/// SMTP result type /// SMTP result type
pub type StubResult = Result<(), ()>; pub type StubResult = Result<(), ()>;
impl<'a, T: Read + 'a> EmailTransport<'a, T, StubResult> for StubEmailTransport { impl<'a> Transport<'a> for StubTransport {
fn send<U: SendableEmail<'a, T>>(&mut self, email: &'a U) -> StubResult { type Result = StubResult;
let envelope = email.envelope();
fn send(&mut self, email: SendableEmail) -> StubResult {
info!( info!(
"{}: from=<{}> to=<{:?}>", "{}: from=<{}> to=<{:?}>",
email.message_id(), email.message_id(),
match envelope.from() { match email.envelope().from() {
Some(address) => address.to_string(), Some(address) => address.to_string(),
None => "".to_string(), None => "".to_string(),
}, },
envelope.to() email.envelope().to()
); );
self.response self.response
} }

74
lettre/tests/r2d2_smtp.rs Normal file
View File

@@ -0,0 +1,74 @@
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
mod test {
extern crate lettre;
extern crate r2d2;
use self::lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient};
use self::lettre::{SmtpConnectionManager, Transport};
use self::r2d2::Pool;
use std::sync::mpsc;
use std::thread;
fn email(message: &str) -> SendableEmail {
SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
message.to_string().into_bytes(),
)
}
#[test]
fn send_one() {
let client = SmtpClient::new("localhost:2525", ClientSecurity::None).unwrap();
let manager = SmtpConnectionManager::new(client).unwrap();
let pool = Pool::builder().max_size(1).build(manager).unwrap();
let mut mailer = pool.get().unwrap();
let result = (*mailer).send(email("send one"));
assert!(result.is_ok());
}
#[test]
fn send_from_thread() {
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap();
let manager = SmtpConnectionManager::new(client).unwrap();
let pool = Pool::builder().max_size(2).build(manager).unwrap();
let (s1, r1) = mpsc::channel();
let (s2, r2) = mpsc::channel();
let pool1 = pool.clone();
let t1 = thread::spawn(move || {
let mut conn = pool1.get().unwrap();
s1.send(()).unwrap();
r2.recv().unwrap();
(*conn)
.send(email("send from thread 1"))
.expect("Send failed from thread 1");
drop(conn);
});
let pool2 = pool.clone();
let t2 = thread::spawn(move || {
let mut conn = pool2.get().unwrap();
s2.send(()).unwrap();
r1.recv().unwrap();
(*conn)
.send(email("send from thread 2"))
.expect("Send failed from thread 2");
drop(conn);
});
t1.join().unwrap();
t2.join().unwrap();
let mut mailer = pool.get().unwrap();
(*mailer)
.send(email("send from main thread"))
.expect("Send failed from main thread");
}
}

View File

@@ -1,11 +1,12 @@
extern crate glob; extern crate glob;
use self::glob::glob; use self::glob::glob;
use std::env::consts::EXE_EXTENSION;
use std::env; use std::env;
use std::env::consts::EXE_EXTENSION;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
/*
#[test] #[test]
fn test_readme() { fn test_readme() {
let readme = Path::new(file!()) let readme = Path::new(file!())
@@ -19,6 +20,7 @@ fn test_readme() {
skeptic_test(&readme); skeptic_test(&readme);
} }
*/
#[test] #[test]
fn book_test() { fn book_test() {
@@ -50,7 +52,8 @@ fn skeptic_test(path: &Path) {
.arg(&depdir) .arg(&depdir)
.arg(path); .arg(path);
let result = cmd.spawn() let result = cmd
.spawn()
.expect("Failed to spawn process") .expect("Failed to spawn process")
.wait() .wait()
.expect("Failed to run process"); .expect("Failed to run process");

View File

@@ -4,34 +4,38 @@ extern crate lettre;
#[cfg(feature = "file-transport")] #[cfg(feature = "file-transport")]
mod test { mod test {
use lettre::{EmailTransport, SendableEmail, SimpleSendableEmail}; use lettre::file::FileTransport;
use lettre::file::FileEmailTransport; use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
use std::env::temp_dir; use std::env::temp_dir;
use std::fs::File;
use std::fs::remove_file; use std::fs::remove_file;
use std::fs::File;
use std::io::Read; use std::io::Read;
#[test] #[test]
fn file_transport() { fn file_transport() {
let mut sender = FileEmailTransport::new(temp_dir()); let mut sender = FileTransport::new(temp_dir());
let email = SimpleSendableEmail::new( let email = SendableEmail::new(
"user@localhost".to_string(), Envelope::new(
&["root@localhost".to_string()], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
"file_id".to_string(), vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"Hello file".to_string(), )
).unwrap(); .unwrap(),
let result = sender.send(&email); "id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let message_id = email.message_id().to_string();
let result = sender.send(email);
assert!(result.is_ok()); assert!(result.is_ok());
let message_id = email.message_id(); let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
let file = format!("{}/{}.txt", temp_dir().to_str().unwrap(), message_id);
let mut f = File::open(file.clone()).unwrap(); let mut f = File::open(file.clone()).unwrap();
let mut buffer = String::new(); let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer); let _ = f.read_to_string(&mut buffer);
assert_eq!( assert_eq!(
buffer, buffer,
"{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"file_id\",\"message\":[72,101,108,108,111,32,102,105,108,101]}" "{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"id\",\"message\":[72,101,108,108,111,32,195,159,226,152,186,32,101,120,97,109,112,108,101]}"
); );
remove_file(file).unwrap(); remove_file(file).unwrap();

View File

@@ -3,21 +3,23 @@ extern crate lettre;
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "sendmail-transport")] #[cfg(feature = "sendmail-transport")]
mod test { mod test {
use lettre::{EmailTransport, SimpleSendableEmail};
use lettre::sendmail::SendmailTransport; use lettre::sendmail::SendmailTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
#[test] #[test]
fn sendmail_transport_simple() { fn sendmail_transport_simple() {
let mut sender = SendmailTransport::new(); let mut sender = SendmailTransport::new();
let email = SimpleSendableEmail::new( let email = SendableEmail::new(
"user@localhost".to_string(), Envelope::new(
&["root@localhost".to_string()], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
"sendmail_id".to_string(), vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"Hello sendmail".to_string(), )
).unwrap(); .unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let result = sender.send(&email); let result = sender.send(email);
println!("{:?}", result); println!("{:?}", result);
assert!(result.is_ok()); assert!(result.is_ok());
} }

View File

@@ -3,22 +3,25 @@ extern crate lettre;
#[cfg(test)] #[cfg(test)]
#[cfg(feature = "smtp-transport")] #[cfg(feature = "smtp-transport")]
mod test { mod test {
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
use lettre::{ClientSecurity, EmailTransport, SimpleSendableEmail, SmtpTransport};
#[test] #[test]
fn smtp_transport_simple() { fn smtp_transport_simple() {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None) let email = SendableEmail::new(
.unwrap() Envelope::new(
.build(); Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
let email = SimpleSendableEmail::new( vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"user@localhost".to_string(), )
&["root@localhost".to_string()], .unwrap(),
"smtp_id".to_string(), "id".to_string(),
"Hello smtp".to_string(), "Hello ß☺ example".to_string().into_bytes(),
).unwrap(); );
sender.send(&email).unwrap(); SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.transport()
.send(email)
.unwrap();
} }
} }

View File

@@ -1,19 +1,31 @@
extern crate lettre; extern crate lettre;
use lettre::{EmailTransport, SimpleSendableEmail}; use lettre::stub::StubTransport;
use lettre::stub::StubEmailTransport; use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
#[test] #[test]
fn stub_transport() { fn stub_transport() {
let mut sender_ok = StubEmailTransport::new_positive(); let mut sender_ok = StubTransport::new_positive();
let mut sender_ko = StubEmailTransport::new(Err(())); let mut sender_ko = StubTransport::new(Err(()));
let email = SimpleSendableEmail::new( let email_ok = SendableEmail::new(
"user@localhost".to_string(), Envelope::new(
&["root@localhost".to_string()], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
"stub_id".to_string(), vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"Hello stub".to_string(), )
).unwrap(); .unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
let email_ko = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
)
.unwrap(),
"id".to_string(),
"Hello ß☺ example".to_string().into_bytes(),
);
sender_ok.send(&email).unwrap(); sender_ok.send(email_ok).unwrap();
sender_ko.send(&email).unwrap_err(); sender_ko.send(email_ko).unwrap_err();
} }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lettre_email" name = "lettre_email"
version = "0.8.2" # remember to update html_root_url version = "0.9.3" # remember to update html_root_url
description = "Email builder" description = "Email builder"
readme = "README.md" readme = "README.md"
homepage = "http://lettre.at" homepage = "http://lettre.at"
@@ -19,13 +19,13 @@ is-it-maintained-issue-resolution = { repository = "lettre/lettre_email" }
is-it-maintained-open-issues = { repository = "lettre/lettre_email" } is-it-maintained-open-issues = { repository = "lettre/lettre_email" }
[dev-dependencies] [dev-dependencies]
lettre = { version = "^0.8", path = "../lettre", features = ["smtp-transport"] } lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
glob = "0.2" glob = "0.3"
[dependencies] [dependencies]
email = "^0.0" email = "^0.0.21"
mime = "^0.3" mime = "^0.3"
time = "^0.1" time = "^0.1"
uuid = { version = "^0.6", features = ["v4"] } uuid = { version = "^0.7", features = ["v4"] }
lettre = { version = "^0.8", path = "../lettre", default-features = false } lettre = { version = "^0.9", path = "../lettre", default-features = false }
base64 = "^0.9" base64 = "^0.10"

View File

@@ -2,28 +2,27 @@ extern crate lettre;
extern crate lettre_email; extern crate lettre_email;
extern crate mime; extern crate mime;
use lettre::{EmailTransport, SmtpTransport}; use lettre::{SmtpClient, Transport};
use lettre_email::EmailBuilder; use lettre_email::Email;
use std::path::Path; use std::path::Path;
fn main() { fn main() {
let email = EmailBuilder::new() let email = Email::builder()
// Addresses can be specified by the tuple (email, alias) // Addresses can be specified by the tuple (email, alias)
.to(("user@example.org", "Firstname Lastname")) .to(("user@example.org", "Firstname Lastname"))
// ... or by an address only // ... or by an address only
.from("user@example.com") .from("user@example.com")
.subject("Hi, Hello world") .subject("Hi, Hello world")
.text("Hello world.") .text("Hello world.")
.attachment(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN).unwrap() .attachment_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
.unwrap()
.build() .build()
.unwrap(); .unwrap();
// Open a local connection on port 25 // Open a local connection on port 25
let mut mailer = SmtpTransport::builder_unencrypted_localhost() let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
.unwrap()
.build();
// Send the email // Send the email
let result = mailer.send(&email); let result = mailer.send(email.into());
if result.is_ok() { if result.is_ok() {
println!("Email sent"); println!("Email sent");

View File

@@ -1,17 +1,18 @@
//! Error and result type for emails //! Error and result type for emails
use self::Error::*;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
use lettre; use lettre;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
use self::Error::*;
/// An enum of all error kinds. /// An enum of all error kinds.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
/// Envelope error /// Envelope error
Email(lettre::Error), Envelope(lettre::error::Error),
/// Unparseable filename for attachment /// Unparseable filename for attachment
CannotParseFilename, CannotParseFilename,
/// IO error /// IO error
@@ -19,29 +20,34 @@ pub enum Error {
} }
impl Display for Error { impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
fmt.write_str(self.description()) fmt.write_str(&match *self {
CannotParseFilename => "Could not parse attachment filename".to_owned(),
Io(ref err) => err.to_string(),
Envelope(ref err) => err.to_string(),
})
} }
} }
impl StdError for Error { impl StdError for Error {
fn description(&self) -> &str { fn cause(&self) -> Option<&dyn StdError> {
match *self { match *self {
Email(ref err) => err.description(), Envelope(ref err) => Some(err),
CannotParseFilename => "the attachment filename could not be parsed", Io(ref err) => Some(err),
Io(ref err) => err.description(), _ => None,
} }
} }
} }
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(err: io::Error) -> Error { fn from(err: io::Error) -> Error {
Io(err) Error::Io(err)
} }
} }
impl From<lettre::Error> for Error { impl From<lettre::error::Error> for Error {
fn from(err: lettre::Error) -> Error { fn from(err: lettre::error::Error) -> Error {
Email(err) Error::Envelope(err)
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
extern crate lettre;
extern crate lettre_email;
use lettre::{EmailAddress, Envelope};
use lettre_email::EmailBuilder;
#[test]
fn build_with_envelope_test() {
let e = Envelope::new(
Some(EmailAddress::new("from@example.org".to_string()).unwrap()),
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
)
.unwrap();
let _email = EmailBuilder::new()
.envelope(e)
.subject("subject")
.text("message")
.build()
.unwrap();
}
#[test]
fn build_with_envelope_without_from_test() {
let e = Envelope::new(
None,
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
)
.unwrap();
let _email = EmailBuilder::new()
.envelope(e)
.subject("subject")
.text("message")
.build()
.unwrap_err();
}

View File

@@ -1,8 +1,8 @@
extern crate glob; extern crate glob;
use self::glob::glob; use self::glob::glob;
use std::env::consts::EXE_EXTENSION;
use std::env; use std::env;
use std::env::consts::EXE_EXTENSION;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
@@ -36,7 +36,8 @@ fn skeptic_test(path: &Path) {
.arg(&depdir) .arg(&depdir)
.arg(path); .arg(path);
let result = cmd.spawn() let result = cmd
.spawn()
.expect("Failed to spawn process") .expect("Failed to spawn process")
.wait() .wait()
.expect("Failed to run process"); .expect("Failed to run process");

1
website/.gitignore vendored
View File

@@ -1,2 +1 @@
node_modules
/_book /_book

View File

@@ -1,13 +1,16 @@
all: depends _book all: depends _book
depends: depends:
gitbook install cargo install --force mdbook --vers "^0.2"
cargo install --force mdbook-linkcheck --vers "^0.2"
serve: serve:
gitbook serve mdbook serve
_book: _book:
gitbook build mdbook build
clean: clean:
rm -rf _book/ rm -rf _book/
.PHONY: _book

View File

@@ -1,10 +0,0 @@
{
"root": "./content",
"plugins": [ "-sharing", "edit-link" ],
"pluginsConfig": {
"edit-link": {
"base": "https://github.com/lettre/lettre/edit/master/website/src",
"label": "Edit"
}
}
}

11
website/book.toml Normal file
View File

@@ -0,0 +1,11 @@
[book]
title = "Lettre"
author = "Alexis Mousset"
description = "The user documentation of the Lettre crate."
[build]
build-dir = "_book"
[output.html]
[output.linkcheck]

View File

@@ -1,23 +0,0 @@
#### Sendmail Transport
The sendmail transport sends the email using the local sendmail command.
```rust,no_run
extern crate lettre;
use lettre::sendmail::SendmailTransport;
use lettre::{SimpleSendableEmail, EmailTransport};
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let mut sender = SendmailTransport::new();
let result = sender.send(&email);
assert!(result.is_ok());
}
```

View File

@@ -1,30 +0,0 @@
#### Stub Transport
The stub transport only logs message envelope and drops the content. It can be useful for
testing purposes.
```rust
extern crate lettre;
use lettre::stub::StubEmailTransport;
use lettre::{SimpleSendableEmail, EmailTransport};
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let mut sender = StubEmailTransport::new_positive();
let result = sender.send(&email);
assert!(result.is_ok());
}
```
Will log (when using a logger like `env_logger`):
```text
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
```

View File

@@ -10,10 +10,10 @@ Lettre is an email library that allows creating and sending messages. It provide
The `lettre_email` crate allows you to compose messages, and the `lettre` The `lettre_email` crate allows you to compose messages, and the `lettre`
provide transports to send them. provide transports to send them.
Lettre requires Rust 1.20 or newer. Add the following to your `Cargo.toml`: Lettre requires Rust 1.32 or newer. Add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
lettre = "0.8" lettre = "0.9"
lettre_email = "0.8" lettre_email = "0.9"
``` ```

View File

@@ -10,17 +10,17 @@ An email is built using an `EmailBuilder`. The simplest email could be:
```rust ```rust
extern crate lettre_email; extern crate lettre_email;
use lettre_email::EmailBuilder; use lettre_email::Email;
fn main() { fn main() {
// Create an email // Create an email
let email = EmailBuilder::new() let email = Email::builder()
// Addresses can be specified by the tuple (email, alias) // Addresses can be specified by the tuple (email, alias)
.to(("user@example.org", "Firstname Lastname")) .to(("user@example.org", "Firstname Lastname"))
// ... or by an address only // ... or by an address only
.from("user@example.com") .from("user@example.com")
.subject("Hi, Hello world") .subject("Hi, Hello world")
.text("Hello world.") .alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
.build(); .build();
assert!(email.is_ok()); assert!(email.is_ok());
@@ -34,32 +34,3 @@ then generate an `Email` that can be sent.
The `text()` method will create a plain text email, while the `html()` method will create an 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 HTML email. You can use the `alternative()` method to provide both versions, using plain text
as fallback for the HTML version. as fallback for the HTML version.
#### Complete example
Below is a more complete example, not using method chaining:
```rust
extern crate lettre_email;
use lettre_email::EmailBuilder;
fn main() {
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.

View File

@@ -9,20 +9,22 @@ extern crate lettre;
use std::env::temp_dir; use std::env::temp_dir;
use lettre::file::FileEmailTransport; use lettre::file::FileTransport;
use lettre::{SimpleSendableEmail, EmailTransport}; use lettre::{Transport, Envelope, EmailAddress, SendableEmail};
fn main() { fn main() {
// Write to the local temp directory // Write to the local temp directory
let mut sender = FileEmailTransport::new(temp_dir()); let mut sender = FileTransport::new(temp_dir());
let email = SimpleSendableEmail::new( let email = SendableEmail::new(
"user@localhost".to_string(), Envelope::new(
&["root@localhost".to_string()], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
"message_id".to_string(), vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"Hello world".to_string(), ).unwrap(),
).unwrap(); "id".to_string(),
"Hello world".to_string().into_bytes(),
);
let result = sender.send(&email); let result = sender.send(email);
assert!(result.is_ok()); assert!(result.is_ok());
} }
``` ```

View File

@@ -0,0 +1,25 @@
#### Sendmail Transport
The sendmail transport sends the email using the local sendmail command.
```rust,no_run
extern crate lettre;
use lettre::sendmail::SendmailTransport;
use lettre::{SendableEmail, Envelope, EmailAddress, Transport};
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id".to_string(),
"Hello world".to_string().into_bytes(),
);
let mut sender = SendmailTransport::new();
let result = sender.send(email);
assert!(result.is_ok());
}
```

View File

@@ -1,4 +1,4 @@
SMTP Transport #### SMTP Transport
This transport uses the SMTP protocol to send emails over the network (locally or remotely). This transport uses the SMTP protocol to send emails over the network (locally or remotely).
@@ -20,21 +20,23 @@ This is the most basic example of usage:
```rust,no_run ```rust,no_run
extern crate lettre; extern crate lettre;
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport}; use lettre::{SendableEmail, EmailAddress, Transport, Envelope, SmtpClient};
fn main() { fn main() {
let email = SimpleSendableEmail::new( let email = SendableEmail::new(
"user@localhost".to_string(), Envelope::new(
&["root@localhost".to_string()], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
"message_id".to_string(), vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"Hello world".to_string(), ).unwrap(),
).unwrap(); "id".to_string(),
"Hello world".to_string().into_bytes(),
);
// Open a local connection on port 25 // Open a local connection on port 25
let mut mailer = let mut mailer =
SmtpTransport::builder_unencrypted_localhost().unwrap().build(); SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email // Send the email
let result = mailer.send(&email); let result = mailer.send(email);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@@ -46,20 +48,31 @@ fn main() {
extern crate lettre; extern crate lettre;
use lettre::smtp::authentication::{Credentials, Mechanism}; use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport}; use lettre::{SendableEmail, Envelope, EmailAddress, Transport, SmtpClient};
use lettre::smtp::extension::ClientId; use lettre::smtp::extension::ClientId;
use lettre::smtp::ConnectionReuseParameters; use lettre::smtp::ConnectionReuseParameters;
fn main() { fn main() {
let email = SimpleSendableEmail::new( let email_1 = SendableEmail::new(
"user@localhost".to_string(), Envelope::new(
&["root@localhost".to_string()], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
"message_id".to_string(), vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"Hello world".to_string(), ).unwrap(),
).unwrap(); "id1".to_string(),
"Hello world".to_string().into_bytes(),
);
let email_2 = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id2".to_string(),
"Hello world a second time".to_string().into_bytes(),
);
// Connect to a remote server on a custom port // Connect to a remote server on a custom port
let mut mailer = SmtpTransport::simple_builder("server.tld").unwrap() let mut mailer = SmtpClient::new_simple("server.tld").unwrap()
// Set the name sent during EHLO/HELO, default is `localhost` // Set the name sent during EHLO/HELO, default is `localhost`
.hello_name(ClientId::Domain("my.hostname.tld".to_string())) .hello_name(ClientId::Domain("my.hostname.tld".to_string()))
// Add credentials for authentication // Add credentials for authentication
@@ -69,13 +82,13 @@ fn main() {
// Configure expected authentication mechanism // Configure expected authentication mechanism
.authentication_mechanism(Mechanism::Plain) .authentication_mechanism(Mechanism::Plain)
// Enable connection reuse // Enable connection reuse
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).build(); .connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
let result_1 = mailer.send(&email); let result_1 = mailer.send(email_1);
assert!(result_1.is_ok()); assert!(result_1.is_ok());
// The second email will use the same connection // The second email will use the same connection
let result_2 = mailer.send(&email); let result_2 = mailer.send(email_2);
assert!(result_2.is_ok()); assert!(result_2.is_ok());
// Explicitly close the SMTP transaction as we enabled connection reuse // Explicitly close the SMTP transaction as we enabled connection reuse
@@ -90,45 +103,46 @@ extern crate native_tls;
extern crate lettre; extern crate lettre;
extern crate lettre_email; extern crate lettre_email;
use native_tls::TlsConnector; use lettre::{
use native_tls::{Protocol}; ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
SendableEmail, SmtpClient, Transport,
};
use lettre::smtp::authentication::{Credentials, Mechanism}; use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::{EmailTransport, SimpleSendableEmail, ClientTlsParameters, ClientSecurity};
use lettre::smtp::ConnectionReuseParameters; use lettre::smtp::ConnectionReuseParameters;
use lettre::smtp::{SmtpTransportBuilder}; use native_tls::{Protocol, TlsConnector};
use lettre_email::EmailBuilder;
fn main() { fn main() {
let email = SimpleSendableEmail::new( let email = SendableEmail::new(
"user@localhost".to_string(), Envelope::new(
&["root@localhost".to_string()], Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
"message_id".to_string(), vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
"Hello world".to_string(), ).unwrap(),
).unwrap(); "message_id".to_string(),
"Hello world".to_string().into_bytes(),
);
let mut tls_builder = TlsConnector::builder().unwrap(); let mut tls_builder = TlsConnector::builder();
tls_builder.supported_protocols(&[Protocol::Tlsv10]).unwrap(); tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
let tls_parameters = let tls_parameters =
ClientTlsParameters::new( ClientTlsParameters::new(
"smtp.example.com".to_string(), "smtp.example.com".to_string(),
tls_builder.build().unwrap() tls_builder.build().unwrap()
); );
let mut mailer = SmtpTransportBuilder::new( let mut mailer = SmtpClient::new(
("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters) ("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
) ).unwrap()
.expect("Failed to create transport")
.authentication_mechanism(Mechanism::Login) .authentication_mechanism(Mechanism::Login)
.credentials(Credentials::new( .credentials(Credentials::new(
"example_username".to_string(), "example_password".to_string() "example_username".to_string(), "example_password".to_string()
)) ))
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited) .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.build(); .transport();
let result = mailer.send(email);
let result = mailer.send(&email);
assert!(result.is_ok()); assert!(result.is_ok());
mailer.close(); mailer.close();
} }
``` ```
@@ -143,13 +157,13 @@ extern crate lettre;
use lettre::EmailAddress; use lettre::EmailAddress;
use lettre::smtp::SMTP_PORT; use lettre::smtp::SMTP_PORT;
use lettre::smtp::client::Client; use lettre::smtp::client::InnerClient;
use lettre::smtp::client::net::NetworkStream; use lettre::smtp::client::net::NetworkStream;
use lettre::smtp::extension::ClientId; use lettre::smtp::extension::ClientId;
use lettre::smtp::commands::*; use lettre::smtp::commands::*;
fn main() { fn main() {
let mut email_client: Client<NetworkStream> = Client::new(); let mut email_client: InnerClient<NetworkStream> = InnerClient::new();
let _ = email_client.connect(&("localhost", SMTP_PORT), None); let _ = email_client.connect(&("localhost", SMTP_PORT), None);
let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string()))); let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
let _ = email_client.command( let _ = email_client.command(

View File

@@ -0,0 +1,32 @@
#### Stub Transport
The stub transport only logs message envelope and drops the content. It can be useful for
testing purposes.
```rust
extern crate lettre;
use lettre::stub::StubTransport;
use lettre::{SendableEmail, Envelope, EmailAddress, Transport};
fn main() {
let email = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
).unwrap(),
"id".to_string(),
"Hello world".to_string().into_bytes(),
);
let mut sender = StubTransport::new_positive();
let result = sender.send(email);
assert!(result.is_ok());
}
```
Will log (when using a logger like `env_logger`):
```text
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB