Compare commits

..

117 Commits

Author SHA1 Message Date
Alexis Mousset
54032b5ce5 fix(builder): lettre_email 0.9.4 2020-04-21 22:05:54 +02:00
Alexis Mousset
6a40f4a5fe fix(builder): Go back to email 0.0.20 2020-04-21 22:04:55 +02:00
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
55 changed files with 1544 additions and 1653 deletions

View File

@@ -4,7 +4,7 @@ set -xe
cd website
make clean && make
echo "lettre.at" > _book/CNAME
echo "lettre.at" > _book/html/CNAME
sudo pip install ghp-import
ghp-import -n _book
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
ghp-import -n _book/html
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
- beta
- nightly
- 1.20.0
- 1.32.0
matrix:
allow_failures:
- rust: nightly
@@ -19,13 +19,11 @@ addons:
- gcc
- binutils-dev
- libiberty-dev
- npm
before_script:
- smtp-sink 2525 1000&
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
- sudo npm set strict-ssl false && sudo npm install -g gitbook-cli
script:
- cargo test --verbose --all
- cargo test --verbose --all --all-features
after_success:
- ./.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,3 +1,69 @@
<a name="v0.9.4"></a>
### v0.9.4 (2020-04-21)
#### Bug Fixes
* **email**
* Go back to `rust-email` 0.0.20 as upgrade broke message formatting ([6a40f4a](https://github.com/lettre/lettre/commit/6a40f4a)
<a name="v0.9.3"></a>
### v0.9.3 (2020-04-19)
#### Bug Fixes
* **all:**
* Fix compilation warnings ([9b591ff](https://github.com/lettre/lettre/commit/9b591ff932e35947f793aaaeec0e3f06e8818449))
* **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>
### v0.8.2 (2018-05-03)

View File

@@ -34,19 +34,18 @@ Lettre provides the following features:
## 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`:
```toml
[dependencies]
lettre = "0.8"
lettre_email = "0.8"
lettre = "0.9"
lettre_email = "0.9"
```
```rust,no_run
extern crate lettre;
extern crate lettre_email;
extern crate mime;
use lettre::{EmailTransport, SmtpTransport};
use lettre_email::EmailBuilder;
@@ -60,7 +59,6 @@ fn main() {
.from("user@example.com")
.subject("Hi, Hello world")
.text("Hello world.")
.attachment(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN).unwrap()
.build()
.unwrap();

View File

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

View File

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

View File

@@ -1,24 +1,25 @@
extern crate env_logger;
extern crate lettre;
use lettre::{EmailTransport, SimpleSendableEmail, SmtpTransport};
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
fn main() {
env_logger::init();
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"my-message-id".to_string(),
"Hello ß☺ example".to_string(),
).unwrap();
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 ß☺ example".to_string().into_bytes(),
);
// Open a local connection on port 25
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
.unwrap()
.build();
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email
let result = mailer.send(&email);
let result = mailer.send(email);
if result.is_ok() {
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 serde_json;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds.
#[derive(Debug)]
@@ -19,20 +21,16 @@ pub enum Error {
impl Display for 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 {
fn description(&self) -> &str {
match *self {
Client(err) => err,
Io(ref err) => err.description(),
JsonSerialization(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
@@ -43,19 +41,19 @@ impl StdError for Error {
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
Error::Io(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
JsonSerialization(err)
Error::JsonSerialization(err)
}
}
impl From<&'static str> for 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.
//!
use EmailTransport;
use SendableEmail;
use SimpleSendableEmail;
use file::error::FileResult;
use serde_json;
use std::fs::File;
use std::io::Read;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use Envelope;
use SendableEmail;
use Transport;
pub mod error;
/// Writes the content and the envelope information to a file
#[derive(Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct FileEmailTransport {
pub struct FileTransport {
path: PathBuf,
}
impl FileEmailTransport {
impl FileTransport {
/// Creates a new transport to the given directory
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport {
let mut path_buf = PathBuf::new();
path_buf.push(path);
FileEmailTransport { path: path_buf }
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport {
path: PathBuf::from(path.as_ref()),
}
}
}
impl<'a, T: Read + 'a> EmailTransport<'a, T, FileResult> for FileEmailTransport {
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> FileResult {
#[derive(PartialEq, Eq, Clone, Debug)]
#[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();
file.push(format!("{}.txt", email.message_id()));
file.push(format!("{}.json", message_id));
let mut f = File::create(file.as_path())?;
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
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())?;
let serialized = serde_json::to_string(&SerializableEmail {
envelope,
message_id,
message: email.message_to_string()?.as_bytes().to_vec(),
})?;
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
Ok(())
}
}

View File

@@ -1,102 +1,80 @@
//! 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
//! emails have to implement `SendableEmail`.
//! This mailer contains the available transports for your emails.
//!
#![doc(html_root_url = "https://docs.rs/lettre/0.8.2")]
#![deny(missing_docs, missing_debug_implementations, missing_copy_implementations, trivial_casts,
trivial_numeric_casts, unsafe_code, unstable_features, unused_import_braces,
unused_qualifications)]
#![doc(html_root_url = "https://docs.rs/lettre/0.9.3")]
#![deny(
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces
)]
#[cfg(feature = "smtp-transport")]
extern crate base64;
#[cfg(feature = "smtp-transport")]
extern crate bufstream;
#[cfg(feature = "crammd5-auth")]
extern crate hex;
#[cfg(feature = "crammd5-auth")]
extern crate hmac;
#[cfg(feature = "smtp-transport")]
extern crate hostname;
#[macro_use]
extern crate log;
#[cfg(feature = "crammd5-auth")]
extern crate md5;
#[cfg(feature = "smtp-transport")]
extern crate native_tls;
#[cfg(feature = "smtp-transport")]
#[macro_use]
extern crate nom;
#[cfg(feature = "serde-impls")]
extern crate serde;
#[cfg(feature = "serde-impls")]
#[macro_use]
extern crate serde_derive;
extern crate fast_chemail;
#[cfg(feature = "connection-pool")]
extern crate r2d2;
#[cfg(feature = "file-transport")]
extern crate serde_json;
#[cfg(feature = "smtp-transport")]
pub mod smtp;
#[cfg(feature = "sendmail-transport")]
pub mod sendmail;
pub mod stub;
pub mod error;
#[cfg(feature = "file-transport")]
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")]
pub use file::FileEmailTransport;
pub use file::FileTransport;
#[cfg(feature = "sendmail-transport")]
pub use sendmail::SendmailTransport;
#[cfg(feature = "smtp-transport")]
pub use smtp::{ClientSecurity, SmtpTransport};
#[cfg(feature = "smtp-transport")]
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::io;
use std::io::Cursor;
use std::io::Read;
use std::error::Error as StdError;
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
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EmailAddress(String);
impl EmailAddress {
/// Creates a new `EmailAddress`. For now it makes no validation.
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))
}
}
@@ -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
///
/// 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> {
self.reverse_path.as_ref()
}
}
/// Creates a new builder
pub fn builder() -> EnvelopeBuilder {
EnvelopeBuilder::new()
pub enum Message {
Reader(Box<dyn Read + Send>),
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
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct EnvelopeBuilder {
/// The envelope recipients' addresses
to: Vec<EmailAddress>,
/// The envelope sender address
from: Option<EmailAddress>,
/// Sendable email structure
pub struct SendableEmail {
envelope: Envelope,
message_id: String,
message: Message,
}
impl EnvelopeBuilder {
/// Constructs an envelope with no recipients and an empty sender
pub fn new() -> Self {
EnvelopeBuilder {
to: vec![],
from: None,
impl SendableEmail {
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Bytes(Cursor::new(message)),
}
}
/// Adds a recipient
pub fn to<S: Into<EmailAddress>>(mut self, address: S) -> Self {
self.add_to(address);
self
pub fn new_with_reader(
envelope: Envelope,
message_id: String,
message: Box<dyn Read + Send>,
) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Reader(message),
}
}
/// Adds a recipient
pub fn add_to<S: Into<EmailAddress>>(&mut self, address: S) {
self.to.push(address.into());
pub fn envelope(&self) -> &Envelope {
&self.envelope
}
/// Sets the sender
pub fn from<S: Into<EmailAddress>>(mut self, address: S) -> Self {
self.set_from(address);
self
pub fn message_id(&self) -> &str {
&self.message_id
}
/// Sets the sender
pub fn set_from<S: Into<EmailAddress>>(&mut self, address: S) {
self.from = Some(address.into());
pub fn message(self) -> Message {
self.message
}
/// Build the envelope
pub fn build(self) -> EmailResult<Envelope> {
Envelope::new(self.from, self.to)
pub fn message_to_string(mut self) -> Result<String, io::Error> {
let mut message_content = String::new();
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
pub trait EmailTransport<'a, U: Read + 'a, V> {
pub trait Transport<'a> {
/// Result type for the transport
type Result;
/// Sends the email
fn send<T: SendableEmail<'a, U> + 'a>(&mut self, email: &'a T) -> V;
}
/// 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())
}
fn send(&mut self, email: SendableEmail) -> Self::Result;
}

View File

@@ -1,9 +1,11 @@
//! Error and result type for sendmail transport
use self::Error::*;
use std::error::Error as StdError;
use std::fmt::{self, Display, Formatter};
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds.
#[derive(Debug)]
@@ -16,19 +18,15 @@ pub enum Error {
impl Display for 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 {
fn description(&self) -> &str {
match *self {
Client(err) => err,
Io(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&StdError> {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Io(ref err) => Some(&*err),
_ => None,
@@ -38,13 +36,13 @@ impl StdError for Error {
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
Error::Io(err)
}
}
impl From<&'static str> for 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.
//!
use {EmailTransport, SendableEmail};
use sendmail::error::SendmailResult;
use std::io::Read;
use std::io::prelude::*;
use std::io::Read;
use std::process::{Command, Stdio};
use SendableEmail;
use Transport;
pub mod error;
@@ -32,22 +33,24 @@ impl SendmailTransport {
}
}
impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTransport {
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SendmailResult {
let envelope = email.envelope();
impl<'a> Transport<'a> for SendmailTransport {
type Result = SendmailResult;
fn send(&mut self, email: SendableEmail) -> SendmailResult {
let message_id = email.message_id().to_string();
// 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)
.args(&[
"-i",
"-f",
&match envelope.from() {
Some(address) => address.to_string(),
None => "\"\"".to_string(),
},
&to_addresses.join(" "),
])
.arg("-i")
.arg("-f")
.arg(
email
.envelope()
.from()
.map(|x| x.as_ref())
.unwrap_or("\"\""),
)
.args(email.envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
@@ -55,26 +58,21 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTranspo
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
match process
process
.stdin
.as_mut()
.unwrap()
.write_all(message_content.as_bytes())
{
Ok(_) => (),
Err(error) => return Err(From::from(error)),
}
.write_all(message_content.as_bytes())?;
info!("Wrote message to stdin");
info!("Wrote {} message to stdin", message_id);
if let Ok(output) = process.wait_with_output() {
if output.status.success() {
Ok(())
} else {
Err(From::from("The message could not be sent"))
}
let output = process.wait_with_output()?;
if output.status.success() {
Ok(())
} 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 std::fmt::{self, Display, Formatter};
/// Accepted authentication mechanisms on an encrypted connection
/// 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];
/// 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] = &[];
/// 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)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Credentials {
username: String,
password: String,
authentication_identity: String,
secret: String,
}
impl Credentials {
/// Create a `Credentials` struct from username and password
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)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login,
/// CRAM-MD5 authentication mechanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195
#[cfg(feature = "crammd5-auth")]
CramMd5,
/// Non-standard XOAUTH2 mechanism
/// https://developers.google.com/gmail/imap/xoauth2-protocol
Xoauth2,
}
impl Display for Mechanism {
@@ -87,8 +71,7 @@ impl Display for Mechanism {
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => "CRAM-MD5",
Mechanism::Xoauth2 => "XOAUTH2",
}
)
}
@@ -96,64 +79,49 @@ impl Display for Mechanism {
impl Mechanism {
/// Does the mechanism supports initial response
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
pub fn supports_initial_response(&self) -> bool {
match *self {
Mechanism::Plain => true,
pub fn supports_initial_response(self) -> bool {
match self {
Mechanism::Plain | Mechanism::Xoauth2 => true,
Mechanism::Login => false,
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => false,
}
}
/// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases
pub fn response(
&self,
self,
credentials: &Credentials,
challenge: Option<&str>,
) -> Result<String, Error> {
match *self {
match self {
Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"{}{}{}{}",
NUL, credentials.username, NUL, credentials.password
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
)),
},
Mechanism::Login => {
let decoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
let decoded_challenge =
challenge.ok_or(Error::Client("This mechanism does expect a 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) {
return Ok(credentials.password.to_string());
return Ok(credentials.secret.to_string());
}
Err(Error::Client("Unrecognized challenge"))
}
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => {
let decoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
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())
))
}
Mechanism::Xoauth2 => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"user={}\x01auth=Bearer {}\x01\x01",
credentials.authentication_identity, credentials.secret
)),
},
}
}
}
@@ -193,21 +161,18 @@ mod test {
}
#[test]
#[cfg(feature = "crammd5-auth")]
fn test_cram_md5() {
let mechanism = Mechanism::CramMd5;
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
let credentials = Credentials::new(
"username".to_string(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
);
assert_eq!(
mechanism
.response(
&credentials,
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")
)
.unwrap(),
"alice a540ebe4ef2304070bbc3c456c1f64c0"
mechanism.response(&credentials, None).unwrap(),
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
);
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 nom::ErrorKind as NomErrorKind;
use smtp::{CRLF, MESSAGE_ENDING};
use smtp::authentication::{Credentials, Mechanism};
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
use smtp::commands::*;
@@ -14,8 +13,8 @@ use std::net::ToSocketAddrs;
use std::string::String;
use std::time::Duration;
pub mod net;
pub mod mock;
pub mod net;
/// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)]
@@ -72,12 +71,12 @@ impl ClientCodec {
/// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays
fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CRLF>")
string.replace("\r\n", "<CRLF>")
}
/// Structure that implements the SMTP client
#[derive(Debug, Default)]
pub struct Client<S: Write + Read = NetworkStream> {
pub struct InnerClient<S: Write + Read = NetworkStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<BufStream<S>>,
@@ -89,17 +88,17 @@ macro_rules! return_err (
})
);
#[cfg_attr(feature = "cargo-clippy", allow(new_without_default_derive))]
impl<S: Write + Read> Client<S> {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
impl<S: Write + Read> InnerClient<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new() -> Client<S> {
Client { stream: None }
pub fn new() -> InnerClient<S> {
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
pub fn close(&mut self) {
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
#[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 {
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
@@ -194,10 +193,11 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
}
/// 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 codec = ClientCodec::new();
let mut message_reader = BufReader::new(message.as_mut());
let mut message_reader = BufReader::new(message);
loop {
out_buf.clear();
@@ -218,7 +218,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
self.write(out_buf.as_slice())?;
}
self.write(MESSAGE_ENDING.as_bytes())?;
self.write(b"\r\n.\r\n")?;
self.read_response()
}
@@ -254,7 +254,13 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
break;
}
// 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>();
}

View File

@@ -24,8 +24,8 @@ impl ClientTlsParameters {
}
/// Accepted protocols by default.
/// This removes TLS 1.0 compared to tls-native defaults.
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv11, Protocol::Tlsv12];
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv12];
#[derive(Debug)]
/// 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<()> {
*self = match *self {
NetworkStream::Tcp(ref mut stream) => match tls_parameters
@@ -134,7 +134,7 @@ impl Connector for NetworkStream {
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 {
match *self {
NetworkStream::Tcp(_) => false,

View File

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

@@ -37,47 +37,41 @@ pub enum Error {
/// TLS error
Tls(native_tls::Error),
/// Parsing error
Parsing(nom::simple_errors::Err),
Parsing(nom::ErrorKind),
}
impl Display for 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 {
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
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> {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
ChallengeParsing(ref err) => Some(&*err),
Utf8Parsing(ref err) => Some(&*err),
Io(ref err) => Some(&*err),
Tls(ref err) => Some(&*err),
Parsing(ref err) => Some(&*err),
_ => None,
}
}
@@ -95,12 +89,24 @@ impl From<native_tls::Error> for Error {
}
}
impl From<nom::simple_errors::Err> for Error {
fn from(err: nom::simple_errors::Err) -> Error {
impl From<nom::ErrorKind> for Error {
fn from(err: nom::ErrorKind) -> Error {
Parsing(err)
}
}
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 {
fn from(response: Response) -> Error {
match response.code.severity {

View File

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

View File

@@ -8,33 +8,33 @@
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and
//! CRAM-MD5 mechanisms
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
use EmailTransport;
use SendableEmail;
use native_tls::TlsConnector;
use smtp::authentication::{Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS,
DEFAULT_UNENCRYPTED_MECHANISMS};
use smtp::client::Client;
use smtp::authentication::{
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
};
use smtp::client::net::ClientTlsParameters;
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
use smtp::client::InnerClient;
use smtp::commands::*;
use smtp::error::{Error, SmtpResult};
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
use std::io::Read;
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use {SendableEmail, Transport};
pub mod extension;
pub mod commands;
pub mod authentication;
pub mod response;
pub mod client;
pub mod commands;
pub mod error;
pub mod extension;
#[cfg(feature = "connection-pool")]
pub mod r2d2;
pub mod response;
pub mod util;
// Registered port numbers:
@@ -43,39 +43,22 @@ pub mod util;
/// Default smtp port
pub const SMTP_PORT: u16 = 25;
/// Default submission port
pub const SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub const SP: &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";
/// Default submission over TLS port
pub const SUBMISSIONS_PORT: u16 = 465;
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub enum ClientSecurity {
/// Insecure connection
/// Insecure connection only (for testing purposes)
None,
/// Use `STARTTLS` when available
/// Start with insecure connection and use `STARTTLS` when available
Opportunistic(ClientTlsParameters),
/// Always use `STARTTLS`
/// Start with insecure connection and require `STARTTLS`
Required(ClientTlsParameters),
/// Use TLS wrapped connection without negotiation
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
/// Use TLS wrapped connection
Wrapper(ClientTlsParameters),
}
@@ -93,7 +76,8 @@ pub enum ConnectionReuseParameters {
/// Contains client configuration
#[allow(missing_debug_implementations)]
pub struct SmtpTransportBuilder {
#[derive(Clone)]
pub struct SmtpClient {
/// Enable connection reuse
connection_reuse: ConnectionReuseParameters,
/// Name sent during EHLO
@@ -114,7 +98,7 @@ pub struct SmtpTransportBuilder {
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpTransportBuilder {
impl SmtpClient {
/// Creates a new SMTP client
///
/// Defaults are:
@@ -123,14 +107,13 @@ impl SmtpTransportBuilder {
/// * No authentication
/// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands
pub fn new<A: ToSocketAddrs>(
addr: A,
security: ClientSecurity,
) -> Result<SmtpTransportBuilder, Error> {
///
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
let mut addresses = addr.to_socket_addrs()?;
match addresses.next() {
Some(addr) => Ok(SmtpTransportBuilder {
Some(addr) => Ok(SmtpClient {
server_addr: addr,
security,
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
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder {
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
self.smtp_utf8 = enabled;
self
}
/// 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
}
/// Enable connection reuse
pub fn connection_reuse(
mut self,
parameters: ConnectionReuseParameters,
) -> SmtpTransportBuilder {
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
self.connection_reuse = parameters;
self
}
/// 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
}
/// 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
}
/// 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
}
@@ -186,7 +187,7 @@ impl SmtpTransportBuilder {
/// Build the SMTP client
///
/// 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)
}
}
@@ -209,9 +210,9 @@ pub struct SmtpTransport {
/// SmtpTransport variable states
state: State,
/// Information about the client
client_info: SmtpTransportBuilder,
client_info: SmtpClient,
/// Low level client
client: Client,
client: InnerClient,
}
macro_rules! try_smtp (
@@ -230,40 +231,11 @@ macro_rules! try_smtp (
);
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
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
let client = Client::new();
pub fn new(builder: SmtpClient) -> SmtpTransport {
let client = InnerClient::new();
SmtpTransport {
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
fn ehlo(&mut self) -> SmtpResult {
// 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
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms, cyclomatic_complexity))]
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SmtpResult {
// Extract email information
let message_id = email.message_id();
let envelope = email.envelope();
#[cfg_attr(
feature = "cargo-clippy",
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
)]
fn send(&mut self, email: SendableEmail) -> SmtpResult {
let message_id = email.message_id().to_string();
// 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 {
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");
}
}
if !self.client.is_connected() {
self.connect()?;
}
// Mail
let mut mail_options = vec![];
if self.server_info
if self
.server_info
.as_ref()
.unwrap()
.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));
}
if self.server_info
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8
.supports_feature(Extension::SmtpUtfEight)
&& self.client_info.smtp_utf8
{
mail_options.push(MailParameter::SmtpUtfEight);
}
try_smtp!(
self.client
.command(MailCommand::new(envelope.from().cloned(), mail_options,)),
self.client.command(MailCommand::new(
email.envelope().from().cloned(),
mail_options,
)),
self
);
@@ -426,17 +420,17 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
info!(
"{}: from=<{}>",
message_id,
match envelope.from() {
match email.envelope().from() {
Some(address) => address.to_string(),
None => "".to_string(),
}
);
// Recipient
for to_address in envelope.to() {
for to_address in email.envelope().to() {
try_smtp!(
self.client
.command(RcptCommand::new(to_address.clone(), vec![]),),
.command(RcptCommand::new(to_address.clone(), vec![])),
self
);
// 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);
// Message content
let result = self.client.message(email.message());
let result = self.client.message(Box::new(email.message()));
if result.is_ok() {
// 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

@@ -1,11 +1,10 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use nom::{crlf, ErrorKind as NomErrorKind, IResult as NomResult};
use nom::simple_errors::Err as NomError;
use nom::{crlf, ErrorKind as NomErrorKind};
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::{FromStr, from_utf8};
use std::str::{from_utf8, FromStr};
/// First digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
@@ -126,13 +125,12 @@ pub struct Response {
}
impl FromStr for Response {
type Err = NomError;
type Err = NomErrorKind;
fn from_str(s: &str) -> result::Result<Response, NomError> {
fn from_str(s: &str) -> result::Result<Response, NomErrorKind> {
match parse_response(s.as_bytes()) {
NomResult::Done(_, res) => Ok(res),
NomResult::Error(e) => Err(e),
NomResult::Incomplete(_) => Err(NomErrorKind::Complete),
Ok((_, res)) => Ok(res),
Err(e) => Err(e.into_error_kind()),
}
}
}
@@ -333,20 +331,19 @@ mod test {
#[test]
fn test_response_is_positive() {
assert!(
Response::new(
Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).is_positive()
);
assert!(Response::new(
Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.is_positive());
assert!(!Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
@@ -358,25 +355,25 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).is_positive());
)
.is_positive());
}
#[test]
fn test_response_has_code() {
assert!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).has_code(451)
);
assert!(Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.has_code(451));
assert!(!Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
@@ -388,7 +385,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).has_code(251));
)
.has_code(251));
}
#[test]
@@ -405,7 +403,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).first_word(),
)
.first_word(),
Some("me")
);
assert_eq!(
@@ -420,7 +419,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).first_word(),
)
.first_word(),
Some("me")
);
assert_eq!(
@@ -431,7 +431,8 @@ mod test {
detail: Detail::One,
},
vec![],
).first_word(),
)
.first_word(),
None
);
assert_eq!(
@@ -442,7 +443,8 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
).first_word(),
)
.first_word(),
None
);
assert_eq!(
@@ -453,7 +455,8 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
).first_word(),
)
.first_word(),
None
);
assert_eq!(
@@ -464,7 +467,8 @@ mod test {
detail: Detail::One,
},
vec!["".to_string()],
).first_word(),
)
.first_word(),
None
);
}
@@ -483,7 +487,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).first_line(),
)
.first_line(),
Some("me")
);
assert_eq!(
@@ -498,7 +503,8 @@ mod test {
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
).first_line(),
)
.first_line(),
Some("me mo")
);
assert_eq!(
@@ -509,7 +515,8 @@ mod test {
detail: Detail::One,
},
vec![],
).first_line(),
)
.first_line(),
None
);
assert_eq!(
@@ -520,7 +527,8 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
).first_line(),
)
.first_line(),
Some(" ")
);
assert_eq!(
@@ -531,7 +539,8 @@ mod test {
detail: Detail::One,
},
vec![" ".to_string()],
).first_line(),
)
.first_line(),
Some(" ")
);
assert_eq!(
@@ -542,7 +551,8 @@ mod test {
detail: Detail::One,
},
vec!["".to_string()],
).first_line(),
)
.first_line(),
Some("")
);
}

View File

@@ -2,42 +2,42 @@
//! testing purposes.
//!
use EmailTransport;
use SendableEmail;
use std::io::Read;
use Transport;
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
pub struct StubEmailTransport {
pub struct StubTransport {
response: StubResult,
}
impl StubEmailTransport {
impl StubTransport {
/// Creates a new transport that always returns the given response
pub fn new(response: StubResult) -> StubEmailTransport {
StubEmailTransport { response }
pub fn new(response: StubResult) -> StubTransport {
StubTransport { response }
}
/// Creates a new transport that always returns a success response
pub fn new_positive() -> StubEmailTransport {
StubEmailTransport { response: Ok(()) }
pub fn new_positive() -> StubTransport {
StubTransport { response: Ok(()) }
}
}
/// SMTP result type
pub type StubResult = Result<(), ()>;
impl<'a, T: Read + 'a> EmailTransport<'a, T, StubResult> for StubEmailTransport {
fn send<U: SendableEmail<'a, T>>(&mut self, email: &'a U) -> StubResult {
let envelope = email.envelope();
impl<'a> Transport<'a> for StubTransport {
type Result = StubResult;
fn send(&mut self, email: SendableEmail) -> StubResult {
info!(
"{}: from=<{}> to=<{:?}>",
email.message_id(),
match envelope.from() {
match email.envelope().from() {
Some(address) => address.to_string(),
None => "".to_string(),
},
envelope.to()
email.envelope().to()
);
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;
use self::glob::glob;
use std::env::consts::EXE_EXTENSION;
use std::env;
use std::env::consts::EXE_EXTENSION;
use std::path::Path;
use std::process::Command;
/*
#[test]
fn test_readme() {
let readme = Path::new(file!())
@@ -19,6 +20,7 @@ fn test_readme() {
skeptic_test(&readme);
}
*/
#[test]
fn book_test() {
@@ -50,7 +52,8 @@ fn skeptic_test(path: &Path) {
.arg(&depdir)
.arg(path);
let result = cmd.spawn()
let result = cmd
.spawn()
.expect("Failed to spawn process")
.wait()
.expect("Failed to run process");

View File

@@ -4,34 +4,38 @@ extern crate lettre;
#[cfg(feature = "file-transport")]
mod test {
use lettre::{EmailTransport, SendableEmail, SimpleSendableEmail};
use lettre::file::FileEmailTransport;
use lettre::file::FileTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
use std::env::temp_dir;
use std::fs::File;
use std::fs::remove_file;
use std::fs::File;
use std::io::Read;
#[test]
fn file_transport() {
let mut sender = FileEmailTransport::new(temp_dir());
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"file_id".to_string(),
"Hello file".to_string(),
).unwrap();
let result = sender.send(&email);
let mut sender = FileTransport::new(temp_dir());
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 ß☺ example".to_string().into_bytes(),
);
let message_id = email.message_id().to_string();
let result = sender.send(email);
assert!(result.is_ok());
let message_id = email.message_id();
let file = format!("{}/{}.txt", temp_dir().to_str().unwrap(), message_id);
let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
let mut f = File::open(file.clone()).unwrap();
let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer);
assert_eq!(
buffer,
"{\"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();

View File

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

View File

@@ -3,22 +3,25 @@ extern crate lettre;
#[cfg(test)]
#[cfg(feature = "smtp-transport")]
mod test {
use lettre::{ClientSecurity, EmailTransport, SimpleSendableEmail, SmtpTransport};
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
#[test]
fn smtp_transport_simple() {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.build();
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"smtp_id".to_string(),
"Hello smtp".to_string(),
).unwrap();
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 ß☺ example".to_string().into_bytes(),
);
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;
use lettre::{EmailTransport, SimpleSendableEmail};
use lettre::stub::StubEmailTransport;
use lettre::stub::StubTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
#[test]
fn stub_transport() {
let mut sender_ok = StubEmailTransport::new_positive();
let mut sender_ko = StubEmailTransport::new(Err(()));
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"stub_id".to_string(),
"Hello stub".to_string(),
).unwrap();
let mut sender_ok = StubTransport::new_positive();
let mut sender_ko = StubTransport::new(Err(()));
let email_ok = 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(),
);
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_ko.send(&email).unwrap_err();
sender_ok.send(email_ok).unwrap();
sender_ko.send(email_ko).unwrap_err();
}

View File

@@ -1,7 +1,7 @@
[package]
name = "lettre_email"
version = "0.8.2" # remember to update html_root_url
version = "0.9.4" # remember to update html_root_url
description = "Email builder"
readme = "README.md"
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" }
[dev-dependencies]
lettre = { version = "^0.8", path = "../lettre", features = ["smtp-transport"] }
glob = "0.2"
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
glob = "0.3"
[dependencies]
email = "^0.0"
email = "^0.0.20"
mime = "^0.3"
time = "^0.1"
uuid = { version = "^0.6", features = ["v4"] }
lettre = { version = "^0.8", path = "../lettre", default-features = false }
base64 = "^0.9"
uuid = { version = "^0.7", features = ["v4"] }
lettre = { version = "^0.9", path = "../lettre", default-features = false }
base64 = "^0.10"

View File

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

View File

@@ -1,17 +1,18 @@
//! 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 std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
use self::Error::*;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Envelope error
Email(lettre::Error),
Envelope(lettre::error::Error),
/// Unparseable filename for attachment
CannotParseFilename,
/// IO error
@@ -19,29 +20,34 @@ pub enum Error {
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
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 {
fn description(&self) -> &str {
fn cause(&self) -> Option<&dyn StdError> {
match *self {
Email(ref err) => err.description(),
CannotParseFilename => "the attachment filename could not be parsed",
Io(ref err) => err.description(),
Envelope(ref err) => Some(err),
Io(ref err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
Error::Io(err)
}
}
impl From<lettre::Error> for Error {
fn from(err: lettre::Error) -> Error {
Email(err)
impl From<lettre::error::Error> for Error {
fn from(err: lettre::error::Error) -> Error {
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;
use self::glob::glob;
use std::env::consts::EXE_EXTENSION;
use std::env;
use std::env::consts::EXE_EXTENSION;
use std::path::Path;
use std::process::Command;
@@ -36,7 +36,8 @@ fn skeptic_test(path: &Path) {
.arg(&depdir)
.arg(path);
let result = cmd.spawn()
let result = cmd
.spawn()
.expect("Failed to spawn process")
.wait()
.expect("Failed to run process");

1
website/.gitignore vendored
View File

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

View File

@@ -1,13 +1,16 @@
all: depends _book
depends:
gitbook install
cargo install --force mdbook --vers "^0.2"
cargo install --force mdbook-linkcheck --vers "^0.2"
serve:
gitbook serve
mdbook serve
_book:
gitbook build
mdbook build
clean:
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,116 +0,0 @@
SMTP Transport
This transport uses the SMTP protocol to send emails over the network (locally or remotely).
It is designed to be:
* Secured: email are encrypted by default
* Modern: Unicode support for email content and sender/recipient addresses when compatible
* Fast: supports tcp connection reuse
This client is designed to send emails to a relay server, and should *not* be used to send
emails directly to the destination.
The relay server can be the local email server, a specific host or a third-party service.
#### Simple example
This is the most basic example of usage:
```rust,no_run
extern crate lettre;
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport};
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
// Open a local connection on port 25
let mut mailer =
SmtpTransport::builder_unencrypted_localhost().unwrap().build();
// Send the email
let result = mailer.send(&email);
assert!(result.is_ok());
}
```
#### Complete example
```rust,no_run
extern crate lettre;
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::{SimpleSendableEmail, EmailTransport, SmtpTransport};
use lettre::smtp::extension::ClientId;
use lettre::smtp::ConnectionReuseParameters;
fn main() {
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
// Connect to a remote server on a custom port
let mut mailer = SmtpTransport::simple_builder("server.tld").unwrap()
// Set the name sent during EHLO/HELO, default is `localhost`
.hello_name(ClientId::Domain("my.hostname.tld".to_string()))
// Add credentials for authentication
.credentials(Credentials::new("username".to_string(), "password".to_string()))
// Enable SMTPUTF8 if the server supports it
.smtp_utf8(true)
// Configure expected authentication mechanism
.authentication_mechanism(Mechanism::Plain)
// Enable connection reuse
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).build();
let result_1 = mailer.send(&email);
assert!(result_1.is_ok());
// The second email will use the same connection
let result_2 = mailer.send(&email);
assert!(result_2.is_ok());
// Explicitly close the SMTP transaction as we enabled connection reuse
mailer.close();
}
```
#### Lower level
You can also send commands, here is a simple email transaction without
error handling:
```rust,no_run
extern crate lettre;
use lettre::EmailAddress;
use lettre::smtp::SMTP_PORT;
use lettre::smtp::client::Client;
use lettre::smtp::client::net::NetworkStream;
use lettre::smtp::extension::ClientId;
use lettre::smtp::commands::*;
fn main() {
let mut email_client: Client<NetworkStream> = Client::new();
let _ = email_client.connect(&("localhost", SMTP_PORT), None);
let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
let _ = email_client.command(
MailCommand::new(Some(EmailAddress::new("user@example.com".to_string()).unwrap()), vec![])
);
let _ = email_client.command(
RcptCommand::new(EmailAddress::new("user@example.org".to_string()).unwrap(), vec![])
);
let _ = email_client.command(DataCommand);
let _ = email_client.message(Box::new("Test email".as_bytes()));
let _ = email_client.command(QuitCommand);
}
```

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`
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
[dependencies]
lettre = "0.8"
lettre_email = "0.8"
lettre = "0.9"
lettre_email = "0.9"
```

View File

@@ -10,17 +10,17 @@ An email is built using an `EmailBuilder`. The simplest email could be:
```rust
extern crate lettre_email;
use lettre_email::EmailBuilder;
use lettre_email::Email;
fn main() {
// Create an email
let email = EmailBuilder::new()
let email = Email::builder()
// Addresses can be specified by the tuple (email, alias)
.to(("user@example.org", "Firstname Lastname"))
// ... or by an address only
.from("user@example.com")
.subject("Hi, Hello world")
.text("Hello world.")
.alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
.build();
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
HTML email. You can use the `alternative()` method to provide both versions, using plain text
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 lettre::file::FileEmailTransport;
use lettre::{SimpleSendableEmail, EmailTransport};
use lettre::file::FileTransport;
use lettre::{Transport, Envelope, EmailAddress, SendableEmail};
fn main() {
// Write to the local temp directory
let mut sender = FileEmailTransport::new(temp_dir());
let email = SimpleSendableEmail::new(
"user@localhost".to_string(),
&["root@localhost".to_string()],
"message_id".to_string(),
"Hello world".to_string(),
).unwrap();
let mut sender = FileTransport::new(temp_dir());
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 result = sender.send(&email);
let result = sender.send(email);
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

@@ -0,0 +1,180 @@
#### SMTP Transport
This transport uses the SMTP protocol to send emails over the network (locally or remotely).
It is designed to be:
* Secured: email are encrypted by default
* Modern: Unicode support for email content and sender/recipient addresses when compatible
* Fast: supports tcp connection reuse
This client is designed to send emails to a relay server, and should *not* be used to send
emails directly to the destination.
The relay server can be the local email server, a specific host or a third-party service.
#### Simple example
This is the most basic example of usage:
```rust,no_run
extern crate lettre;
use lettre::{SendableEmail, EmailAddress, Transport, Envelope, SmtpClient};
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(),
);
// Open a local connection on port 25
let mut mailer =
SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email
let result = mailer.send(email);
assert!(result.is_ok());
}
```
#### Complete example
```rust,no_run
extern crate lettre;
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::{SendableEmail, Envelope, EmailAddress, Transport, SmtpClient};
use lettre::smtp::extension::ClientId;
use lettre::smtp::ConnectionReuseParameters;
fn main() {
let email_1 = SendableEmail::new(
Envelope::new(
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
vec![EmailAddress::new("root@localhost".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
let mut mailer = SmtpClient::new_simple("server.tld").unwrap()
// Set the name sent during EHLO/HELO, default is `localhost`
.hello_name(ClientId::Domain("my.hostname.tld".to_string()))
// Add credentials for authentication
.credentials(Credentials::new("username".to_string(), "password".to_string()))
// Enable SMTPUTF8 if the server supports it
.smtp_utf8(true)
// Configure expected authentication mechanism
.authentication_mechanism(Mechanism::Plain)
// Enable connection reuse
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
let result_1 = mailer.send(email_1);
assert!(result_1.is_ok());
// The second email will use the same connection
let result_2 = mailer.send(email_2);
assert!(result_2.is_ok());
// Explicitly close the SMTP transaction as we enabled connection reuse
mailer.close();
}
```
You can specify custom TLS settings:
```rust,no_run
extern crate native_tls;
extern crate lettre;
extern crate lettre_email;
use lettre::{
ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
SendableEmail, SmtpClient, Transport,
};
use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre::smtp::ConnectionReuseParameters;
use native_tls::{Protocol, TlsConnector};
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(),
"message_id".to_string(),
"Hello world".to_string().into_bytes(),
);
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
let tls_parameters =
ClientTlsParameters::new(
"smtp.example.com".to_string(),
tls_builder.build().unwrap()
);
let mut mailer = SmtpClient::new(
("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
).unwrap()
.authentication_mechanism(Mechanism::Login)
.credentials(Credentials::new(
"example_username".to_string(), "example_password".to_string()
))
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.transport();
let result = mailer.send(email);
assert!(result.is_ok());
mailer.close();
}
```
#### Lower level
You can also send commands, here is a simple email transaction without
error handling:
```rust,no_run
extern crate lettre;
use lettre::EmailAddress;
use lettre::smtp::SMTP_PORT;
use lettre::smtp::client::InnerClient;
use lettre::smtp::client::net::NetworkStream;
use lettre::smtp::extension::ClientId;
use lettre::smtp::commands::*;
fn main() {
let mut email_client: InnerClient<NetworkStream> = InnerClient::new();
let _ = email_client.connect(&("localhost", SMTP_PORT), None);
let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
let _ = email_client.command(
MailCommand::new(Some(EmailAddress::new("user@example.com".to_string()).unwrap()), vec![])
);
let _ = email_client.command(
RcptCommand::new(EmailAddress::new("user@example.org".to_string()).unwrap(), vec![])
);
let _ = email_client.command(DataCommand);
let _ = email_client.message(Box::new("Test email".as_bytes()));
let _ = email_client.command(QuitCommand);
}
```

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