Compare commits

...

526 Commits

Author SHA1 Message Date
Alexis Mousset
5e474677f9 Prepare 0.9.6 (#630) 2021-05-22 20:02:58 +02:00
Alexis Mousset
8bfc20506c fix(transport-smtp): Fix transparency codec - 0.9.x (#628)
Co-authored-by: Paolo Barbolini <paolo@paolo565.org>
2021-05-22 19:58:27 +02:00
Alexis Mousset
d930c42d50 Prepare 0.9.5 release 2020-11-11 17:39:04 +01:00
Alexis Mousset
1c4a630833 Prepare 0.9.4 release 2020-11-11 17:32:30 +01:00
Alexis Mousset
1738c8e52d fix(transport-sendmail): Stop argument parsing before destination addresses 2020-11-11 17:28:02 +01:00
Alexis Mousset
2b9e476b17 Merge pull request #421 from Zoruk/v0.9.x-fix-attachment
Fix attachment line length
2020-05-06 11:07:22 +02:00
Loïc Haas
44852e42f2 Fix attachment line length 2020-05-06 10:50:36 +02:00
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
Alexis Mousset
a3d6722e7e Merge pull request #253 from Eijebong/skeptic
tests: Replace skeptic by some custom logic code and rustdoc calls
2018-04-11 22:09:52 +02:00
Bastien Orivel
fd56ec8877 test(lettre_email): Replace skeptic by some custom rustdoc invocations 2018-04-11 22:00:52 +02:00
Bastien Orivel
81bad13175 test(lettre): Replace skeptic by some custom rustdoc invocations
Unfortunately skeptic pulls all its dependencies in projects using
lettre (see https://github.com/budziq/rust-skeptic/issues/60).
2018-04-11 22:00:20 +02:00
Alexis Mousset
27c8e206cf Merge pull request #252 from amousset/add-changelog-sections
docs(all): Add changelog sections for style and docs
2018-04-08 15:14:45 +02:00
Alexis Mousset
b4d03ead8c docs(all): Add changelog sections for style and docs 2018-04-08 14:56:19 +02:00
Alexis Mousset
d692a9488f Merge pull request #251 from amousset/avoid-empty-format-strings
style(transport-smtp): Avoid useless empty format strings
2018-04-08 14:46:47 +02:00
Alexis Mousset
f3271715ec style(transport-smtp): Avoid useless empty format strings 2018-04-08 14:31:50 +02:00
Alexis Mousset
ee51cf7454 Merge pull request #250 from amousset/formal-changelog
Use clog to generate changelogs
2018-04-08 14:23:23 +02:00
Alexis Mousset
8981a7758c docs(all): Use clog to generate changelogs
Changelog will be unique, and generated from commit
messages using clog-cli.
Commit message should follow guidelines in CONTRIBUTING.md,
and GitCop has been (re)enabled.

Fixes #233.
2018-04-08 13:58:09 +02:00
Alexis Mousset
b91cb0770d Update contact email 2018-04-01 19:41:51 +02:00
Alexis Mousset
0cff889ace Merge pull request #247 from amousset/improve-readme
Add lettre.at link to README
2018-04-01 19:37:23 +02:00
Alexis Mousset
d00568cbd6 Add lettre.at link to README 2018-04-01 19:36:55 +02:00
Alexis Mousset
32cace1252 Merge pull request #246 from amousset/fix-base-url
Fix site base url
2018-04-01 19:29:32 +02:00
Alexis Mousset
a86cc3328e Fix site base url 2018-04-01 19:28:54 +02:00
Alexis Mousset
b51b2843f4 Create CNAME 2018-04-01 17:58:49 +02:00
Alexis Mousset
91a17ae281 Merge pull request #243 from amousset/prepare-0-8
Prepare 0.8 release
2018-03-31 21:11:13 +02:00
Alexis Mousset
bd752daf85 Prepare 0.8 release 2018-03-31 20:38:40 +02:00
Alexis Mousset
ed01efd890 Merge pull request #242 from amousset/more-lints
feat(all): Add more compiler lints
2018-03-31 20:00:12 +02:00
Alexis Mousset
088db45e41 feat(all): Add more compiler lints 2018-03-31 19:36:51 +02:00
Alexis Mousset
955a453df9 Merge pull request #241 from amousset/add-serde-impls
feat(transport): Add serde derive when possible
2018-03-31 17:49:14 +02:00
Alexis Mousset
71eda4b174 feat(transport): Add serde derive when possible 2018-03-31 17:30:36 +02:00
Alexis Mousset
d283254b1a Merge pull request #239 from amousset/move-envelope-lettre
feat(all): Move Envelope from lettre_email to lettre
2018-03-31 16:38:40 +02:00
Alexis Mousset
f3f963c6a5 feat(all): Move Envelope from lettre_email to lettre 2018-03-31 16:25:33 +02:00
Alexis Mousset
bef45c48f7 Merge pull request #237 from amousset/fix-style
style(all): rustfmt and clippy
2018-03-20 11:03:40 +01:00
Alexis Mousset
e024806402 style(all): rustfmt and clippy 2018-03-20 10:44:41 +01:00
Alexis Mousset
d7a8574464 Merge pull request #236 from amousset/prepare-0-8
Prepare changelog for 0.8
2018-03-11 15:39:54 +01:00
Alexis Mousset
9b22f5867e Prepare changelog for 0.8 2018-03-11 15:25:26 +01:00
Alexis Mousset
17abeb3957 Merge pull request #235 from amousset/html-utf8
fix(builder): Specify utf-8 charset for html
2018-03-11 10:49:50 +01:00
Alexis Mousset
e6a5c158da fix(builder): Specify utf-8 charset for html 2018-03-11 10:36:58 +01:00
Alexis Mousset
f4fc427a03 Merge pull request #234 from amousset/fix-attachment-multipart
fix(builder): Use parts for text and html methods to fix attachment i…
2018-03-11 01:19:24 +01:00
Alexis Mousset
662072e692 fix(builder): Use parts for text and html methods to fix attachment inclusion 2018-03-10 21:16:01 +01:00
Alexis Mousset
4f16d9ee69 Merge pull request #231 from amousset/clippy-redundant-field-name
style(all): Run stable rustfmt and remove redundant field names in st…
2018-03-03 07:52:27 +01:00
Alexis Mousset
9d68629bb6 style(all): Run stable rustfmt and remove redundant field names in structs 2018-03-03 00:28:45 +01:00
Alexis Mousset
96e4f845ec Merge pull request #230 from amousset/missing-bcc
fix(builder): Add bcc headers in builder
2018-03-01 07:38:12 +01:00
Alexis Mousset
4dc95281ad fix(builder): Add bcc headers in builder 2018-03-01 00:33:17 +01:00
Alexis Mousset
f3311456ad Merge pull request #228 from SpiderPigSpy/master
feat(email): Support binary file as attachment
2018-02-25 21:52:59 +01:00
Alex
98a250f015 feat(email): Support binary file as attachment
A pretty easy fix by using base64 encoding
worked pretty well in my project
934fb660b7/src/bot/email.rs

Closes issue
https://github.com/lettre/lettre/issues/224
2018-02-25 21:59:54 +03:00
Alexis Mousset
f2f2f98905 Merge pull request #227 from amousset/clippt-fix
feat(all): Apply clippy advice
2018-02-21 00:01:19 +01:00
Alexis Mousset
53e79d9620 feat(all): Apply clippy advice 2018-02-20 23:47:53 +01:00
Alexis Mousset
4f11ae61ef Merge pull request #225 from amousset/update-depends
feat(all): Update uuid to 0.6
2018-02-17 10:19:45 +01:00
Alexis Mousset
9344ff7e5c feat(all): Update uuid to 0.6 2018-02-17 09:53:58 +01:00
Alexis Mousset
36d20bc7b6 Merge pull request #223 from amousset/update-ci-conf
feat(all): Do not install OpenSSL for Windows tests
2018-01-27 16:20:20 +01:00
Alexis Mousset
620c3e96dc feat(all): Do not install OpenSSL for Windows tests 2018-01-27 16:08:05 +01:00
Alexis Mousset
7cab860cde Merge pull request #222 from amousset/prepare-doc-0-8
feat(all): Move doc to website and test it
2018-01-27 15:55:53 +01:00
Alexis Mousset
f10e4e81d0 feat(all): Move doc to website and test it 2018-01-27 15:44:18 +01:00
Alexis Mousset
5face8614b Merge pull request #220 from amousset/update-env-logger-0-5
feat(all): Update env_logger to 0.5
2018-01-18 09:02:20 +01:00
Alexis Mousset
480ed11785 feat(all): Update env_logger to 0.5 2018-01-18 00:14:14 +01:00
Alexis Mousset
a082da6ea4 Merge pull request #219 from amousset/fix-doc
fix(all): Fix documentation issues
2018-01-15 19:59:25 +01:00
Alexis Mousset
2a847c1b3b fix(all): Fix documentation issues 2018-01-15 19:50:47 +01:00
Alexis Mousset
f6c07f0720 Merge pull request #216 from amousset/update-log-04
feat(transport): Update log to 0.4
2017-12-28 21:01:51 +01:00
Alexis Mousset
bff687f55c feat(transport): Update log to 0.4 2017-12-28 20:17:22 +01:00
Alexis Mousset
66cd6fe3ac Merge pull request #215 from amousset/update-base64
feat(transport): Update base64 to 0.9
2017-12-23 08:24:59 +01:00
Alexis Mousset
bc4714a2c8 feat(transport): Update base64 to 0.9 2017-12-23 08:12:35 +01:00
Alexis Mousset
547be305c5 Merge pull request #214 from amousset/detail-type-enum
feat(transport-smtp): Make detail in response an enum
2017-12-10 19:12:07 +01:00
Alexis Mousset
ba719f7255 feat(transport-smtp): Make detail in response an enum 2017-12-10 18:56:35 +01:00
Alexis Mousset
4005fc88bc Merge pull request #212 from amousset/update-readme
feat(All): Update README and add CoC
2017-12-06 01:26:02 +01:00
Alexis Mousset
173f8aa2dd Merge branch 'master' into update-readme 2017-12-06 01:09:40 +01:00
Alexis Mousset
aa9e9dd96e feat(All): Update README and add CoC 2017-12-06 01:08:33 +01:00
Alexis Mousset
dd6601b9e5 Merge pull request #211 from lettre/add-code-of-conduct-1
Add a code of conduct
2017-12-06 01:03:51 +01:00
Alexis Mousset
78d8f9afb7 Add a code of conduct 2017-12-06 00:52:34 +01:00
Alexis Mousset
487bee0769 Merge pull request #210 from amousset/transport-public-methods
feat(transport-smtp): get_ehlo and reset in SmtpTransport should not …
2017-12-05 23:56:01 +01:00
Alexis Mousset
ab35bac204 feat(transport-smtp): get_ehlo and reset in SmtpTransport should not be public 2017-12-05 23:41:57 +01:00
Alexis Mousset
30ea70edab Merge pull request #209 from amousset/cleanup
Change rustfmt style
2017-11-25 12:02:09 +01:00
Alexis Mousset
eb4e7f9829 Change rustfmt style 2017-11-25 11:55:05 +01:00
Alexis Mousset
104935b443 Merge pull request #208 from amousset/use-hostname-clientid
feat(transport-smtp): Use hostname as clientid when available
2017-11-19 22:33:17 +01:00
Alexis Mousset
1936211f8e feat(transport-smtp): Use hostname as clientid when available 2017-11-19 22:21:13 +01:00
Alexis Mousset
87d0dbdf70 Merge pull request #207 from amousset/code-cleanup
feat(all): Add html_root_url
2017-11-19 14:59:38 +01:00
Alexis Mousset
7bc28caf27 feat(all): Add html_root_url 2017-11-19 14:54:04 +01:00
Alexis Mousset
7498bed378 Merge pull request #205 from amousset/update-openssl-windows-tests
fix(all): Update openssl for windows tests
2017-11-19 11:08:39 +01:00
Alexis Mousset
d2475ae1aa fix(all): Update openssl for windows tests 2017-11-19 11:00:25 +01:00
Alexis Mousset
12174676d3 Merge pull request #204 from amousset/improve-response-parsing
feat(transport-smtp): Add tests for response parsing and clean from_str
2017-11-19 01:44:26 +01:00
Alexis Mousset
1850d56ec1 feat(transport-smtp): Add tests for response parsing and clean from_str 2017-11-19 01:36:34 +01:00
Alexis Mousset
92134e22a4 Merge pull request #203 from amousset/response-parsing-with-nom
feat(transport): Use nom for parsing smtp responses
2017-11-19 01:11:12 +01:00
Alexis Mousset
01fde07a48 feat(transport): Use nom for parsing smtp responses 2017-11-19 00:42:07 +01:00
Alexis Mousset
16223ee9c3 Merge pull request #202 from amousset/update-dependencies
Update dependencies and improve style
2017-11-18 19:45:30 +01:00
Alexis Mousset
b010126c19 Update dependencies and improve style 2017-11-18 19:39:57 +01:00
Alexis Mousset
166178b011 Merge pull request #197 from jacobbudin/website-hugo-theme
fix(doc): Update Hugo theme
2017-10-16 23:02:35 +02:00
Jacob Budin
aecbce50e3 fix(doc): Update Hugo theme
Updated Hugo Learn Theme and regenerated web docs.
2017-10-15 18:17:26 -04:00
Alexis Mousset
cc324b4705 Merge pull request #196 from amousset/master
Add a version to lettre dependency in lettre_email
2017-10-08 17:59:02 +02:00
Alexis Mousset
2785f14f31 Add a version to lettre dependency in lettre_email 2017-10-08 17:41:01 +02:00
Alexis Mousset
0d3988d499 Merge pull request #195 from amousset/master
feat(all): Prepare 0.7.0 release
2017-10-08 17:28:49 +02:00
Alexis Mousset
44a1c40d41 feat(all): Prepare 0.7.0 release 2017-10-08 17:21:09 +02:00
Alexis Mousset
bef224105c Merge pull request #194 from amousset/update-base64
feat(transport): Update base64 crate to 0.7
2017-10-08 17:17:15 +02:00
Alexis Mousset
776f12c99b feat(transport): Update base64 crate to 0.7 2017-10-08 17:04:19 +02:00
Alexis Mousset
1305277ba2 Merge pull request #191 from jethrogb/transport-features
Add features for SMTP and sendmail transport
2017-09-23 18:48:29 +02:00
Alexis Mousset
85b5bbaae0 Merge pull request #192 from jethrogb/fix_ci
fix(letter): Fix beta/nightly CI
2017-09-22 12:12:44 +02:00
Jethro Beekman
8519f6881d fix(email): Don't use transport features in lettre_email 2017-09-18 22:41:08 -07:00
Jethro Beekman
60ac8ae0f3 feat(transport): Add features for SMTP and sendmail transport 2017-09-18 22:41:05 -07:00
Jethro Beekman
4e9a5575a6 fix(transport-stub): Make Stub transport independent from SMTP 2017-09-18 22:41:00 -07:00
Jethro Beekman
1f11a3ae94 fix(letter): Fix beta/nightly CI 2017-09-18 22:39:39 -07:00
Alexis Mousset
9be980ce0b Merge pull request #190 from amousset/fix-benchmark
fix(transport): Update benchmark code
2017-08-21 15:23:39 +02:00
Alexis Mousset
2838174c65 fix(transport): Update benchmark code 2017-08-21 15:00:58 +02:00
Alexis Mousset
445623db30 Merge pull request #189 from amousset/update-website
fix(doc): Update doc and specify the target version
2017-08-20 22:29:11 +02:00
Alexis Mousset
3216a3f0b9 fix(doc): Update doc and specify the target version 2017-08-20 20:28:34 +02:00
Alexis Mousset
87e25490b2 Merge pull request #188 from amousset/disable-tls-10-default
feat(transport): Disallow TLS 1.0 by default
2017-08-20 03:42:04 +02:00
Alexis Mousset
b4250036c6 feat(transport): Disallow TLS 1.0 by default 2017-08-20 03:32:24 +02:00
Alexis Mousset
4012d58dca Merge pull request #187 from amousset/fix-message-streaming
fix(transport): Fix message streaming
2017-08-20 02:40:40 +02:00
Alexis Mousset
75c184c5c3 fix(transport): Fix message streaming 2017-08-20 02:33:04 +02:00
Alexis Mousset
b087dec2d9 feat(email): Add attachments support (#186) 2017-08-20 02:31:12 +02:00
Alexis Mousset
f07fe8687d Codec from tokio smtp (#185)
feat(transport): Allow streaming emails
2017-08-20 00:42:45 +02:00
Alexis Mousset
e81351bfa8 Merge pull request #184 from amousset/missing-env-logger
fix(transport-stub): Explain that a logger is needed to get logs, and…
2017-08-06 18:09:29 +02:00
Alexis Mousset
c34f3443f5 fix(transport-stub): Explain that a logger is needed to get logs, and add env_logger to the example (fixes #181) 2017-08-06 18:02:13 +02:00
Alexis Mousset
9f4ae7b8dc Merge pull request #180 from amousset/import-client-security-tokio-smtp
feat(transport): Use structured types for transports parameters
2017-07-21 14:26:19 +02:00
Alexis Mousset
8bfee207b4 feat(transport): Use structured types for transports parameters 2017-07-21 14:19:07 +02:00
Alexis Mousset
9bf5adc052 Merge pull request #178 from amousset/mailbox-public
feat(email): Export email_format types (fixes #148)
2017-07-19 09:09:03 +02:00
Alexis Mousset
a18e219000 feat(email): Export email_format types (fixes #148) 2017-07-19 08:48:27 +02:00
Alexis Mousset
b7e4bfb375 Merge pull request #177 from amousset/fix-formatting
style(all): Move to rustfmt-nightly
2017-07-18 16:21:59 +02:00
Alexis Mousset
66836b0522 style(all): Move to rustfmt-nightly 2017-07-18 15:50:24 +02:00
Alexis Mousset
59d47dfdf5 Merge pull request #176 from amousset/method-builder
Add methods to create builder and reexport Transport types
2017-07-18 10:37:44 +02:00
Alexis Mousset
75e6c0d115 Add methods to create builder and reexport Transport types 2017-07-18 00:35:16 +02:00
Alexis Mousset
a093e38f7e Merge pull request #175 from amousset/rename-ssl-tls
style(all): Rename ssl to tls
2017-07-17 17:23:44 +02:00
Alexis Mousset
7d535f29a8 style(all): Rename ssl to tls 2017-07-17 17:13:27 +02:00
Alexis Mousset
2bfd67273b Merge pull request #174 from amousset/crammd5-option
feat(transport): Make use of hex and rust-crypto for crammd5 an option
2017-07-17 16:35:11 +02:00
Alexis Mousset
e656e9e325 feat(transport): Make use of hex and rust-crypto for crammd5 an optionnal feature 2017-07-17 16:28:43 +02:00
Alexis Mousset
e90fe50943 Merge pull request #173 from amousset/test-no-features
feat(transport): Run tests when no features are enabled
2017-07-17 15:06:40 +02:00
Alexis Mousset
2aa3cd0670 feat(transport): Run tests when no features are enabled 2017-07-17 15:00:56 +02:00
Alexis Mousset
a2143caf02 Merge pull request #172 from amousset/fix-benches
feat(transport): Fix benches
2017-07-17 14:49:11 +02:00
Alexis Mousset
04c83fc20d feat(transport): Fix benches 2017-07-17 14:30:42 +02:00
Alexis Mousset
c7c42cb207 Merge pull request #171 from amousset/serde-option
feat(transport): Make serde optionnal
2017-07-17 13:51:40 +02:00
Alexis Mousset
8a90f8f7b6 feat(transport): Make serde optionnal 2017-07-17 13:44:39 +02:00
Alexis Mousset
0e01820ea4 Merge pull request #170 from amousset/try-macro
style(all): Replace try! by ?
2017-07-17 12:24:58 +02:00
Alexis Mousset
e2d0e31453 style(all): Replace try! by ? 2017-07-17 12:19:56 +02:00
Alexis Mousset
669c558120 Merge pull request #169 from amousset/email-type
feat(transport): Use command types for mail and rcpt
2017-07-17 12:04:40 +02:00
Alexis Mousset
12794d36b3 feat(transport): Use command types for mail and rcpt 2017-07-17 11:58:58 +02:00
Alexis Mousset
0d7cba9657 Merge pull request #168 from amousset/allow-chosing-stub-response
feat(transport): Allow specifying a response for stub transport
2017-07-16 22:22:40 +02:00
Alexis Mousset
215c9d5136 feat(transport): Allow specifying a response for stub transport 2017-07-16 22:16:01 +02:00
Alexis Mousset
77210d1b5e Merge pull request #167 from amousset/use-native-tls
feat(transport): Use tls native
2017-07-16 21:45:36 +02:00
Alexis Mousset
441c4e8228 feat(transport): Use tls native 2017-07-16 21:38:18 +02:00
Alexis Mousset
858cecfdde Merge pull request #166 from amousset/run-clippy
style(all): Style improvement with clippy
2017-07-16 18:46:45 +02:00
Alexis Mousset
1047e84962 style(all): Style improvement with clippy 2017-07-16 18:41:33 +02:00
Alexis Mousset
7fc0ed5c56 Merge pull request #165 from amousset/add-smtp-command-types
feat(transport): Use types for SMTP commands
2017-07-16 18:31:11 +02:00
Alexis Mousset
6840555473 feat(transport): Use types for SMTP commands 2017-07-16 18:21:52 +02:00
Alexis Mousset
b31fd465ad Merge pull request #162 from amousset/extension-copy
feat(transport): Implement Copy for Extension
2017-06-18 00:39:13 +02:00
Alexis Mousset
b1d2a89b2e feat(transport): Implement Copy for Extension 2017-06-18 00:34:30 +02:00
Alexis Mousset
5b2a1be24c Merge pull request #161 from amousset/interface-response
feat(transport): Avoid useless methods in response and code
2017-06-18 00:19:45 +02:00
Alexis Mousset
7e69f90a6f feat(transport): Avoid useless methods in response and code 2017-06-18 00:15:06 +02:00
Alexis Mousset
f95bbff997 Merge pull request #160 from amousset/import-tokio-smtp-structs
feat(transport): Use ClientId and Detail from tokio-smtp
2017-06-17 23:22:34 +02:00
Alexis Mousset
339f65f618 feat(transport): Use ClientId and Detail from tokio-smtp 2017-06-17 23:17:19 +02:00
Alexis Mousset
1ae8c6370c Merge pull request #159 from amousset/fix-error-display-other-transports
feat(transport): Improve description of all transport error types
2017-06-17 18:21:58 +02:00
Alexis Mousset
eb3b88288a Merge branch 'master' into fix-error-display-other-transports 2017-06-17 18:10:39 +02:00
Alexis Mousset
9b8449a908 Merge pull request #158 from amousset/add-utf8-example
feat(transport): Use utf-8 chars in example email
2017-06-17 18:10:24 +02:00
Alexis Mousset
ff87e4c595 feat(transport): Improve description of all transport error types 2017-06-17 18:10:06 +02:00
Alexis Mousset
918c679d94 feat(transport): Use utf-8 chars in example email 2017-06-17 17:55:24 +02:00
Alexis Mousset
04e9a824b3 Merge pull request #156 from amousset/file-serde
feat(transport): Use serde to serialize email in the file transport
2017-06-17 17:52:24 +02:00
Alexis Mousset
63f35f78a6 feat(transport): Use serde to serialize email in the file transport 2017-06-17 17:45:49 +02:00
Alexis Mousset
1378b8959d Merge pull request #155 from amousset/improve-variables
style(all): Fix LOGIN auth detection and improve response tests
2017-06-17 16:10:46 +02:00
Alexis Mousset
c5034324d2 style(all): Fix LOGIN auth detection and improve response tests 2017-06-17 16:05:59 +02:00
Alexis Mousset
7eb0828bca Merge pull request #154 from amousset/run-clippy
style(all): Style improvement with clippy
2017-06-17 13:03:05 +02:00
Alexis Mousset
80fb92161d style(all): Style improvement with clippy 2017-06-17 12:57:54 +02:00
Alexis Mousset
fc741b7390 Merge pull request #153 from amousset/fix-error-display
feat(transport): More precise error descriptions
2017-06-17 12:38:05 +02:00
Alexis Mousset
153af016e7 feat(transport): More precise error descriptions 2017-06-17 12:33:46 +02:00
Alexis Mousset
d3a4e353b1 Merge pull request #151 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 22:28:19 +02:00
Alexis Mousset
4a49db92c1 Merge branch 'master' into master 2017-06-14 22:24:04 +02:00
Alexis Mousset
eea9354e2f feat(all): Fix AppVeyor tests 2017-06-14 22:20:00 +02:00
Alexis Mousset
e6d62a5e64 Merge pull request #150 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 22:05:27 +02:00
Alexis Mousset
782962ce5a feat(all): Fix AppVeyor tests 2017-06-14 21:50:41 +02:00
Alexis Mousset
53f1d07f00 Merge pull request #149 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 21:45:38 +02:00
Alexis Mousset
a1de7a2b24 Merge branch 'master' into master 2017-06-14 21:37:42 +02:00
Alexis Mousset
bae92bcf08 feat(all): Fix AppVeyor tests 2017-06-14 21:36:51 +02:00
Alexis Mousset
df91f38323 Merge pull request #147 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 21:08:42 +02:00
Alexis Mousset
622c4a8ff0 Merge branch 'master' into master 2017-06-14 21:01:36 +02:00
Alexis Mousset
b27013765a feat(all): Fix AppVeyor tests 2017-06-14 21:01:03 +02:00
Alexis Mousset
53072e2c89 Merge pull request #146 from amousset/master
feat(all): Fix AppVeyor tests
2017-06-14 20:55:35 +02:00
Alexis Mousset
c3a2409957 feat(all): Fix AppVeyor tests 2017-06-14 20:51:01 +02:00
Alexis Mousset
093e16cad0 Merge pull request #145 from amousset/fix-appveyor
feat(all): Fix AppVeyor tests
2017-06-14 20:47:01 +02:00
Alexis Mousset
28fb7961df feat(all): Fix AppVeyor tests 2017-06-14 20:42:36 +02:00
Alexis Mousset
9c3991af6d Merge pull request #144 from amousset/upgrade-mime-crate
feat(email): Upgrade mime crate to 0.3
2017-06-14 20:35:04 +02:00
Alexis Mousset
912e0579a6 feat(email): Upgrade mime crate to 0.3 2017-06-14 20:28:05 +02:00
Alexis Mousset
0c1b440f8b Merge pull request #142 from amousset/formatting
style(all): Run last rustfmt
2017-06-14 00:53:45 +02:00
Alexis Mousset
3b46c56bbd style(all): Run last rustfmt 2017-06-14 00:48:32 +02:00
Alexis Mousset
bcf2110804 Merge pull request #141 from amousset/improve-network
feat(transport): Add a mock network stream
2017-06-14 00:32:14 +02:00
Alexis Mousset
7270d0807f feat(transport): Add a mock network stream 2017-06-14 00:12:07 +02:00
Alexis Mousset
d26a771207 Merge pull request #139 from amousset/master
Fix logos
2017-05-26 00:55:14 +02:00
Alexis Mousset
ebf75c2f00 Merge branch 'master' into master 2017-05-26 00:44:48 +02:00
Alexis Mousset
30175d941d Fix logos 2017-05-26 00:44:16 +02:00
Alexis Mousset
b4161afe14 Merge pull request #138 from amousset/master
Fix logos
2017-05-26 00:43:41 +02:00
Alexis Mousset
6f09f1c52c Merge branch 'master' into master 2017-05-26 00:36:05 +02:00
Alexis Mousset
aabc07b0a9 Fix logos 2017-05-26 00:34:40 +02:00
Alexis Mousset
dca316deae Merge pull request #137 from amousset/master
Add logo
2017-05-26 00:25:11 +02:00
Alexis Mousset
d429763b85 Merge branch 'master' into master 2017-05-26 00:16:48 +02:00
Alexis Mousset
bae531e264 Add logo 2017-05-26 00:15:56 +02:00
Alexis Mousset
c63eba4d1c Merge pull request #136 from amousset/master
Add search to website
2017-05-25 23:44:53 +02:00
Alexis Mousset
0db90c0997 Merge branch 'master' into master 2017-05-25 23:40:52 +02:00
Alexis Mousset
d0b038ac72 Add search to website 2017-05-25 23:35:32 +02:00
Alexis Mousset
691e7ff4f8 Merge pull request #135 from amousset/master
Add redirection for index page
2017-05-25 23:21:29 +02:00
Alexis Mousset
d860051d1a Merge branch 'master' into master 2017-05-25 23:12:55 +02:00
Alexis Mousset
a166b2e6a4 Add redirection for index page 2017-05-25 23:12:03 +02:00
Alexis Mousset
85a81ca383 Merge pull request #134 from amousset/master
Add redirection for index page
2017-05-25 23:08:36 +02:00
Alexis Mousset
e521f9bb0f Merge branch 'master' into master 2017-05-25 22:57:04 +02:00
Alexis Mousset
d2400de365 Add redirection for index page 2017-05-25 22:56:12 +02:00
Alexis Mousset
5a76aaf839 Merge pull request #133 from amousset/master
Add website
2017-05-25 22:41:48 +02:00
Alexis Mousset
1dddbde053 Merge branch 'master' into master 2017-05-25 22:28:03 +02:00
Alexis Mousset
3368872f9f Add website 2017-05-25 22:27:44 +02:00
Alexis Mousset
cb15e32454 Merge pull request #132 from amousset/master
Add website
2017-05-25 22:24:36 +02:00
Alexis Mousset
e85c3a4d70 add website 2017-05-25 22:18:31 +02:00
Alexis Mousset
ea99f66a5e Merge pull request #131 from callahad/error-typo
Fix typo in error string
2017-05-25 09:49:35 +02:00
Dan Callahan
6757fadee3 Fix typo in error string 2017-05-24 21:52:49 -05:00
Alexis Mousset
843f6b9a39 Merge pull request #130 from amousset/style
style(all): Run clippy without errors
2017-05-21 13:26:26 +02:00
Alexis Mousset
aa7a6dfcac Merge branch 'master' into style 2017-05-21 13:10:01 +02:00
Alexis Mousset
73c5630634 style(all): Run clippy without errors 2017-05-21 13:09:31 +02:00
Alexis Mousset
9fe4d4ad84 Merge pull request #129 from amousset/master
style(all): Split changelogs
2017-05-21 09:45:10 +02:00
Alexis Mousset
3b4434467a style(all): Split changelogs 2017-05-21 02:27:38 +02:00
Alexis Mousset
e50f287598 Merge pull request #128 from amousset/split-crates-transport
Split crates
2017-05-21 01:26:52 +02:00
Alexis Mousset
ae640da631 refactor(all): split email and transport into different crates 2017-05-21 00:59:39 +02:00
Alexis Mousset
87aa3ca701 Merge pull request #127 from mpietrzak/master
feat(all): Allow uuid 0.4 and 0.5 in dependencies.
2017-05-19 23:14:52 +02:00
Alexis Mousset
12ecad34ba Merge branch 'master' into master 2017-05-19 23:04:44 +02:00
Alexis Mousset
11a983f078 Merge pull request #125 from chills42/no-more-rustc-serialize
refactor(transport-smtp): migrate away from rustc-serialize
2017-05-19 23:03:14 +02:00
Alexis Mousset
332e05278c Merge branch 'master' into no-more-rustc-serialize 2017-05-19 22:54:09 +02:00
Alexis Mousset
9942acf8ff Merge pull request #126 from amousset/master
style(all): Fix tests on travis
2017-05-19 22:53:57 +02:00
Alexis Mousset
66c214c2b7 style(all): Fix tests on some platforms 2017-05-19 22:47:22 +02:00
Craig Hills
dfa01dbb7a remove rustc-serialize dependency 2017-05-19 11:49:32 -04:00
Maciej Pietrzak
9e2f0af9a6 feat(all): Allow uuid 0.4 and 0.5 in dependencies. 2017-05-03 12:57:18 +02:00
Craig Hills
3178db04e2 refactor(transport-smtp): migrate away from rustc-serialize
This drops the deprecated rustc-serialize dependency in favor of the
base64 and hex crates.
2017-04-26 21:43:26 -04:00
Alexis Mousset
4f41eef936 Merge pull request #123 from amousset/disallow-unencrypted-by-default
feat(transport-smtp): Disallow unencrypted connection by default
2017-03-26 22:54:29 +02:00
Alexis Mousset
8069b9e9ae feat(transport-smtp): Disallow unencrypted connection by default
By default, do not silently use unencrypted transport.
2017-03-26 22:34:49 +02:00
Alexis Mousset
4d879dabba Merge pull request #122 from amousset/add-login-auth
feat(transport-smtp): Add support for LOGIN auth mechanism
2017-03-26 22:00:42 +02:00
Alexis Mousset
20f6c5db3f feat(transport-smtp): Add support for LOGIN auth mechanism 2017-03-26 21:54:28 +02:00
Alexis Mousset
9953820174 Merge pull request #120 from amousset/changelog-0.6.2
feat(chore): Bump to v0.6.2
2017-02-18 18:53:09 +01:00
Alexis Mousset
1c8b78066f feat(chore): Bump to v0.6.2 2017-02-18 18:45:58 +01:00
Alexis Mousset
4d3e51d115 Merge pull request #118 from amousset/update-uuid
feat(all) Update uuid crate to 0.4
2017-02-18 18:07:20 +01:00
Alexis Mousset
4fc73cdde0 feat(all): Update uuid crate to 0.4 2017-02-18 17:58:40 +01:00
Alexis Mousset
caeb6b807c Merge pull request #116 from amousset/master
feat(email): Use Enveloppe directly
2017-02-18 17:54:30 +01:00
Alexis Mousset
315e248d63 feat(all): Update env_logger 2017-02-12 23:40:21 +01:00
Alexis Mousset
e068c2d41f feat(email): Use Enveloppe directly 2017-02-12 23:39:27 +01:00
Alexis Mousset
0488e3f943 Merge pull request #112 from amousset/zsck-master
feat(transport): Upgrade to OpenSSL ^0.9
2017-01-01 13:08:26 +01:00
Zack Mullaly
2ed25fdbb4 feat(transport): Upgrade to OpenSSL ^0.9 2017-01-01 13:02:10 +01:00
Alexis Mousset
90b00ae4ff Merge pull request #107 from amousset/stream-timeout
feat(transport-smtp): Add timeout suppor to SMTP transport
2016-11-06 22:20:49 +01:00
Alexis Mousset
bd8b1265c4 feat(transport-smtp): Add timeout suppor to SMTP transport
Fixes #106
2016-11-06 22:10:46 +01:00
Alexis Mousset
f8f024ae7c Merge pull request #105 from amousset/style-improvement
style(all): Improve coding style (using rust-clippy)
2016-10-23 21:14:38 +02:00
Alexis Mousset
0f71490c61 style(all): Improve coding style (using rust-clippy) 2016-10-23 21:08:59 +02:00
Alexis Mousset
6fe2ef679b Merge pull request #104 from amousset/sendmail
test(transport): add sendmail transport
2016-10-23 20:57:43 +02:00
Alexis Mousset
73e7aa3639 test(transport): add sendmail transport 2016-10-23 20:50:30 +02:00
Alexis Mousset
cc6ca7633d Merge branch 'sendmail' of https://github.com/paradoxix/lettre 2016-10-23 18:37:19 +02:00
Alexis Mousset
64d2f2e81c Merge pull request #103 from amousset/improve-api
style(all): Incompatible improvements in API
2016-10-23 18:19:11 +02:00
Alexis Mousset
e572892a48 Merge branch 'master' into improve-api 2016-10-23 17:19:20 +02:00
Alexis Mousset
3b4f4a739e style(all): Incompatible improvements in API 2016-10-23 17:16:54 +02:00
Alexis Mousset
8400e47cfc Merge pull request #101 from amousset/add-examples
docs(all): Add an example for simple builder usage and smtp transport
2016-10-23 12:58:29 +02:00
Alexis Mousset
b7cb4e88c4 Merge branch 'master' into add-examples 2016-10-23 12:41:30 +02:00
Alexis Mousset
73c957e350 docs(all): Add an example for simple builder usage and smtp transport 2016-10-23 12:37:30 +02:00
Alexis Mousset
783918a403 Merge pull request #100 from amousset/use-docsrs-links-for-doc
docs(all): Change doc links to use docs.rs
2016-10-22 23:44:40 +02:00
Alexis Mousset
d0bf2327e3 docs(all): Change doc links to use docs.rs 2016-10-22 23:36:03 +02:00
Alexis Mousset
e63730d960 Merge pull request #99 from stephank/master
Implement basic traits for SecurityLevel.
2016-10-22 19:42:44 +02:00
Stéphan Kochen
d50bb404b9 feat(transport-smtp): Add derives to SecurityLevel 2016-10-22 19:32:22 +02:00
Alexis Mousset
747d8cabc5 Merge pull request #94 from ConnyOnny/master
feat(email): support for a custom envelope
2016-10-22 19:28:43 +02:00
Constantin Berhard
b415edcfe0 refactor(email): requested changes for PR #94
renamed variables meaningfully
return more errors in email building
modified some comments

PR #94
2016-10-22 18:11:35 +02:00
Constantin
0b01211a34 Merge branch 'master' into master 2016-10-22 12:02:22 +02:00
Lars Reichardt
13ee61d5cf test(transport): add sendmail transport test 2016-10-21 15:54:53 +02:00
Lars Reichardt
a302df61d4 feat(transport): add sendmail transport 2016-10-21 14:22:47 +02:00
Alexis Mousset
5be0f86c83 Merge pull request #97 from amousset/bump-to-0.6.1
chore(all): Bump to v0.6.1
2016-10-19 23:24:58 +02:00
Alexis Mousset
53f9bada4c chore(all): Bump to v0.6.1 2016-10-19 23:19:32 +02:00
Alexis Mousset
ce7d55ffa8 Merge pull request #96 from amousset/master
docs(all): Add complete documentation information to README
2016-10-19 22:32:07 +02:00
Alexis Mousset
a6ea43a842 Merge branch 'master' into master 2016-10-19 22:24:52 +02:00
Alexis Mousset
eac29768ae docs(all): Add complete documentation information to README 2016-10-19 22:23:43 +02:00
Constantin Berhard
7788498762 feat(email): support for a custom envelope
The EmailBuilder now has a function to add a preconfigured envelope,
overriding the auto generated one.

fixes #84
2016-10-19 13:00:41 +02:00
Alexis Mousset
d944aed9d3 Merge pull request #93 from amousset/master
docs(all): Force building tests before coverage computing
2016-10-19 02:33:33 +02:00
Alexis Mousset
67318ac759 docs(all): Force building tests before coverage computing 2016-10-19 02:26:25 +02:00
Alexis Mousset
90999bfc24 Merge pull request #92 from amousset/improve-doc-build
docs(all): Fix token name
2016-10-19 02:05:36 +02:00
Alexis Mousset
7635830399 Merge branch 'master' into improve-doc-build 2016-10-19 01:58:05 +02:00
Alexis Mousset
4fbd06e18b docs(all): Fix token name 2016-10-19 01:56:53 +02:00
Alexis Mousset
5247e2c2aa Merge pull request #91 from amousset/improve-doc-build
docs(all): Build seperate docs for each release
2016-10-19 01:43:20 +02:00
Alexis Mousset
8e21de8de3 docs(all): Build seperate docs for each release 2016-10-19 01:37:51 +02:00
Alexis Mousset
d976f48b7b Merge pull request #86 from ConnyOnny/master
fix(email): address-list for "To", "From" etc.
2016-10-18 00:23:47 +02:00
Constantin Berhard
9ed51a2d3d fix(email): address-list for "To", "From" etc.
Recipients etc. are accumulated by the EmailBuilder and put into the
email as one header with an address-list instead of multiple headers.
This is required by RFC 5322. Also a new test for this behaviour was
added.

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

Closes #65
2016-05-10 00:12:39 +02:00
Alexis Mousset
8fda23435c Merge pull request #64 from craftytrickster/improvements
Improvements
2016-05-07 08:02:34 +02:00
David Raifaizen
4f4a1436ae refactor(client): Replacing manual unwrap with match statement 2016-05-06 22:56:49 -04:00
David Raifaizen
a8324795e5 style(all): Minor typo/grammar fixes 2016-05-06 22:56:33 -04:00
Alexis Mousset
f347dcc97b Merge pull request #63 from amousset/add-changelog
fix(email): Add required MIME-Version header
2016-05-06 08:59:03 +02:00
Alexis Mousset
df69c8b7c2 Merge branch 'master' into add-changelog 2016-05-06 08:51:06 +02:00
Alexis Mousset
fd20e90bb5 fix(email): Add required MIME-Version header
Add a MIME-Version header to all messages.
2016-05-06 00:33:31 +02:00
Alexis Mousset
1da35ec17f Merge pull request #61 from amousset/add-changelog
docs(all): Add a changelog file
2016-05-05 23:54:46 +02:00
Alexis Mousset
69f29cfb45 Merge branch 'master' into add-changelog 2016-05-05 23:41:12 +02:00
Alexis Mousset
e047aedee1 docs(all): Add a changelog file
Add a CHANGELOG file starting from 0.6.
2016-05-05 23:27:34 +02:00
Alexis Mousset
d14236be4d Merge pull request #60 from amousset/fix-contributing
docs(all): Fix contributing doc formatting
2016-05-05 22:28:45 +02:00
Alexis Mousset
c814300f96 docs(all): Fix contributing doc formatting
Fix the formatting of the titles.
2016-05-05 22:21:45 +02:00
Alexis Mousset
3171dec6e9 Merge pull request #59 from amousset/contributing-doc
docs(all): Add a contributing documentation
2016-05-05 21:40:35 +02:00
Alexis Mousset
e9b658d868 docs(all): Add a contributing documentation
Adds a CONTRIBUTING.md describing the commit format.
Commit message formatting is now enforced by Gitcop.
2016-05-05 21:30:09 +02:00
Alexis Mousset
72b8bc3866 Merge pull request #57 from amousset/fix-nightly
Add documentation for multipart messages
2016-05-05 19:22:41 +02:00
Alexis Mousset
f3742adc9f Add documentation for multipart messages 2016-05-05 19:22:07 +02:00
Alexis Mousset
c5fec283f7 Merge pull request #56 from amousset/fix-nightly
Improve multipart support
2016-05-05 16:34:35 +02:00
Alexis Mousset
2d9ad22102 Improve multipart support 2016-05-05 16:32:42 +02:00
Alexis Mousset
d0eab7a09f Merge pull request #55 from amousset/fix-nightly
Do not use default type parameter in impl
2016-05-04 00:03:01 +02:00
Alexis Mousset
4f6e6185fc Do not use default type parameter in impl 2016-05-04 00:01:13 +02:00
Alexis Mousset
256624f3d8 Merge pull request #54 from amousset/multipart
Add initial multipart support
2016-05-03 23:55:17 +02:00
Alexis Mousset
74de004e6c Add initial multipart support 2016-05-03 23:51:30 +02:00
Alexis Mousset
b7d81016e1 Merge pull request #53 from amousset/update-rependencies
Update uuid crate from 0.1 to 0.2
2016-05-01 19:10:52 +02:00
Alexis Mousset
6bd2b364ec Update uuid crate from 0.1 to 0.2 2016-05-01 19:09:59 +02:00
Alexis Mousset
bc764aec5a Merge pull request #52 from amousset/improve-doc-4
Doc formatting
2016-03-27 22:47:15 +02:00
Alexis Mousset
d18eec4d1b Doc formatting 2016-03-27 21:40:20 +02:00
Alexis Mousset
ec68ca2ca8 Merge pull request #51 from amousset/improve-doc-4
Improve doc
2016-03-27 21:38:35 +02:00
Alexis Mousset
c9076fef63 Improve doc 2016-03-27 21:37:33 +02:00
Alexis Mousset
c770dc6205 Merge pull request #50 from amousset/improve-doc-4
Improve doc
2016-03-27 21:07:08 +02:00
Alexis Mousset
da8c733939 Improve doc 2016-03-27 21:06:18 +02:00
Alexis Mousset
c21bdaff43 Merge pull request #47 from amousset/small-fixes
Little refactoring
2016-03-25 22:23:57 +01:00
Alexis Mousset
dab8b111d3 Little refactoring 2016-03-25 21:54:15 +01:00
Alexis Mousset
521681c0f7 Merge pull request #46 from amousset/appveyor
Add an AppVeyor status
2016-03-20 02:10:59 +01:00
Alexis Mousset
2e3f82b98a Add an AppVeyor status 2016-03-20 02:10:25 +01:00
Alexis Mousset
2816e8ee73 Merge pull request #45 from amousset/appveyor
Update appveyor file
2016-03-20 01:55:15 +01:00
Alexis Mousset
4441be6b7b Update appveyor file 2016-03-20 01:54:16 +01:00
Alexis Mousset
b992ca9694 Merge pull request #44 from amousset/appveyor
Update appveyor file
2016-03-20 01:49:42 +01:00
Alexis Mousset
d7e100692b Update appveyor file 2016-03-20 01:48:11 +01:00
Alexis Mousset
2c979a6fbd Merge pull request #43 from amousset/update
rustfmt pass
2016-03-20 01:35:31 +01:00
Alexis Mousset
f8c883f58e rustfmt pass 2016-03-20 01:27:58 +01:00
Alexis Mousset
af20cfa8ff Merge pull request #41 from amousset/master
Improve doc
2016-01-16 12:22:40 +01:00
Alexis Mousset
7a9f9111a5 Improve doc 2016-01-16 12:13:06 +01:00
Alexis Mousset
89c0be219d Merge pull request #39 from lorenz/patch-3
Allow multiple To addresses in SimpleSendableMail
2015-12-18 22:57:31 +01:00
Lorenz Brun
6ee7fdb3d1 Allow multiple To addresses in SimpleSendableMail
Because the struct internally stores a `Vec<String>` it would be nice to be able to construct messages with multiple To addresses.
This is in its current form a breaking API change, so feel free to change the way it's implemented.
2015-12-05 00:10:24 +01:00
Alexis Mousset
b7ac3a897f Merge pull request #38 from cloudvlts/master
Update 'openssl' to 0.7
2015-11-30 10:47:23 +01:00
Darius Clark
c436716277 Update 'openssl' to 0.7 2015-11-29 20:51:49 -05:00
Alexis Mousset
eabdb960b0 Merge pull request #37 from amousset/master
Fix doc build
2015-11-07 08:47:13 +01:00
Alexis Mousset
59ba9e84dc Fix doc build 2015-11-07 08:46:51 +01:00
Alexis Mousset
e569c030bc Merge pull request #36 from amousset/master
Fix doc build
2015-11-07 08:40:35 +01:00
Alexis Mousset
72aea756fa Fix doc build 2015-11-07 08:40:02 +01:00
Alexis Mousset
655ae6d2ff Update .travis.yml 2015-11-03 22:20:33 +01:00
Alexis Mousset
150536d242 Update .travis.yml 2015-11-03 22:00:17 +01:00
Alexis Mousset
7d707fab25 Update .travis.yml 2015-11-03 21:49:03 +01:00
Alexis Mousset
4ec34987f8 Merge pull request #35 from amousset/improve-doc
Improve documentation
2015-11-03 21:48:02 +01:00
Alexis Mousset
7f3680f125 Improve documentation 2015-11-03 21:47:06 +01:00
Alexis Mousset
67566c2152 Merge pull request #34 from amousset/improve-doc
Improve documentation
2015-11-03 21:22:26 +01:00
Alexis Mousset
d863a7677e Improve documentation 2015-11-03 21:18:18 +01:00
Alexis Mousset
c1fe40479b Improve documentation 2015-11-03 21:08:56 +01:00
Alexis Mousset
8d03545062 Merge pull request #32 from amousset/improve-doc
Improve documentation
2015-10-31 21:41:47 +01:00
Alexis Mousset
489a6e892e Improve documentation 2015-10-31 21:23:06 +01:00
Alexis Mousset
b3fe1e0f65 Merge pull request #31 from amousset/fix-benchmarks
Fix benchmarks
2015-10-30 19:15:32 +01:00
Alexis Mousset
3612ffca7a Fix becnhmarks 2015-10-30 19:08:11 +01:00
Alexis Mousset
7940ad6c15 Merge pull request #30 from amousset/comply-with-rfc1214
Comply with RFC1214
2015-10-30 17:29:01 +01:00
Alexis Mousset
5ffb169bc9 Comply with RFC1214 2015-10-29 23:26:20 +01:00
Alexis Mousset
ea0bb256cd Merge pull request #29 from amousset/add-smtps-support
feature(smtp): Add SMTPS support
2015-10-27 22:47:16 +01:00
Alexis Mousset
9f177047f8 Add SMTPS support 2015-10-27 22:38:45 +01:00
Alexis Mousset
48eb859804 Merge pull request #26 from norcalli/master
Renames and fixes typo from mecanism to mechanism
2015-10-27 14:34:47 +01:00
Ashkan Kiani
8d9877233d Renames and fixes typo from mecanism to mechanism 2015-10-27 06:20:04 -07:00
Alexis Mousset
09f61a9fc9 Merge pull request #24 from amousset/rustfmt
style(formatting): Run rustfmt
2015-10-26 22:58:09 +01:00
Alexis Mousset
40e749a04a style(formatting): Run rustfmt 2015-10-26 22:51:07 +01:00
Alexis Mousset
4efb560bc8 Merge pull request #23 from lettre/update-readme
docs(readme): Update readme for 0.5.0
2015-10-26 21:14:24 +01:00
Alexis Mousset
500c4fb39d Update readme for 0.5.0 2015-10-26 21:05:18 +01:00
Alexis Mousset
d488910010 Merge pull request #22 from lorenz/patch-2
Release version 0.5.0
2015-10-26 21:01:33 +01:00
Lorenz Brun
4155e44dbd Release new version
I'd like to use the new features you introduced in the last few days, especially `transport.send(mail)`. Also the docs are currently built for the `master` branch which means they currently show features not yet available in the Crate.
2015-10-26 20:30:57 +01:00
Alexis Mousset
401118ee68 Improved test for File transport 2015-10-26 00:17:46 +01:00
Alexis Mousset
e6dd9d5a46 Install complete openssl on windows 2015-10-25 23:40:59 +01:00
Alexis Mousset
c8187c4a7c Install packages for kcov 2015-10-25 17:59:56 +01:00
Alexis Mousset
8f211c88a8 Add OpenSSL path to windows build 2015-10-25 17:50:20 +01:00
Alexis Mousset
62df24c5b1 Add file transport 2015-10-25 17:47:07 +01:00
Alexis Mousset
7ac43b73c3 Remove Mailer struct 2015-10-25 15:29:40 +01:00
Alexis Mousset
3c91c065d6 Split integration tests 2015-10-25 12:52:56 +01:00
Alexis Mousset
9e30e7185e Use travis-cargo for benchmarks 2015-10-22 23:52:30 +02:00
Alexis Mousset
4da9e16bfc Add cache for release (used in benchmarks) 2015-10-22 23:51:01 +02:00
Alexis Mousset
2977eb0509 Travis build without sudo 2015-10-22 23:45:20 +02:00
Alexis Mousset
2884da8f90 Use smtp-sink for benchmarks 2015-10-22 23:37:04 +02:00
Alexis Mousset
31a7504d54 Add becnhmarks 2015-10-22 23:34:04 +02:00
Alexis Mousset
9a93feea96 Fix SMTPUTF8 test 2015-10-22 21:51:29 +02:00
Alexis Mousset
f102f321d3 Tests email with travis 2015-10-22 21:45:19 +02:00
Alexis Mousset
1ba47e473c Tests email with travis 2015-10-22 21:41:09 +02:00
Alexis Mousset
3acf21a316 Tests email with travis 2015-10-22 21:32:48 +02:00
Alexis Mousset
544894def9 Add integration testing on travis 2015-10-22 19:30:36 +02:00
Alexis Mousset
f74fb4f89c Add openssl to AppVeyor env 2015-10-22 18:32:00 +02:00
Alexis Mousset
085998c730 Add mingw to AppVeyor 2015-10-22 18:07:20 +02:00
Alexis Mousset
d3d7c4b44e Formatting with rustfmt 2015-10-22 16:53:13 +02:00
Alexis Mousset
88b9cfb847 Fix documentation link 2015-10-21 23:16:09 +02:00
Alexis Mousset
250ed7bcf4 Add SMTPUTF8 support 2015-10-21 23:02:14 +02:00
Alexis Mousset
63d9216df3 Change crate name 2015-10-15 00:03:07 +02:00
Alexis Mousset
54758ebde9 Rename rust-smtp to lettre, add multiple transports support 2015-10-14 23:44:25 +02:00
Alexis Mousset
bd67d80d3e Try to authenticate only once 2015-10-12 23:46:12 +02:00
Alexis Mousset
a91db14869 Configurable TLS security level 2015-10-12 22:47:53 +02:00
Alexis Mousset
b5c6663629 Update AppVeyor configuration 2015-10-12 02:50:53 +02:00
Alexis Mousset
4b4150ed99 Version 0.3.0 2015-10-12 02:20:39 +02:00
Alexis Mousset
5f911dce12 Formatting with rustfmt 2015-10-12 02:19:33 +02:00
Alexis Mousset
47d6870d93 Add STARTTLS support instead of SMTPS 2015-10-12 00:59:39 +02:00
Alexis Mousset
51de392086 Add documentation 2015-10-11 21:21:31 +02:00
Alexis Mousset
fefb5f7978 smtps with rust-openssl 2015-10-11 19:56:02 +02:00
Alexis Mousset
5d125bdbdb Format code with rustfmt 2015-10-08 20:12:07 +02:00
Alexis Mousset
a1bf0170db Version 0.2.0 2015-10-06 18:42:35 +02:00
Alexis Mousset
5bedba4b24 Use Tm::rfc822z to support local timezones (workaround for time crate incomplete feature) 2015-10-06 18:42:23 +02:00
Alexis Mousset
813f09a314 v0.1.2 2015-08-02 22:05:55 +02:00
Alexis Mousset
6a6023431b Add test cases for authentication 2015-08-02 21:24:24 +02:00
Alexis Mousset
7f6aa0ffae Document authentication mecanism configuration 2015-08-02 19:23:29 +02:00
Alexis Mousset
1830f084c0 Let the user configure the authentication mecanisms 2015-08-02 19:12:59 +02:00
Alexis Mousset
49a995f68d Merge branch 'master' of github.com:amousset/rust-smtp 2015-07-23 01:01:43 +02:00
Alexis Mousset
9c34b5a055 Fix badges formatting 2015-07-23 00:53:10 +02:00
77 changed files with 5461 additions and 2149 deletions

View File

@@ -1,23 +1,14 @@
environment:
CARGO_TARGET: x86_64-pc-windows-gnu
matrix:
- TARGET: x86_64-pc-windows-msvc
- TARGET: i686-pc-windows-gnu
matrix:
- TARGET: x86_64-pc-windows-msvc
install:
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rustc-nightly-${env:TARGET}.tar.gz"
- ps: Start-FileDownload "https://static.rust-lang.org/cargo-dist/cargo-nightly-${env:CARGO_TARGET}.tar.gz"
- 7z x rustc-nightly-%TARGET%.tar.gz > nul
- 7z x rustc-nightly-%TARGET%.tar > nul
- 7z x cargo-nightly-%CARGO_TARGET%.tar.gz > nul
- 7z x cargo-nightly-%CARGO_TARGET%.tar > nul
- call "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" amd64
- set PATH=%PATH%;%cd%/rustc-nightly-%TARGET%/rustc/bin
- set PATH=%PATH%;%cd%/cargo-nightly-%CARGO_TARGET%/cargo/bin
- SET PATH=%PATH%;C:\MinGW\bin
- rustc -V
- cargo -V
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
- rustup-init.exe -y --default-host %TARGET%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -vV
- cargo -vV
build: false
test_script:
- cargo test --verbose --no-default-features
- cargo build --verbose --manifest-path lettre/Cargo.toml
- cargo test --verbose --manifest-path lettre_email/Cargo.toml

20
.build-scripts/codecov.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -xe
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz
tar xzf master.tar.gz
cd kcov-master
mkdir build
cd build
cmake ..
make
make install DESTDIR=../../kcov-build
cd ../..
rm -rf kcov-master
for file in target/debug/lettre*[^\.d]; do
mkdir -p "target/cov/$(basename $file)"
./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"
done
bash <(curl -s https://codecov.io/bash)
echo "Uploaded code coverage"

10
.build-scripts/site-upload.sh Executable file
View File

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

7
.clog.toml Normal file
View File

@@ -0,0 +1,7 @@
[clog]
repository = "https://github.com/lettre/lettre"
changelog = "CHANGELOG.md"
[sections]
Style = ["style"]
Documentation = ["docs"]

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.

7
.gitignore vendored
View File

@@ -1,3 +1,6 @@
.project
/target/
.vscode/
.project/
.idea/
lettre.iml
target/
/Cargo.lock

View File

@@ -1,21 +1,29 @@
language: rust
sudo: required
rust:
- stable
- beta
- nightly
- stable
- beta
- nightly
- 1.32.0
matrix:
allow_failures:
- rust: nightly
sudo: required
addons:
apt:
packages:
- postfix
- libcurl4-openssl-dev
- libelf-dev
- libdw-dev
- cmake
- gcc
- binutils-dev
- libiberty-dev
before_script:
- pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH
- smtp-sink 2525 1000&
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
script:
- |
travis-cargo build &&
travis-cargo test &&
travis-cargo doc
- cargo test --verbose --all --all-features
after_success:
- travis-cargo --only stable doc-upload
- travis-cargo --only stable coveralls
env:
global:
secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc="
- ./.build-scripts/codecov.sh
- '[ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_BRANCH" = "v0.9.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'

205
CHANGELOG.md Normal file
View File

@@ -0,0 +1,205 @@
<a name="v0.9.6"></a>
### v0.9.6 (2021-05-22)
#### Bug Fixes
* **transport**
* **SECURITY**: Prevent SMTP command injection in smtp transport
<a name="v0.9.5"></a>
### v0.9.5 (2020-11-11)
#### Bug Fixes
* **transport**
* **SECURITY**: Prevent argument injection in sendmail transport
<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)
#### Bug Fixes
* **transport:** Write timeout is not set in smtp transport ([cc3580a8](https://github.com/lettre/lettre/commit/cc3580a8942e11c2addf6677f05e16fb451c7ea0))
#### Style
* **all:** Fix typos ([360c42ff](https://github.com/lettre/lettre/commit/360c42ffb8f706222eaad14e72619df1e4857814))
#### Features
* **all:**
* Add set -xe option to build scripts ([57bbabaa](https://github.com/lettre/lettre/commit/57bbabaa6a10cc1a4de6f379e25babfee7adf6ad))
* Move post-success scripts to separate files ([3177b58c](https://github.com/lettre/lettre/commit/3177b58c6d11ffae73c958713f6f0084173924e1))
* Add website upload to travis build script ([a5294df6](https://github.com/lettre/lettre/commit/a5294df63728e14e24eeb851bb4403abd6a7bd36))
* Add codecov upload in travis ([a03bfa00](https://github.com/lettre/lettre/commit/a03bfa008537b1d86ff789d0823e89ad5d99bd79))
* Update README to put useful links at the top ([1ebbe660](https://github.com/lettre/lettre/commit/1ebbe660f5e142712f702c02d5d1e45211763b42))
* Update badges in README and Cargo.toml ([f7ee5c42](https://github.com/lettre/lettre/commit/f7ee5c427ad71e4295f2f1d8e3e9e2dd850223e8))
* Move docs from hugo to gitbook ([27935e32](https://github.com/lettre/lettre/commit/27935e32ef097db8db004569f35cad1d6cd30eca))
* **transport:** Use md-5 and hmac instead of rust-crypto ([0cf018a8](https://github.com/lettre/lettre/commit/0cf018a85e4ea1ad16c7216670da560cc915ec32))
<a name="v0.8.1"></a>
### v0.8.1 (2018-04-11)
#### Fix
* **all:**
* Replace skeptic by some custom rustdoc invocations ([81bad131](https://github.com/lettre/lettre/commit/81bad1317519d330c46ea02f2b7a266b97cc00dd))
#### Documentation
* **all:**
* Add changelog sections for style and docs ([b4d03ead](https://github.com/lettre/lettre/commit/b4d03ead8cce04e0c3d65a30e7a07acca9530f30))
* Use clog to generate changelogs ([8981a775](https://github.com/lettre/lettre/commit/8981a7758c89be69974ef204c4390744aea94e4f), closes [#233](https://github.com/lettre/lettre/issues/233))
#### Style
* **transport-smtp:** Avoid useless empty format strings ([f3271715](https://github.com/lettre/lettre/commit/f3271715ecaf2793c9064462184867e4f22b0ead))
<a name="v0.8.0"></a>
### v0.8.0 (2018-03-31)
#### Added
* Support binary files as attachment
* Move doc to a dedicated website
* Add tests for the doc using skeptic
* Added a code of conduct
* Use hostname as `ClientId` when available
#### Changed
* Detail in SMTP Response is now an enum
* Use nom for parsing smtp responses
* `Envelope` was moved from `lettre_email` to `lettre`
* `EmailAddress::new()` now returns a `Result`
* `SendableEmail` replaces `from` and `to` by `envelope` that returns an `Envelope`
* `File` transport storage format has changed
#### Fixed
* Add missing "Bcc" headers when building the email
* Specify utf-8 charset for html
* Use parts for text and html methods to work with attachments
#### Removed
* `get_ehlo` and `reset` in SmtpTransport are now private
<a name="v0.7.0"></a>
### v0.7.0 (2017-10-08)
#### Added
* Allow validating server certificate
* Initial (incomplete) attachments support
#### Changed
* Split into the *lettre* and *lettre_email* crates
* A lot of small improvements
* Use *tls-native* instead of *openssl* in smtp transport
<a name="v0.6.2"></a>
### v0.6.2 (2017-02-18)
#### Changed
* Update env-logger crate to 0.4
* Update openssl crate to 0.9
* Update uuid crate to 0.4
<a name="v0.6.1"></a>
### v0.6.1 (2016-10-19)
#### Changes
* **documentation**
* #91: Build separate docs for each release
* #96: Add complete documentation information to README
#### Fixed
* #85: Use address-list for "To", "From" etc.
* #93: Force building tests before coverage computing
<a name="v0.6.0"></a>
### v0.6.0 (2016-05-05)
#### Changes
* multipart support
* add non-consuming methods for Email builders
* `add_header` does not return the builder anymore,
for consistency with other methods. Use the `header`
method instead

46
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.at. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

35
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,35 @@
## Contributing to Lettre
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
### Code formatting
All code must be formatted using `rustfmt`.
### Commit Message Format
Each commit message consists of a header, a body and a footer. The header has a special format that includes a type, a scope and a subject:
```text
<type>(<scope>): <subject> <BLANK LINE> <body> <BLANK LINE> <footer>
```
Any line of the commit message cannot be longer 72 characters.
**type** must be one of the following:
feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
perf: A code change that improves performance
**scope** is the lettre part that is being touched. Examples:
email
transport-smtp
transport-file
transport
all
The body explains the change, and the footer contains relevant changelog notes and references to fixed issues.

View File

@@ -1,26 +1,5 @@
[package]
name = "smtp"
version = "0.1.1"
description = "Simple SMTP client"
readme = "README.md"
documentation = "http://amousset.me/rust-smtp/smtp/"
repository = "https://github.com/amousset/rust-smtp"
license = "MIT"
authors = ["Alexis Mousset <alexis.mousset@gmx.fr>"]
keywords = ["email", "smtp", "mailer"]
[dependencies]
time = "0.1"
uuid = "0.1"
log = "0.3"
rustc-serialize = "0.3"
rust-crypto = "0.2"
bufstream = "0.1"
email = "0.0"
[dev-dependencies]
env_logger = "0.3"
[features]
unstable = []
[workspace]
members = [
"lettre",
"lettre_email",
]

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014 Alexis Mousset
Copyright (c) 2014-2018 Alexis Mousset
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated

View File

@@ -1,22 +1,94 @@
rust-smtp [![Build Status](https://travis-ci.org/amousset/rust-smtp.svg?branch=master)](https://travis-ci.org/amousset/rust-smtp) [![Coverage Status](https://coveralls.io/repos/github/amousset/rust-smtp/badge.svg?branch=master)](https://coveralls.io/github/amousset/rust-smtp?branch=master) [![](https://meritbadge.herokuapp.com/smtp)](https://crates.io/crates/smtp)[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
=========
# lettre
This library implements a simple SMTP client.
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information.
**Lettre is a mailer library for Rust.**
Install
-------
[![Build Status](https://travis-ci.org/lettre/lettre.svg?branch=master)](https://travis-ci.org/lettre/lettre)
[![Build status](https://ci.appveyor.com/api/projects/status/mpwglemugjtkps2d/branch/master?svg=true)](https://ci.appveyor.com/project/amousset/lettre/branch/master)
[![codecov](https://codecov.io/gh/lettre/lettre/branch/master/graph/badge.svg)](https://codecov.io/gh/lettre/lettre)
[![Crate](https://img.shields.io/crates/v/lettre.svg)](https://crates.io/crates/lettre)
[![Docs](https://docs.rs/lettre/badge.svg)](https://docs.rs/lettre/)
[![Required Rust version](https://img.shields.io/badge/rustc-1.20-green.svg)]()
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Gitter](https://badges.gitter.im/lettre/lettre.svg)](https://gitter.im/lettre/lettre?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/lettre/lettre.svg)](http://isitmaintained.com/project/lettre/lettre "Average time to resolve an issue")
[![Percentage of issues still open](http://isitmaintained.com/badge/open/lettre/lettre.svg)](http://isitmaintained.com/project/lettre/lettre "Percentage of issues still open")
Useful links:
* [User documentation](http://lettre.at/)
* [API documentation](https://docs.rs/lettre/)
* [Changelog](https://github.com/lettre/lettre/blob/master/CHANGELOG.md)
---
## Features
Lettre provides the following features:
* Multiple transport methods
* Unicode support (for email content and addresses)
* Secure delivery with SMTP using encryption and authentication
* Easy email builders
## Example
This library requires Rust 1.32 or newer.
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
smtp = "0.1"
lettre = "0.9"
lettre_email = "0.9"
```
License
-------
```rust,no_run
extern crate lettre;
extern crate lettre_email;
use lettre::{EmailTransport, SmtpTransport};
use lettre_email::EmailBuilder;
use std::path::Path;
fn main() {
let email = EmailBuilder::new()
// Addresses can be specified by the tuple (email, alias)
.to(("user@example.org", "Firstname Lastname"))
// ... or by an address only
.from("user@example.com")
.subject("Hi, Hello world")
.text("Hello world.")
.build()
.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);
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}
```
## Testing
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
## Code of conduct
Anyone who interacts with Lettre in any space, including but not limited to
this GitHub repository, must follow our [code of conduct](https://github.com/lettre/lettre/blob/master/CODE_OF_CONDUCT.md).
## License
This program is distributed under the terms of the MIT license.
See LICENSE for details.
See [LICENSE](./LICENSE) for details.

View File

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

1
lettre/CHANGELOG.md Symbolic link
View File

@@ -0,0 +1 @@
../CHANGELOG.md

53
lettre/Cargo.toml Normal file
View File

@@ -0,0 +1,53 @@
[package]
name = "lettre"
version = "0.9.6" # remember to update html_root_url
description = "Email client"
readme = "README.md"
homepage = "http://lettre.at"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>"]
categories = ["email"]
keywords = ["email", "smtp", "mailer"]
[badges]
travis-ci = { repository = "lettre/lettre" }
appveyor = { repository = "lettre/lettre" }
maintenance = { status = "actively-developed" }
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
is-it-maintained-open-issues = { repository = "lettre/lettre" }
[dependencies]
log = "^0.4"
nom = { version = "^4.0", optional = true }
bufstream = { version = "^0.1", optional = true }
native-tls = { version = "^0.2", optional = true }
base64 = { version = "^0.10", optional = true }
hostname = { version = "^0.1", 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.6"
glob = "0.3"
[features]
default = ["file-transport", "smtp-transport", "sendmail-transport"]
unstable = []
serde-impls = ["serde", "serde_derive"]
file-transport = ["serde-impls", "serde_json"]
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"]

1
lettre/LICENSE Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE

1
lettre/README.md Symbolic link
View File

@@ -0,0 +1 @@
../README.md

View File

@@ -0,0 +1,50 @@
#![feature(test)]
extern crate lettre;
extern crate test;
use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, Envelope, SmtpTransport};
use lettre::{EmailAddress, SendableEmail, Transport};
#[bench]
fn bench_simple_send(b: &mut test::Bencher) {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.build();
b.iter(|| {
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);
assert!(result.is_ok());
});
}
#[bench]
fn bench_reuse_send(b: &mut test::Bencher) {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.build();
b.iter(|| {
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);
assert!(result.is_ok());
});
sender.close()
}

31
lettre/examples/smtp.rs Normal file
View File

@@ -0,0 +1,31 @@
extern crate env_logger;
extern crate lettre;
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
fn main() {
env_logger::init();
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 = SmtpClient::new_unencrypted_localhost().unwrap().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());
}

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

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

@@ -0,0 +1,61 @@
//! Error and result type for file transport
use self::Error::*;
use serde_json;
use std::io;
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Internal client error
Client(&'static str),
/// IO error
Io(io::Error),
/// JSON serialization error
JsonSerialization(serde_json::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
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 cause(&self) -> Option<&dyn StdError> {
match *self {
Io(ref err) => Some(&*err),
JsonSerialization(ref err) => Some(&*err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::JsonSerialization(err)
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Error::Client(string)
}
}
/// SMTP result type
pub type FileResult = Result<(), Error>;

60
lettre/src/file/mod.rs Normal file
View File

@@ -0,0 +1,60 @@
//! The file transport writes the emails to the given directory. The name of the file will be
//! `message_id.txt`.
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
//!
use file::error::FileResult;
use serde_json;
use std::fs::File;
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 FileTransport {
path: PathBuf,
}
impl FileTransport {
/// Creates a new transport to the given directory
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
FileTransport {
path: PathBuf::from(path.as_ref()),
}
}
}
#[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!("{}.json", message_id));
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(())
}
}

213
lettre/src/lib.rs Normal file
View File

@@ -0,0 +1,213 @@
//! 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.
//!
#![doc(html_root_url = "https://docs.rs/lettre/0.9.6")]
#![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 = "smtp-transport")]
extern crate hostname;
#[macro_use]
extern crate log;
#[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;
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::FileTransport;
#[cfg(feature = "sendmail-transport")]
pub use sendmail::SendmailTransport;
#[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::str::FromStr;
/// Email address
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EmailAddress(String);
impl EmailAddress {
pub fn new(address: String) -> EmailResult<EmailAddress> {
if !is_valid_email(&address) && !address.ends_with("localhost") {
return Err(Error::InvalidEmailAddress);
}
Ok(EmailAddress(address))
}
}
impl FromStr for EmailAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
EmailAddress::new(s.to_string())
}
}
impl Display for EmailAddress {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
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).
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Envelope {
/// The envelope recipients' addresses
///
/// This can not be empty.
forward_path: Vec<EmailAddress>,
/// The envelope sender address
reverse_path: Option<EmailAddress>,
}
impl Envelope {
/// Creates a new envelope, which may fail if `to` is empty.
pub fn new(from: Option<EmailAddress>, to: Vec<EmailAddress>) -> EmailResult<Envelope> {
if to.is_empty() {
return Err(Error::MissingTo);
}
Ok(Envelope {
forward_path: to,
reverse_path: from,
})
}
/// Destination addresses of the envelope
pub fn to(&self) -> &[EmailAddress] {
self.forward_path.as_slice()
}
/// Source address of the envelope
pub fn from(&self) -> Option<&EmailAddress> {
self.reverse_path.as_ref()
}
}
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),
}
}
}
/// Sendable email structure
pub struct SendableEmail {
envelope: Envelope,
message_id: String,
message: Message,
}
impl SendableEmail {
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Bytes(Cursor::new(message)),
}
}
pub fn new_with_reader(
envelope: Envelope,
message_id: String,
message: Box<dyn Read + Send>,
) -> SendableEmail {
SendableEmail {
envelope,
message_id,
message: Message::Reader(message),
}
}
pub fn envelope(&self) -> &Envelope {
&self.envelope
}
pub fn message_id(&self) -> &str {
&self.message_id
}
pub fn message(self) -> Message {
self.message
}
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)
}
}
/// Transport method for emails
pub trait Transport<'a> {
/// Result type for the transport
type Result;
/// Sends the email
fn send(&mut self, email: SendableEmail) -> Self::Result;
}

View File

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

View File

@@ -0,0 +1,79 @@
//! The sendmail transport sends the email using the local sendmail command.
//!
use sendmail::error::SendmailResult;
use std::io::prelude::*;
use std::io::Read;
use std::process::{Command, Stdio};
use SendableEmail;
use Transport;
pub mod error;
/// Sends an email using the `sendmail` command
#[derive(Debug, Default)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct SendmailTransport {
command: String,
}
impl SendmailTransport {
/// Creates a new transport with the default `/usr/sbin/sendmail` command
pub fn new() -> SendmailTransport {
SendmailTransport {
command: "/usr/sbin/sendmail".to_string(),
}
}
/// Creates a new transport to the given sendmail command
pub fn new_with_command<S: Into<String>>(command: S) -> SendmailTransport {
SendmailTransport {
command: command.into(),
}
}
}
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 mut process = Command::new(&self.command)
.arg("-i")
.arg("-f")
.arg(
email
.envelope()
.from()
.map(|x| x.as_ref())
.unwrap_or("\"\""),
)
.arg("--")
.args(email.envelope.to())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content);
process
.stdin
.as_mut()
.unwrap()
.write_all(message_content.as_bytes())?;
info!("Wrote {} message to stdin", message_id);
let output = process.wait_with_output()?;
if output.status.success() {
Ok(())
} else {
// TODO display stderr
Err(error::Error::Client("The message could not be sent"))
}
}
}

View File

@@ -0,0 +1,178 @@
//! Provides limited SASL authentication mechanisms
use smtp::error::Error;
use std::fmt::{self, Display, Formatter};
/// Accepted authentication mechanisms on an encrypted connection
/// Trying LOGIN last as it is deprecated.
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
/// Accepted authentication mechanisms on an unencrypted connection
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
/// Convertable to user credentials
pub trait IntoCredentials {
/// Converts to a `Credentials` struct
fn into_credentials(self) -> Credentials;
}
impl IntoCredentials for Credentials {
fn into_credentials(self) -> Credentials {
self
}
}
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
fn into_credentials(self) -> Credentials {
let (username, password) = self;
Credentials::new(username.into(), password.into())
}
}
/// Contains user credentials
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Credentials {
authentication_identity: String,
secret: String,
}
impl Credentials {
/// Create a `Credentials` struct from username and password
pub fn new(username: String, password: String) -> Credentials {
Credentials {
authentication_identity: username,
secret: password,
}
}
}
/// Represents authentication mechanisms
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Mechanism {
/// PLAIN authentication mechanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
Plain,
/// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login,
/// Non-standard XOAUTH2 mechanism
/// https://developers.google.com/gmail/imap/xoauth2-protocol
Xoauth2,
}
impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{}",
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
Mechanism::Xoauth2 => "XOAUTH2",
}
)
}
}
impl Mechanism {
/// Does the mechanism supports initial response
pub fn supports_initial_response(self) -> bool {
match self {
Mechanism::Plain | Mechanism::Xoauth2 => true,
Mechanism::Login => false,
}
}
/// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases
pub fn response(
self,
credentials: &Credentials,
challenge: Option<&str>,
) -> Result<String, Error> {
match self {
Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
)),
},
Mechanism::Login => {
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.authentication_identity.to_string());
}
if vec!["Password", "Password:"].contains(&decoded_challenge) {
return Ok(credentials.secret.to_string());
}
Err(Error::Client("Unrecognized challenge"))
}
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
)),
},
}
}
}
#[cfg(test)]
mod test {
use super::{Credentials, Mechanism};
#[test]
fn test_plain() {
let mechanism = Mechanism::Plain;
let credentials = Credentials::new("username".to_string(), "password".to_string());
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
"\u{0}username\u{0}password"
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
#[test]
fn test_login() {
let mechanism = Mechanism::Login;
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
assert_eq!(
mechanism.response(&credentials, Some("Username")).unwrap(),
"alice"
);
assert_eq!(
mechanism.response(&credentials, Some("Password")).unwrap(),
"wonderland"
);
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new(
"username".to_string(),
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
);
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
}

View File

@@ -0,0 +1,120 @@
#![allow(missing_docs)]
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
use std::io::{self, Cursor, Read, Write};
use std::sync::{Arc, Mutex};
pub type MockCursor = Cursor<Vec<u8>>;
#[derive(Clone, Debug)]
pub struct MockStream {
reader: Arc<Mutex<MockCursor>>,
writer: Arc<Mutex<MockCursor>>,
}
impl Default for MockStream {
fn default() -> Self {
Self::new()
}
}
impl MockStream {
pub fn new() -> MockStream {
MockStream {
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
}
pub fn with_vec(vec: Vec<u8>) -> MockStream {
MockStream {
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
}
pub fn take_vec(&mut self) -> Vec<u8> {
let mut cursor = self.writer.lock().unwrap();
let vec = cursor.get_ref().to_vec();
cursor.set_position(0);
cursor.get_mut().clear();
vec
}
pub fn next_vec(&mut self, vec: &[u8]) {
let mut cursor = self.reader.lock().unwrap();
cursor.set_position(0);
cursor.get_mut().clear();
cursor.get_mut().extend_from_slice(vec);
}
pub fn swap(&mut self) {
let mut cur_write = self.writer.lock().unwrap();
let mut cur_read = self.reader.lock().unwrap();
let vec_write = cur_write.get_ref().to_vec();
let vec_read = cur_read.get_ref().to_vec();
cur_write.set_position(0);
cur_read.set_position(0);
cur_write.get_mut().clear();
cur_read.get_mut().clear();
// swap cursors
cur_read.get_mut().extend_from_slice(vec_write.as_slice());
cur_write.get_mut().extend_from_slice(vec_read.as_slice());
}
}
impl Write for MockStream {
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
self.writer.lock().unwrap().write(msg)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.lock().unwrap().flush()
}
}
impl Read for MockStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.lock().unwrap().read(buf)
}
}
#[cfg(test)]
mod test {
use super::MockStream;
use std::io::{Read, Write};
#[test]
fn write_take_test() {
let mut mock = MockStream::new();
// write to mock stream
mock.write(&[1, 2, 3]).unwrap();
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
}
#[test]
fn read_with_vec_test() {
let mut mock = MockStream::with_vec(vec![4, 5]);
let mut vec = Vec::new();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![4, 5]);
}
#[test]
fn clone_test() {
let mut mock = MockStream::new();
let mut cloned = mock.clone();
mock.write(&[6, 7]).unwrap();
assert_eq!(cloned.take_vec(), vec![6, 7]);
}
#[test]
fn swap_test() {
let mut mock = MockStream::new();
let mut vec = Vec::new();
mock.write(&[8, 9, 10]).unwrap();
mock.swap();
mock.read_to_end(&mut vec).unwrap();
assert_eq!(vec, vec![8, 9, 10]);
}
}

View File

@@ -0,0 +1,321 @@
//! SMTP client
use bufstream::BufStream;
use nom::ErrorKind as NomErrorKind;
use smtp::authentication::{Credentials, Mechanism};
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
use smtp::commands::*;
use smtp::error::{Error, SmtpResult};
use smtp::response::Response;
use std::fmt::{Debug, Display};
use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::ToSocketAddrs;
use std::string::String;
use std::time::Duration;
pub mod mock;
pub mod net;
/// The codec used for transparency
#[derive(Default, Clone, Copy, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct ClientCodec {
escape_count: u8,
}
impl ClientCodec {
/// Creates a new client codec
pub fn new() -> Self {
ClientCodec::default()
}
}
impl ClientCodec {
/// Adds transparency
/// TODO: replace CR and LF by CRLF
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) -> Result<(), Error> {
match frame.len() {
0 => {
match self.escape_count {
0 => buf.write_all(b"\r\n.\r\n")?,
1 => buf.write_all(b"\n.\r\n")?,
2 => buf.write_all(b".\r\n")?,
_ => unreachable!(),
}
self.escape_count = 0;
Ok(())
}
_ => {
let mut start = 0;
for (idx, byte) in frame.iter().enumerate() {
match self.escape_count {
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
2 => {
self.escape_count = if *byte == b'.' {
3
} else if *byte == b'\r' {
1
} else {
0
}
}
_ => unreachable!(),
}
if self.escape_count == 3 {
self.escape_count = 0;
buf.write_all(&frame[start..idx])?;
buf.write_all(b".")?;
start = idx;
}
}
buf.write_all(&frame[start..])?;
Ok(())
}
}
}
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays
fn escape_crlf(string: &str) -> String {
string.replace("\r\n", "<CRLF>")
}
/// Structure that implements the SMTP client
#[derive(Debug, Default)]
pub struct InnerClient<S: Write + Read = NetworkStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<BufStream<S>>,
}
macro_rules! return_err (
($err: expr, $client: ident) => ({
return Err(From::from($err))
})
);
#[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() -> InnerClient<S> {
InnerClient { stream: None }
}
}
impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.command(QuitCommand);
self.stream = None;
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: S) {
self.stream = Some(BufStream::new(stream));
}
/// Upgrades the underlying connection to SSL/TLS
pub fn upgrade_tls_stream(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
match self.stream {
Some(ref mut stream) => stream.get_mut().upgrade_tls(tls_parameters),
None => Ok(()),
}
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
match self.stream {
Some(ref stream) => stream.get_ref().is_encrypted(),
None => false,
}
}
/// Set timeout
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match self.stream {
Some(ref mut stream) => {
stream.get_mut().set_read_timeout(duration)?;
stream.get_mut().set_write_timeout(duration)?;
Ok(())
}
None => Ok(()),
}
}
/// Connects to the configured server
pub fn connect<A: ToSocketAddrs>(
&mut self,
addr: &A,
tls_parameters: Option<&ClientTlsParameters>,
) -> SmtpResult {
// Connect should not be called when the client is already connected
if self.stream.is_some() {
return_err!("The connection is already established", self);
}
let mut addresses = addr.to_socket_addrs()?;
let server_addr = match addresses.next() {
Some(addr) => addr,
None => return_err!("Could not resolve hostname", self),
};
debug!("connecting to {}", server_addr);
// Try to connect
self.set_stream(Connector::connect(&server_addr, tls_parameters)?);
self.read_response()
}
/// Checks if the server is connected using the NOOP SMTP command
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
pub fn is_connected(&mut self) -> bool {
self.stream.is_some() && self.command(NoopCommand).is_ok()
}
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
// TODO
let mut challenges = 10;
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
while challenges > 0 && response.has_code(334) {
challenges -= 1;
response = self.command(AuthCommand::new_from_response(
mechanism,
credentials.clone(),
&response,
)?)?;
}
if challenges == 0 {
Err(Error::ResponseParsing("Unexpected number of challenges"))
} else {
Ok(response)
}
}
/// Sends the message content
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);
loop {
out_buf.clear();
let consumed = match message_reader.fill_buf() {
Ok(bytes) => {
codec.encode(bytes, &mut out_buf)?;
bytes.len()
}
Err(ref err) => panic!("Failed with: {}", err),
};
message_reader.consume(consumed);
if consumed == 0 {
break;
}
self.write(out_buf.as_slice())?;
}
self.write(b"\r\n.\r\n")?;
self.read_response()
}
/// Sends an SMTP command
pub fn command<C: Display>(&mut self, command: C) -> SmtpResult {
self.write(command.to_string().as_bytes())?;
self.read_response()
}
/// Writes a string to the server
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
if self.stream.is_none() {
return Err(From::from("Connection closed"));
}
self.stream.as_mut().unwrap().write_all(string)?;
self.stream.as_mut().unwrap().flush()?;
debug!(
"Wrote: {}",
escape_crlf(String::from_utf8_lossy(string).as_ref())
);
Ok(())
}
/// Gets the SMTP response
fn read_response(&mut self) -> SmtpResult {
let mut raw_response = String::new();
let mut response = raw_response.parse::<Response>();
while response.is_err() {
if response.as_ref().err().unwrap() != &NomErrorKind::Complete {
break;
}
// TODO read more than one line
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>();
}
debug!("Read: {}", escape_crlf(raw_response.as_ref()));
let final_response = response?;
if final_response.is_positive() {
Ok(final_response)
} else {
Err(From::from(final_response))
}
}
}
#[cfg(test)]
mod test {
use super::{escape_crlf, ClientCodec};
#[test]
fn test_codec() {
let mut codec = ClientCodec::new();
let mut buf: Vec<u8> = vec![];
assert!(codec.encode(b"test\r\n", &mut buf).is_ok());
assert!(codec.encode(b"test\r\n\r\n", &mut buf).is_ok());
assert!(codec.encode(b".\r\n", &mut buf).is_ok());
assert!(codec.encode(b"\r\ntest", &mut buf).is_ok());
assert!(codec.encode(b"te\r\n.\r\nst", &mut buf).is_ok());
assert!(codec.encode(b"test", &mut buf).is_ok());
assert!(codec.encode(b"test.", &mut buf).is_ok());
assert!(codec.encode(b"test\n", &mut buf).is_ok());
assert!(codec.encode(b".test\n", &mut buf).is_ok());
assert!(codec.encode(b"test", &mut buf).is_ok());
assert_eq!(
String::from_utf8(buf).unwrap(),
"test\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
);
}
#[test]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
assert_eq!(
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CRLF>SIZE 42<CRLF>"
);
}
}

View File

@@ -0,0 +1,172 @@
//! A trait to represent a stream
use native_tls::{Protocol, TlsConnector, TlsStream};
use smtp::client::mock::MockStream;
use std::io::{self, ErrorKind, Read, Write};
use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream};
use std::time::Duration;
/// Parameters to use for secure clients
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub struct ClientTlsParameters {
/// A connector from `native-tls`
pub connector: TlsConnector,
/// The domain to send during the TLS handshake
pub domain: String,
}
impl ClientTlsParameters {
/// Creates a `ClientTlsParameters`
pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters {
ClientTlsParameters { connector, domain }
}
}
/// Accepted protocols by default.
/// 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
pub enum NetworkStream {
/// Plain TCP stream
Tcp(TcpStream),
/// Encrypted TCP stream
Tls(TlsStream<TcpStream>),
/// Mock stream
Mock(MockStream),
}
impl NetworkStream {
/// Returns peer's address
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
match *self {
NetworkStream::Tcp(ref s) => s.peer_addr(),
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(127, 0, 0, 1),
80,
))),
}
}
/// Shutdowns the connection
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref s) => s.shutdown(how),
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
NetworkStream::Mock(_) => Ok(()),
}
}
}
impl Read for NetworkStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
NetworkStream::Tcp(ref mut s) => s.read(buf),
NetworkStream::Tls(ref mut s) => s.read(buf),
NetworkStream::Mock(ref mut s) => s.read(buf),
}
}
}
impl Write for NetworkStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
NetworkStream::Tcp(ref mut s) => s.write(buf),
NetworkStream::Tls(ref mut s) => s.write(buf),
NetworkStream::Mock(ref mut s) => s.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut s) => s.flush(),
NetworkStream::Tls(ref mut s) => s.flush(),
NetworkStream::Mock(ref mut s) => s.flush(),
}
}
}
/// A trait for the concept of opening a stream
pub trait Connector: Sized {
/// Opens a connection to the given IP socket
fn connect(addr: &SocketAddr, tls_parameters: Option<&ClientTlsParameters>)
-> io::Result<Self>;
/// Upgrades to TLS connection
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()>;
/// Is the NetworkStream encrypted
fn is_encrypted(&self) -> bool;
}
impl Connector for NetworkStream {
fn connect(
addr: &SocketAddr,
tls_parameters: Option<&ClientTlsParameters>,
) -> io::Result<NetworkStream> {
let tcp_stream = TcpStream::connect(addr)?;
match tls_parameters {
Some(context) => context
.connector
.connect(context.domain.as_ref(), tcp_stream)
.map(NetworkStream::Tls)
.map_err(|e| io::Error::new(ErrorKind::Other, e)),
None => Ok(NetworkStream::Tcp(tcp_stream)),
}
}
#[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
.connector
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
{
Ok(tls_stream) => NetworkStream::Tls(tls_stream),
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
},
NetworkStream::Tls(_) => return Ok(()),
NetworkStream::Mock(_) => return Ok(()),
};
Ok(())
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Tcp(_) => false,
NetworkStream::Tls(_) => true,
NetworkStream::Mock(_) => false,
}
}
}
/// A trait for read and write timeout support
pub trait Timeout: Sized {
/// Set read timeout for IO calls
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
/// Set write timeout for IO calls
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
}
impl Timeout for NetworkStream {
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
/// Set write timeout for IO calls
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
match *self {
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
NetworkStream::Mock(_) => Ok(()),
}
}
}

378
lettre/src/smtp/commands.rs Normal file
View File

@@ -0,0 +1,378 @@
#![cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
//! SMTP commands
use base64;
use smtp::authentication::{Credentials, Mechanism};
use smtp::error::Error;
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)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct EhloCommand {
client_id: ClientId,
}
impl Display for EhloCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EHLO {}\r\n", self.client_id)
}
}
impl EhloCommand {
/// Creates a EHLO command
pub fn new(client_id: ClientId) -> EhloCommand {
EhloCommand { client_id }
}
}
/// STARTTLS command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct StarttlsCommand;
impl Display for StarttlsCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("STARTTLS\r\n")
}
}
/// MAIL command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct MailCommand {
sender: Option<EmailAddress>,
parameters: Vec<MailParameter>,
}
impl Display for MailCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"MAIL FROM:<{}>",
self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str("\r\n")
}
}
impl MailCommand {
/// Creates a MAIL command
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
MailCommand { sender, parameters }
}
}
/// RCPT command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct RcptCommand {
recipient: EmailAddress,
parameters: Vec<RcptParameter>,
}
impl Display for RcptCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "RCPT TO:<{}>", self.recipient)?;
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str("\r\n")
}
}
impl RcptCommand {
/// Creates an RCPT command
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
RcptCommand {
recipient,
parameters,
}
}
}
/// DATA command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct DataCommand;
impl Display for DataCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("DATA\r\n")
}
}
/// QUIT command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct QuitCommand;
impl Display for QuitCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("QUIT\r\n")
}
}
/// NOOP command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct NoopCommand;
impl Display for NoopCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NOOP\r\n")
}
}
/// HELP command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct HelpCommand {
argument: Option<String>,
}
impl Display for HelpCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("HELP")?;
if self.argument.is_some() {
write!(f, " {}", self.argument.as_ref().unwrap())?;
}
f.write_str("\r\n")
}
}
impl HelpCommand {
/// Creates an HELP command
pub fn new(argument: Option<String>) -> HelpCommand {
HelpCommand { argument }
}
}
/// VRFY command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct VrfyCommand {
argument: String,
}
impl Display for VrfyCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
#[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
write!(f, "VRFY {}\r\n", self.argument)
}
}
impl VrfyCommand {
/// Creates a VRFY command
pub fn new(argument: String) -> VrfyCommand {
VrfyCommand { argument }
}
}
/// EXPN command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct ExpnCommand {
argument: String,
}
impl Display for ExpnCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EXPN {}\r\n", self.argument)
}
}
impl ExpnCommand {
/// Creates an EXPN command
pub fn new(argument: String) -> ExpnCommand {
ExpnCommand { argument }
}
}
/// RSET command
#[derive(PartialEq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct RsetCommand;
impl Display for RsetCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("RSET\r\n")
}
}
/// AUTH command
#[derive(PartialEq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct AuthCommand {
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
response: Option<String>,
}
impl Display for AuthCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
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())?;
} else {
match encoded_response {
Some(response) => f.write_str(&response)?,
None => write!(f, "AUTH {}", self.mechanism)?,
}
}
f.write_str("\r\n")
}
}
impl AuthCommand {
/// Creates an AUTH command (from a challenge if provided)
pub fn new(
mechanism: Mechanism,
credentials: Credentials,
challenge: Option<String>,
) -> Result<AuthCommand, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_deref())?)
} else {
None
};
Ok(AuthCommand {
mechanism,
credentials,
challenge,
response,
})
}
/// Creates an AUTH command from a response that needs to be a
/// valid challenge (with 334 response code)
pub fn new_from_response(
mechanism: Mechanism,
credentials: Credentials,
response: &Response,
) -> Result<AuthCommand, Error> {
if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a 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 = String::from_utf8(base64::decode(&encoded_challenge)?)?;
debug!("auth decoded challenge: {}", decoded_challenge);
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
Ok(AuthCommand {
mechanism,
credentials,
challenge: Some(decoded_challenge),
response,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use smtp::extension::MailBodyParameter;
#[test]
fn test_display() {
let id = ClientId::Domain("localhost".to_string());
let email = EmailAddress::new("test@example.com".to_string()).unwrap();
let mail_parameter = MailParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
};
let rcpt_parameter = RcptParameter::Other {
keyword: "TEST".to_string(),
value: Some("value".to_string()),
};
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
assert_eq!(
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
"MAIL FROM:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", MailCommand::new(None, vec![])),
"MAIL FROM:<>\r\n"
);
assert_eq!(
format!(
"{}",
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])
),
"MAIL FROM:<test@example.com> SIZE=42\r\n"
);
assert_eq!(
format!(
"{}",
MailCommand::new(
Some(email.clone()),
vec![
MailParameter::Size(42),
MailParameter::Body(MailBodyParameter::EightBitMime),
mail_parameter,
],
)
),
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
);
assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![])),
"RCPT TO:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
"RCPT TO:<test@example.com> TEST=value\r\n"
);
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
assert_eq!(
format!("{}", HelpCommand::new(Some("test".to_string()))),
"HELP test\r\n"
);
assert_eq!(
format!("{}", VrfyCommand::new("test".to_string())),
"VRFY test\r\n"
);
assert_eq!(
format!("{}", ExpnCommand::new("test".to_string())),
"EXPN test\r\n"
);
assert_eq!(format!("{}", RsetCommand), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string());
assert_eq!(
format!(
"{}",
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
),
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
);
assert_eq!(
format!(
"{}",
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
),
"AUTH LOGIN\r\n"
);
}
}

127
lettre/src/smtp/error.rs Normal file
View File

@@ -0,0 +1,127 @@
//! Error and result type for SMTP clients
use self::Error::*;
use base64::DecodeError;
use native_tls;
use nom;
use smtp::response::{Response, Severity};
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use std::string::FromUtf8Error;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Transient(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
Permanent(Response),
/// Error parsing a response
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(DecodeError),
/// Error parsing UTF8in response
Utf8Parsing(FromUtf8Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
Resolution,
/// IO error
Io(io::Error),
/// TLS error
Tls(native_tls::Error),
/// Parsing error
Parsing(nom::ErrorKind),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
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 {
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),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
}
}
impl From<native_tls::Error> for Error {
fn from(err: native_tls::Error) -> Error {
Tls(err)
}
}
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 {
Severity::TransientNegativeCompletion => Transient(response),
Severity::PermanentNegativeCompletion => Permanent(response),
_ => Client("Unknown error code"),
}
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
Client(string)
}
}
/// SMTP result type
pub type SmtpResult = Result<Response, Error>;

View File

@@ -0,0 +1,389 @@
//! ESMTP features
use hostname::get_hostname;
use smtp::authentication::Mechanism;
use smtp::error::Error;
use smtp::response::Response;
use smtp::util::XText;
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::result::Result;
/// Default client id
pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
/// Client identifier, the parameter to `EHLO`
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum ClientId {
/// A fully-qualified domain name
Domain(String),
/// An IPv4 address
Ipv4(Ipv4Addr),
/// An IPv6 address
Ipv6(Ipv6Addr),
}
impl Display for ClientId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
ClientId::Domain(ref value) => f.write_str(value),
ClientId::Ipv4(ref value) => write!(f, "{}", value),
ClientId::Ipv6(ref value) => write!(f, "{}", value),
}
}
}
impl ClientId {
/// Creates a new `ClientId` from a fully qualified domain name
pub fn new(domain: String) -> ClientId {
ClientId::Domain(domain)
}
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
/// found
pub fn hostname() -> ClientId {
ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
}
}
/// Supported ESMTP keywords
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Extension {
/// 8BITMIME keyword
///
/// RFC 6152: https://tools.ietf.org/html/rfc6152
EightBitMime,
/// SMTPUTF8 keyword
///
/// RFC 6531: https://tools.ietf.org/html/rfc6531
SmtpUtfEight,
/// STARTTLS keyword
///
/// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls,
/// AUTH mechanism
Authentication(Mechanism),
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "SMTPUTF8"),
Extension::StartTls => write!(f, "STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
}
}
}
/// Contains information about an SMTP server
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct ServerInfo {
/// Server name
///
/// The name given in the server banner
pub name: String,
/// ESMTP features supported by the server
///
/// It contains the features supported by the server and known by the `Extension` module.
pub features: HashSet<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{} with {}",
self.name,
if self.features.is_empty() {
"no supported features".to_string()
} else {
format!("{:?}", self.features)
}
)
}
}
impl ServerInfo {
/// Parses a EHLO response to create a `ServerInfo`
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(Error::ResponseParsing("Could not read server name")),
};
let mut features: HashSet<Extension> = HashSet::new();
for line in response.message.as_slice() {
if line.is_empty() {
continue;
}
let split: Vec<&str> = line.split_whitespace().collect();
match split[0] {
"8BITMIME" => {
features.insert(Extension::EightBitMime);
}
"SMTPUTF8" => {
features.insert(Extension::SmtpUtfEight);
}
"STARTTLS" => {
features.insert(Extension::StartTls);
}
"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));
}
_ => (),
}
}
}
_ => (),
};
}
Ok(ServerInfo {
name: name.to_string(),
features,
})
}
/// Checks if the server supports an ESMTP feature
pub fn supports_feature(&self, keyword: Extension) -> bool {
self.features.contains(&keyword)
}
/// Checks if the server supports an ESMTP feature
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
self.features
.contains(&Extension::Authentication(mechanism))
}
}
/// A `MAIL FROM` extension parameter
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum MailParameter {
/// `BODY` parameter
Body(MailBodyParameter),
/// `SIZE` parameter
Size(usize),
/// `SMTPUTF8` parameter
SmtpUtfEight,
/// Custom parameter
Other {
/// Parameter keyword
keyword: String,
/// Parameter value
value: Option<String>,
},
}
impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
MailParameter::Size(size) => write!(f, "SIZE={}", size),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other {
ref keyword,
value: Some(ref value),
} => write!(f, "{}={}", keyword, XText(value)),
MailParameter::Other {
ref keyword,
value: None,
} => f.write_str(keyword),
}
}
}
/// Values for the `BODY` parameter to `MAIL FROM`
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum MailBodyParameter {
/// `7BIT`
SevenBit,
/// `8BITMIME`
EightBitMime,
}
impl Display for MailBodyParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
MailBodyParameter::SevenBit => f.write_str("7BIT"),
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
}
}
}
/// A `RCPT TO` extension parameter
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum RcptParameter {
/// Custom parameter
Other {
/// Parameter keyword
keyword: String,
/// Parameter value
value: Option<String>,
},
}
impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
RcptParameter::Other {
ref keyword,
value: Some(ref value),
} => write!(f, "{}={}", keyword, XText(value)),
RcptParameter::Other {
ref keyword,
value: None,
} => f.write_str(keyword),
}
}
}
#[cfg(test)]
mod test {
use super::{ClientId, Extension, ServerInfo};
use smtp::authentication::Mechanism;
use smtp::response::{Category, Code, Detail, Response, Severity};
use std::collections::HashSet;
#[test]
fn test_clientid_fmt() {
assert_eq!(
format!("{}", ClientId::new("test".to_string())),
"test".to_string()
);
}
#[test]
fn test_extension_fmt() {
assert_eq!(
format!("{}", Extension::EightBitMime),
"8BITMIME".to_string()
);
assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string()
);
}
#[test]
fn test_serverinfo_fmt() {
let mut eightbitmime = HashSet::new();
assert!(eightbitmime.insert(Extension::EightBitMime));
assert_eq!(
format!(
"{}",
ServerInfo {
name: "name".to_string(),
features: eightbitmime.clone(),
}
),
"name with {EightBitMime}".to_string()
);
let empty = HashSet::new();
assert_eq!(
format!(
"{}",
ServerInfo {
name: "name".to_string(),
features: empty,
}
),
"name with no supported features".to_string()
);
let mut plain = HashSet::new();
assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
assert_eq!(
format!(
"{}",
ServerInfo {
name: "name".to_string(),
features: plain.clone(),
}
),
"name with {Authentication(Plain)}".to_string()
);
}
#[test]
fn test_serverinfo() {
let response = Response::new(
Code::new(
Severity::PositiveCompletion,
Category::Unspecified4,
Detail::One,
),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
);
let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo {
name: "me".to_string(),
features,
};
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
assert!(server_info.supports_feature(Extension::EightBitMime));
assert!(!server_info.supports_feature(Extension::StartTls));
let response2 = Response::new(
Code::new(
Severity::PositiveCompletion,
Category::Unspecified4,
Detail::One,
),
vec![
"me".to_string(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
);
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo {
name: "me".to_string(),
features: features2,
};
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
assert!(server_info2.supports_feature(Extension::EightBitMime));
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
assert!(!server_info2.supports_feature(Extension::StartTls));
}
}

479
lettre/src/smtp/mod.rs Normal file
View File

@@ -0,0 +1,479 @@
//! The SMTP transport sends emails using the SMTP protocol.
//!
//! This SMTP client follows [RFC
//! 5321](https://tools.ietf.org/html/rfc5321), and is designed to efficiently send emails from an
//! application to a relay email server, as it relies as much as possible on the relay server
//! for sanity and RFC compliance checks.
//!
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
use native_tls::TlsConnector;
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::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use {SendableEmail, Transport};
pub mod authentication;
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:
// https://www.iana.
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port
pub const SMTP_PORT: u16 = 25;
/// Default submission port
pub const SUBMISSION_PORT: u16 = 587;
/// 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 only (for testing purposes)
None,
/// Start with insecure connection and use `STARTTLS` when available
Opportunistic(ClientTlsParameters),
/// Start with insecure connection and require `STARTTLS`
Required(ClientTlsParameters),
/// Use TLS wrapped connection
Wrapper(ClientTlsParameters),
}
/// Configures connection reuse behavior
#[derive(Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum ConnectionReuseParameters {
/// Unlimited connection reuse
ReuseUnlimited,
/// Maximum number of connection reuse
ReuseLimited(u16),
/// Disable connection reuse, close connection after each transaction
NoReuse,
}
/// Contains client configuration
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpClient {
/// Enable connection reuse
connection_reuse: ConnectionReuseParameters,
/// Name sent during EHLO
hello_name: ClientId,
/// Credentials
credentials: Option<Credentials>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// TLS security configuration
security: ClientSecurity,
/// Enable UTF8 mailboxes in envelope or headers
smtp_utf8: bool,
/// Optional enforced authentication mechanism
authentication_mechanism: Option<Mechanism>,
/// Define network timeout
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
timeout: Option<Duration>,
}
/// Builder for the SMTP `SmtpTransport`
impl SmtpClient {
/// Creates a new SMTP client
///
/// Defaults are:
///
/// * No connection reuse
/// * No authentication
/// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands
///
/// 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(SmtpClient {
server_addr: addr,
security,
smtp_utf8: false,
credentials: None,
connection_reuse: ConnectionReuseParameters::NoReuse,
hello_name: ClientId::hostname(),
authentication_mechanism: None,
timeout: Some(Duration::new(60, 0)),
}),
None => Err(Error::Resolution),
}
}
/// 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) -> SmtpClient {
self.smtp_utf8 = enabled;
self
}
/// Set the name used during EHLO
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) -> SmtpClient {
self.connection_reuse = parameters;
self
}
/// Set the client credentials
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) -> SmtpClient {
self.authentication_mechanism = Some(mechanism);
self
}
/// Set the timeout duration
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
self.timeout = timeout;
self
}
/// Build the SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn transport(self) -> SmtpTransport {
SmtpTransport::new(self)
}
}
/// Represents the state of a client
#[derive(Debug)]
struct State {
/// Panic state
pub panic: bool,
/// Connection reuse counter
pub connection_reuse_count: u16,
}
/// Structure that implements the high level SMTP client
#[allow(missing_debug_implementations)]
pub struct SmtpTransport {
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
/// SmtpTransport variable states
state: State,
/// Information about the client
client_info: SmtpClient,
/// Low level client
client: InnerClient,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.close();
}
return Err(From::from(err))
},
}
})
);
impl<'a> SmtpTransport {
/// Creates a new SMTP client
///
/// It does not connect to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpClient) -> SmtpTransport {
let client = InnerClient::new();
SmtpTransport {
client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
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
let ehlo_response = try_smtp!(
self.client.command(EhloCommand::new(ClientId::new(
self.client_info.hello_name.to_string()
),)),
self
);
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
Ok(ehlo_response)
}
/// Reset the client state
pub fn close(&mut self) {
// Close the SMTP transaction if needed
self.client.close();
// Reset the client state
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
}
}
impl<'a> Transport<'a> for SmtpTransport {
type Result = SmtpResult;
/// Sends an email
#[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();
if !self.client.is_connected() {
self.connect()?;
}
// Mail
let mut mail_options = vec![];
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::EightBitMime)
{
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::SmtpUtfEight)
&& self.client_info.smtp_utf8
{
mail_options.push(MailParameter::SmtpUtfEight);
}
try_smtp!(
self.client.command(MailCommand::new(
email.envelope().from().cloned(),
mail_options,
)),
self
);
// Log the mail command
info!(
"{}: from=<{}>",
message_id,
match email.envelope().from() {
Some(address) => address.to_string(),
None => "".to_string(),
}
);
// Recipient
for to_address in email.envelope().to() {
try_smtp!(
self.client
.command(RcptCommand::new(to_address.clone(), vec![])),
self
);
// Log the rcpt command
info!("{}: to=<{}>", message_id, to_address);
}
// Data
try_smtp!(self.client.command(DataCommand), self);
// Message content
let result = self.client.message(Box::new(email.message()));
if result.is_ok() {
// Increment the connection reuse counter
self.state.connection_reuse_count += 1;
// Log the message
info!(
"{}: conn_use={}, status=sent ({})",
message_id,
self.state.connection_reuse_count,
result
.as_ref()
.ok()
.unwrap()
.message
.iter()
.next()
.unwrap_or(&"no response".to_string())
);
}
// Test if we can reuse the existing connection
match self.client_info.connection_reuse {
ConnectionReuseParameters::ReuseLimited(limit)
if self.state.connection_reuse_count >= limit =>
{
self.close()
}
ConnectionReuseParameters::NoReuse => self.close(),
_ => (),
}
result
}
}

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

559
lettre/src/smtp/response.rs Normal file
View File

@@ -0,0 +1,559 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use nom::{crlf, ErrorKind as NomErrorKind};
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::{from_utf8, FromStr};
/// First digit indicates severity
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Severity {
/// 2yx
PositiveCompletion = 2,
/// 3yz
PositiveIntermediate = 3,
/// 4yz
TransientNegativeCompletion = 4,
/// 5yz
PermanentNegativeCompletion = 5,
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", *self as u8)
}
}
/// Second digit
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Category {
/// x0z
Syntax = 0,
/// x1z
Information = 1,
/// x2z
Connections = 2,
/// x3z
Unspecified3 = 3,
/// x4z
Unspecified4 = 4,
/// x5z
MailSystem = 5,
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", *self as u8)
}
}
/// The detail digit of a response code (third digit)
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum Detail {
#[allow(missing_docs)]
Zero = 0,
#[allow(missing_docs)]
One = 1,
#[allow(missing_docs)]
Two = 2,
#[allow(missing_docs)]
Three = 3,
#[allow(missing_docs)]
Four = 4,
#[allow(missing_docs)]
Five = 5,
#[allow(missing_docs)]
Six = 6,
#[allow(missing_docs)]
Seven = 7,
#[allow(missing_docs)]
Eight = 8,
#[allow(missing_docs)]
Nine = 9,
}
impl Display for Detail {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", *self as u8)
}
}
/// Represents a 3 digit SMTP response code
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Code {
/// First digit of the response code
pub severity: Severity,
/// Second digit of the response code
pub category: Category,
/// Third digit
pub detail: Detail,
}
impl Display for Code {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}{}{}", self.severity, self.category, self.detail)
}
}
impl Code {
/// Creates a new `Code` structure
pub fn new(severity: Severity, category: Category, detail: Detail) -> Code {
Code {
severity,
category,
detail,
}
}
}
/// Contains an SMTP reply, with separated code and message
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct Response {
/// Response code
pub code: Code,
/// Server response string (optional)
/// Handle multiline responses
pub message: Vec<String>,
}
impl FromStr for Response {
type Err = NomErrorKind;
fn from_str(s: &str) -> result::Result<Response, NomErrorKind> {
match parse_response(s.as_bytes()) {
Ok((_, res)) => Ok(res),
Err(e) => Err(e.into_error_kind()),
}
}
}
impl Response {
/// Creates a new `Response`
pub fn new(code: Code, message: Vec<String>) -> Response {
Response { code, message }
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.code.severity {
Severity::PositiveCompletion | Severity::PositiveIntermediate => true,
_ => false,
}
}
/// Tests code equality
pub fn has_code(&self, code: u16) -> bool {
self.code.to_string() == code.to_string()
}
/// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<&str> {
self.message
.get(0)
.and_then(|line| line.split_whitespace().next())
}
/// Returns only the line of the message if possible
pub fn first_line(&self) -> Option<&str> {
self.message.first().map(String::as_str)
}
}
// Parsers (originally from tokio-smtp)
named!(
parse_code<Code>,
map!(
tuple!(parse_severity, parse_category, parse_detail),
|(severity, category, detail)| Code {
severity,
category,
detail,
}
)
);
named!(
parse_severity<Severity>,
alt!(
tag!("2") => { |_| Severity::PositiveCompletion } |
tag!("3") => { |_| Severity::PositiveIntermediate } |
tag!("4") => { |_| Severity::TransientNegativeCompletion } |
tag!("5") => { |_| Severity::PermanentNegativeCompletion }
)
);
named!(
parse_category<Category>,
alt!(
tag!("0") => { |_| Category::Syntax } |
tag!("1") => { |_| Category::Information } |
tag!("2") => { |_| Category::Connections } |
tag!("3") => { |_| Category::Unspecified3 } |
tag!("4") => { |_| Category::Unspecified4 } |
tag!("5") => { |_| Category::MailSystem }
)
);
named!(
parse_detail<Detail>,
alt!(
tag!("0") => { |_| Detail::Zero } |
tag!("1") => { |_| Detail::One } |
tag!("2") => { |_| Detail::Two } |
tag!("3") => { |_| Detail::Three } |
tag!("4") => { |_| Detail::Four} |
tag!("5") => { |_| Detail::Five } |
tag!("6") => { |_| Detail::Six} |
tag!("7") => { |_| Detail::Seven } |
tag!("8") => { |_| Detail::Eight } |
tag!("9") => { |_| Detail::Nine }
)
);
named!(
parse_response<Response>,
map_res!(
tuple!(
// Parse any number of continuation lines.
many0!(tuple!(
parse_code,
preceded!(char!('-'), take_until_and_consume!(b"\r\n".as_ref()))
)),
// Parse the final line.
tuple!(
parse_code,
terminated!(
opt!(preceded!(char!(' '), take_until!(b"\r\n".as_ref()))),
crlf
)
)
),
|(lines, (last_code, last_line)): (Vec<_>, _)| {
// Check that all codes are equal.
if !lines.iter().all(|&(ref code, _)| *code == last_code) {
return Err(());
}
// Extract text from lines, and append last line.
let mut lines = lines.into_iter().map(|(_, text)| text).collect::<Vec<_>>();
if let Some(text) = last_line {
lines.push(text);
}
Ok(Response {
code: last_code,
message: lines
.into_iter()
.map(|line| from_utf8(line).map(|s| s.to_string()))
.collect::<result::Result<Vec<_>, _>>()
.map_err(|_| ())?,
})
}
)
);
#[cfg(test)]
mod test {
use super::{Category, Code, Detail, Response, Severity};
#[test]
fn test_severity_fmt() {
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
}
#[test]
fn test_category_fmt() {
assert_eq!(format!("{}", Category::Unspecified4), "4");
}
#[test]
fn test_code_new() {
assert_eq!(
Code::new(
Severity::TransientNegativeCompletion,
Category::Connections,
Detail::Zero,
),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: Detail::Zero,
}
);
}
#[test]
fn test_code_display() {
let code = Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: Detail::One,
};
assert_eq!(code.to_string(), "421");
}
#[test]
fn test_response_from_str() {
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
assert_eq!(
raw_response.parse::<Response>().unwrap(),
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
message: vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"AUTH PLAIN CRAM-MD5".to_string(),
],
}
);
let wrong_code = "2506-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
assert!(wrong_code.parse::<Response>().is_err());
let wrong_end = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250-AUTH PLAIN CRAM-MD5\r\n";
assert!(wrong_end.parse::<Response>().is_err());
}
#[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::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::Zero,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.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(251));
}
#[test]
fn test_response_first_word() {
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_word(),
Some("me")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_word(),
Some("me")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![],
)
.first_word(),
None
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_word(),
None
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_word(),
None
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec!["".to_string()],
)
.first_word(),
None
);
}
#[test]
fn test_response_first_line() {
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_line(),
Some("me")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![
"me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
)
.first_line(),
Some("me mo")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![],
)
.first_line(),
None
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_line(),
Some(" ")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec![" ".to_string()],
)
.first_line(),
Some(" ")
);
assert_eq!(
Response::new(
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::MailSystem,
detail: Detail::One,
},
vec!["".to_string()],
)
.first_line(),
Some("")
);
}
}

46
lettre/src/smtp/util.rs Normal file
View File

@@ -0,0 +1,46 @@
//! Utils for string manipulation
use std::fmt::{Display, Formatter, Result as FmtResult};
/// Encode a string as xtext
#[derive(Debug)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub struct XText<'a>(pub &'a str);
impl<'a> Display for XText<'a> {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
let mut rest = self.0;
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
let (start, end) = rest.split_at(idx);
f.write_str(start)?;
let mut end_iter = end.char_indices();
let (_, c) = end_iter.next().expect("char");
write!(f, "+{:X}", c as u8)?;
if let Some((idx, _)) = end_iter.next() {
rest = &end[idx..];
} else {
rest = "";
}
}
f.write_str(rest)
}
}
#[cfg(test)]
mod tests {
use super::XText;
#[test]
fn test() {
for (input, expect) in vec![
("bjorn", "bjorn"),
("bjørn", "bjørn"),
("Ø+= ❤️‰", "Ø+2B+3D+20❤"),
("+", "+2B"),
] {
assert_eq!(format!("{}", XText(input)), expect);
}
}
}

44
lettre/src/stub/mod.rs Normal file
View File

@@ -0,0 +1,44 @@
//! The stub transport only logs message envelope and drops the content. It can be useful for
//! testing purposes.
//!
use SendableEmail;
use Transport;
/// This transport logs the message envelope and returns the given response
#[derive(Debug, Clone, Copy)]
pub struct StubTransport {
response: StubResult,
}
impl StubTransport {
/// Creates a new transport that always returns the given response
pub fn new(response: StubResult) -> StubTransport {
StubTransport { response }
}
/// Creates a new transport that always returns a success response
pub fn new_positive() -> StubTransport {
StubTransport { response: Ok(()) }
}
}
/// SMTP result type
pub type StubResult = Result<(), ()>;
impl<'a> Transport<'a> for StubTransport {
type Result = StubResult;
fn send(&mut self, email: SendableEmail) -> StubResult {
info!(
"{}: from=<{}> to=<{:?}>",
email.message_id(),
match email.envelope().from() {
Some(address) => address.to_string(),
None => "".to_string(),
},
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");
}
}

65
lettre/tests/skeptic.rs Normal file
View File

@@ -0,0 +1,65 @@
extern crate glob;
use self::glob::glob;
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!())
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("README.md");
skeptic_test(&readme);
}
*/
#[test]
fn book_test() {
let mut book_path = env::current_dir().unwrap();
book_path.push(
Path::new(file!())
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("../website/content/sending-messages"),
); // For some reasons, calling .parent() once more gives us None...
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
skeptic_test(&md.unwrap());
}
}
fn skeptic_test(path: &Path) {
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
let exe = env::current_exe().unwrap();
let depdir = exe.parent().unwrap();
let mut cmd = Command::new(rustdoc);
cmd.args(&["--verbose", "--test"])
.arg("-L")
.arg(&depdir)
.arg(path);
let result = cmd
.spawn()
.expect("Failed to spawn process")
.wait()
.expect("Failed to run process");
assert!(
result.success(),
format!("Failed to run rustdoc tests on {:?}", path)
);
}

View File

@@ -0,0 +1,44 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "file-transport")]
mod test {
use lettre::file::FileTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
use std::env::temp_dir;
use std::fs::remove_file;
use std::fs::File;
use std::io::Read;
#[test]
fn file_transport() {
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 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\":\"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

@@ -0,0 +1,27 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "sendmail-transport")]
mod test {
use lettre::sendmail::SendmailTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
#[test]
fn sendmail_transport_simple() {
let mut sender = SendmailTransport::new();
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);
println!("{:?}", result);
assert!(result.is_ok());
}
}

View File

@@ -0,0 +1,27 @@
extern crate lettre;
#[cfg(test)]
#[cfg(feature = "smtp-transport")]
mod test {
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
#[test]
fn smtp_transport_simple() {
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(),
);
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
.unwrap()
.transport()
.send(email)
.unwrap();
}
}

View File

@@ -0,0 +1,31 @@
extern crate lettre;
use lettre::stub::StubTransport;
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
#[test]
fn stub_transport() {
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_ok).unwrap();
sender_ko.send(email_ko).unwrap_err();
}

1
lettre_email/CHANGELOG.md Symbolic link
View File

@@ -0,0 +1 @@
../CHANGELOG.md

31
lettre_email/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "lettre_email"
version = "0.9.4" # remember to update html_root_url
description = "Email builder"
readme = "README.md"
homepage = "http://lettre.at"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>"]
categories = ["email"]
keywords = ["email", "mailer"]
[badges]
travis-ci = { repository = "lettre/lettre_email" }
appveyor = { repository = "lettre/lettre_email" }
maintenance = { status = "actively-developed" }
is-it-maintained-issue-resolution = { repository = "lettre/lettre_email" }
is-it-maintained-open-issues = { repository = "lettre/lettre_email" }
[dev-dependencies]
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
glob = "0.3"
[dependencies]
email = "^0.0.20"
mime = "^0.3"
time = "^0.1"
uuid = { version = "^0.7", features = ["v4"] }
lettre = { version = "^0.9", path = "../lettre", default-features = false }
base64 = "^0.10"

1
lettre_email/LICENSE Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE

1
lettre_email/README.md Symbolic link
View File

@@ -0,0 +1 @@
../README.md

View File

@@ -0,0 +1,34 @@
extern crate lettre;
extern crate lettre_email;
extern crate mime;
use lettre::{SmtpClient, Transport};
use lettre_email::Email;
use std::path::Path;
fn main() {
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_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
.unwrap()
.build()
.unwrap();
// Open a local connection on port 25
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
// Send the email
let result = mailer.send(email.into());
if result.is_ok() {
println!("Email sent");
} else {
println!("Could not send email: {:?}", result);
}
assert!(result.is_ok());
}

53
lettre_email/src/error.rs Normal file
View File

@@ -0,0 +1,53 @@
//! Error and result type for emails
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
Envelope(lettre::error::Error),
/// Unparseable filename for attachment
CannotParseFilename,
/// IO error
Io(io::Error),
}
impl Display for Error {
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 cause(&self) -> Option<&dyn StdError> {
match *self {
Envelope(ref err) => Some(err),
Io(ref err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error::Io(err)
}
}
impl From<lettre::error::Error> for Error {
fn from(err: lettre::error::Error) -> Error {
Error::Envelope(err)
}
}

588
lettre_email/src/lib.rs Normal file
View File

@@ -0,0 +1,588 @@
//! Lettre is a mailer written in Rust. lettre_email provides a simple email builder.
//!
#![doc(html_root_url = "https://docs.rs/lettre_email/0.9.4")]
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unstable_features,
unused_import_braces
)]
extern crate base64;
extern crate email as email_format;
extern crate lettre;
pub extern crate mime;
extern crate time;
extern crate uuid;
pub mod error;
pub use email_format::{Address, Header, Mailbox, MimeMessage, MimeMultipartType};
use error::Error;
use lettre::{error::Error as LettreError, EmailAddress, Envelope, SendableEmail};
use mime::Mime;
use std::fs;
use std::path::Path;
use std::str::FromStr;
use time::{now, Tm};
use uuid::Uuid;
/// Builds a `MimeMessage` structure
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct PartBuilder {
/// Message
message: MimeMessage,
}
impl Default for PartBuilder {
fn default() -> Self {
Self::new()
}
}
/// Represents a message id
pub type MessageId = String;
/// Builds an `Email` structure
#[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct EmailBuilder {
/// Message
message: PartBuilder,
/// The recipients' addresses for the mail header
to: Vec<Address>,
/// The sender addresses for the mail header
from: Vec<Address>,
/// The Cc addresses for the mail header
cc: Vec<Address>,
/// The Bcc addresses for the mail header
bcc: Vec<Address>,
/// The Reply-To addresses for the mail header
reply_to: Vec<Address>,
/// The In-Reply-To ids for the mail header
in_reply_to: Vec<MessageId>,
/// The References ids for the mail header
references: Vec<MessageId>,
/// The sender address for the mail header
sender: Option<Mailbox>,
/// The envelope
envelope: Option<Envelope>,
/// Date issued
date_issued: bool,
}
/// Simple email representation
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Email {
/// Message
message: Vec<u8>,
/// Envelope
envelope: Envelope,
/// Message-ID
message_id: Uuid,
}
impl Into<SendableEmail> for Email {
fn into(self) -> SendableEmail {
SendableEmail::new(
self.envelope.clone(),
self.message_id.to_string(),
self.message,
)
}
}
impl Email {
/// TODO
pub fn builder() -> EmailBuilder {
EmailBuilder::new()
}
}
impl PartBuilder {
/// Creates a new empty part
pub fn new() -> PartBuilder {
PartBuilder {
message: MimeMessage::new_blank_message(),
}
}
/// Adds a generic header
pub fn header<A: Into<Header>>(mut self, header: A) -> PartBuilder {
self.message.headers.insert(header.into());
self
}
/// Sets the body
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
self.message.body = body.into();
self
}
/// Defines a `MimeMultipartType` value
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
self.message.message_type = Some(mime_type);
self
}
/// Adds a `ContentType` header with the given MIME type
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
self.header(("Content-Type", content_type.to_string()))
}
/// Adds a child part
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
self.message.children.push(child);
self
}
/// Gets built `MimeMessage`
pub fn build(mut self) -> MimeMessage {
self.message.update_headers();
self.message
}
}
impl EmailBuilder {
/// Creates a new empty email
pub fn new() -> EmailBuilder {
EmailBuilder {
message: PartBuilder::new(),
to: vec![],
from: vec![],
cc: vec![],
bcc: vec![],
reply_to: vec![],
in_reply_to: vec![],
references: vec![],
sender: None,
envelope: None,
date_issued: false,
}
}
/// Sets the email body
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
self.message = self.message.body(body);
self
}
/// Add a generic header
pub fn header<A: Into<Header>>(mut self, header: A) -> EmailBuilder {
self.message = self.message.header(header);
self
}
/// Adds a `From` header and stores the sender address
pub fn from<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.from.push(Address::Mailbox(mailbox));
self
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.to.push(Address::Mailbox(mailbox));
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.cc.push(Address::Mailbox(mailbox));
self
}
/// Adds a `Bcc` header and stores the recipient address
pub fn bcc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.bcc.push(Address::Mailbox(mailbox));
self
}
/// Adds a `Reply-To` header
pub fn reply_to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.reply_to.push(Address::Mailbox(mailbox));
self
}
/// Adds a `In-Reply-To` header
pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder {
self.in_reply_to.push(message_id);
self
}
/// Adds a `References` header
pub fn references(mut self, message_id: MessageId) -> EmailBuilder {
self.references.push(message_id);
self
}
/// Adds a `Sender` header
pub fn sender<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
let mailbox = address.into();
self.sender = Some(mailbox);
self
}
/// Adds a `Subject` header
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
self.message = self.message.header(("Subject".to_string(), subject.into()));
self
}
/// Adds a `Date` header with the given date
pub fn date(mut self, date: &Tm) -> EmailBuilder {
self.message = self.message.header(("Date", Tm::rfc822z(date).to_string()));
self.date_issued = true;
self
}
/// Adds an attachment to the email from a file
///
/// If not specified, the filename will be extracted from the file path.
pub fn attachment_from_file(
self,
path: &Path,
filename: Option<&str>,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
self.attachment(
fs::read(path)?.as_slice(),
filename.unwrap_or(
path.file_name()
.and_then(|x| x.to_str())
.ok_or(Error::CannotParseFilename)?,
),
content_type,
)
}
/// Adds an attachment to the email from a vector of bytes.
pub fn attachment(
self,
body: &[u8],
filename: &str,
content_type: &Mime,
) -> Result<EmailBuilder, Error> {
let encoded_body = base64::encode(&body)
.as_bytes()
.chunks(72)
// base64 encoding is guaranteed to return utf-8, so this won't panic
.map(|s| std::str::from_utf8(s).unwrap())
.collect::<Vec<_>>()
.join("\r\n");
let content = PartBuilder::new()
.body(encoded_body)
.header((
"Content-Disposition",
format!("attachment; filename=\"{}\"", filename),
))
.header(("Content-Type", content_type.to_string()))
.header(("Content-Transfer-Encoding", "base64"))
.build();
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
}
/// Set the message type
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
self.message = self.message.message_type(message_type);
self
}
/// Adds a child
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
self.message = self.message.child(child);
self
}
/// Sets the email body to plain text content
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
let text = PartBuilder::new()
.body(body)
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
.build();
self.child(text)
}
/// Sets the email body to HTML content
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
let html = PartBuilder::new()
.body(body)
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
.build();
self.child(html)
}
/// Sets the email content
pub fn alternative<S: Into<String>, T: Into<String>>(
self,
body_html: S,
body_text: T,
) -> EmailBuilder {
let text = PartBuilder::new()
.body(body_text)
.header(("Content-Type", mime::TEXT_PLAIN_UTF_8.to_string()))
.build();
let html = PartBuilder::new()
.body(body_html)
.header(("Content-Type", mime::TEXT_HTML_UTF_8.to_string()))
.build();
let alternate = PartBuilder::new()
.message_type(MimeMultipartType::Alternative)
.child(text)
.child(html);
self.message_type(MimeMultipartType::Mixed)
.child(alternate.build())
}
/// Sets the envelope for manual destination control
/// If this function is not called, the envelope will be calculated
/// from the "to" and "cc" addresses you set.
pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder {
self.envelope = Some(envelope);
self
}
/// Builds the Email
pub fn build(mut self) -> Result<Email, Error> {
// If there are multiple addresses in "From", the "Sender" is required.
if self.from.len() >= 2 && self.sender.is_none() {
// So, we must find something to put as Sender.
for possible_sender in &self.from {
// Only a mailbox can be used as sender, not Address::Group.
if let Address::Mailbox(ref mbx) = *possible_sender {
self.sender = Some(mbx.clone());
break;
}
}
// Address::Group is not yet supported, so the line below will never panic.
// If groups are supported one day, add another Error for this case
// and return it here, if sender_header is still None at this point.
assert!(self.sender.is_some());
}
// Add the sender header, if any.
if let Some(ref v) = self.sender {
self.message = self.message.header(("Sender", v.to_string()));
}
// Calculate the envelope
let envelope = match self.envelope {
Some(e) => e,
None => {
// we need to generate the envelope
let mut to = vec![];
// add all receivers in to_header and cc_header
for receiver in self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter()) {
match *receiver {
Address::Mailbox(ref m) => to.push(EmailAddress::from_str(&m.address)?),
Address::Group(_, ref ms) => {
for m in ms.iter() {
to.push(EmailAddress::from_str(&m.address.clone())?);
}
}
}
}
let from = Some(EmailAddress::from_str(&match self.sender {
Some(x) => Ok(x.address), // if we have a sender_header, use it
None => {
// use a from header
debug_assert!(self.from.len() <= 1); // else we'd have sender_header
match self.from.first() {
Some(a) => match *a {
// if we have a from header
Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()), // use it
Address::Group(_, ref mailbox_list) => match mailbox_list.first() {
// if it's an author group, use the first author
Some(mailbox) => Ok(mailbox.address.clone()),
// for an empty author group (the rarest of the rare cases)
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
},
},
// if we don't have a from header
None => Err(Error::Envelope(LettreError::MissingFrom)), // empty envelope sender
}
}
}?)?);
Envelope::new(from, to)?
}
};
// Add the collected addresses as mailbox-list all at once.
// The unwraps are fine because the conversions for Vec<Address> never errs.
if !self.to.is_empty() {
self.message = self
.message
.header(Header::new_with_value("To".into(), self.to).unwrap());
}
if !self.from.is_empty() {
self.message = self
.message
.header(Header::new_with_value("From".into(), self.from).unwrap());
} else if let Some(from) = envelope.from() {
let from = vec![Address::new_mailbox(from.to_string())];
self.message = self
.message
.header(Header::new_with_value("From".into(), from).unwrap());
} else {
return Err(Error::Envelope(LettreError::MissingFrom));
}
if !self.cc.is_empty() {
self.message = self
.message
.header(Header::new_with_value("Cc".into(), self.cc).unwrap());
}
if !self.reply_to.is_empty() {
self.message = self
.message
.header(Header::new_with_value("Reply-To".into(), self.reply_to).unwrap());
}
if !self.in_reply_to.is_empty() {
self.message = self.message.header(
Header::new_with_value("In-Reply-To".into(), self.in_reply_to.join(" ")).unwrap(),
);
}
if !self.references.is_empty() {
self.message = self.message.header(
Header::new_with_value("References".into(), self.references.join(" ")).unwrap(),
);
}
if !self.date_issued {
self.message = self
.message
.header(("Date", Tm::rfc822z(&now()).to_string()));
}
self.message = self.message.header(("MIME-Version", "1.0"));
let message_id = Uuid::new_v4();
if let Ok(header) = Header::new_with_value(
"Message-ID".to_string(),
format!("<{}.lettre@localhost>", message_id),
) {
self.message = self.message.header(header)
}
Ok(Email {
message: self.message.build().as_string().into_bytes(),
envelope,
message_id,
})
}
}
#[cfg(test)]
mod test {
use super::{EmailBuilder, SendableEmail};
use lettre::EmailAddress;
use time::now;
#[test]
fn test_multiple_from() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = email_builder
.to("anna@example.com")
.from("dieter@example.com")
.from("joachim@example.com")
.date(&date_now)
.subject("Invitation")
.body("We invite you!")
.build()
.unwrap()
.into();
let id = email.message_id().to_string();
assert_eq!(
email.message_to_string().unwrap(),
format!(
"Date: {}\r\nSubject: Invitation\r\nSender: \
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
date_now.rfc822z(),
id
)
);
}
#[test]
fn test_email_builder() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = email_builder
.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.bcc("bcc@localhost")
.reply_to("reply@localhost")
.in_reply_to("original".to_string())
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap()
.into();
let id = email.message_id().to_string();
assert_eq!(
email.message_to_string().unwrap(),
format!(
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\n\
Reply-To: <reply@localhost>\r\nIn-Reply-To: original\r\n\
MIME-Version: 1.0\r\nMessage-ID: \
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
date_now.rfc822z(),
id
)
);
}
#[test]
fn test_email_sendable() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email: SendableEmail = email_builder
.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.bcc("bcc@localhost")
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap()
.into();
assert_eq!(
email.envelope().from().unwrap().to_string(),
"sender@localhost".to_string()
);
assert_eq!(
email.envelope().to(),
vec![
EmailAddress::new("user@localhost".to_string()).unwrap(),
EmailAddress::new("cc@localhost".to_string()).unwrap(),
EmailAddress::new("bcc@localhost".to_string()).unwrap(),
]
.as_slice()
);
}
}

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

@@ -0,0 +1,49 @@
extern crate glob;
use self::glob::glob;
use std::env;
use std::env::consts::EXE_EXTENSION;
use std::path::Path;
use std::process::Command;
#[test]
fn book_test() {
let mut book_path = env::current_dir().unwrap();
book_path.push(
Path::new(file!())
.parent()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("../website/content/creating-messages"),
); // For some reasons, calling .parent() once more gives us None...
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
skeptic_test(&md.unwrap());
}
}
fn skeptic_test(path: &Path) {
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
let exe = env::current_exe().unwrap();
let depdir = exe.parent().unwrap();
let mut cmd = Command::new(rustdoc);
cmd.args(&["--verbose", "--test"])
.arg("-L")
.arg(&depdir)
.arg(path);
let result = cmd
.spawn()
.expect("Failed to spawn process")
.wait()
.expect("Failed to run process");
assert!(
result.success(),
format!("Failed to run rustdoc tests on {:?}!", path)
);
}

View File

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

View File

@@ -1,254 +0,0 @@
//! SMTP client
use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use std::io::{BufRead, Read, Write};
use bufstream::BufStream;
use response::ResponseParser;
use authentication::Mecanism;
use error::{Error, SmtpResult};
use client::net::{Connector, SmtpStream};
use {CRLF, MESSAGE_ENDING};
pub mod net;
/// Returns the string after adding a dot at the beginning of each line starting with a dot
///
/// Reference : https://tools.ietf.org/html/rfc5321#page-62 (4.5.2. Transparency)
#[inline]
fn escape_dot(string: &str) -> String {
if string.starts_with(".") {
format!(".{}", string)
} else {
string.to_string()
}.replace("\r.", "\r..")
.replace("\n.", "\n..")
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
#[inline]
fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CR><LF>")
}
/// Returns the string removing all the CRLF
#[inline]
fn remove_crlf(string: &str) -> String {
string.replace(CRLF, "")
}
/// Structure that implements the SMTP client
pub struct Client<S: Write + Read = SmtpStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<BufStream<S>>,
/// Socket we are connecting to
server_addr: SocketAddr,
}
macro_rules! return_err (
($err: expr, $client: ident) => ({
return Err(From::from($err))
})
);
impl<S: Write + Read = SmtpStream> Client<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<Client<S>, Error> {
let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() {
Some(addr) => Ok(Client {
stream: None,
server_addr: addr,
}),
None => Err(From::from("Could nor resolve hostname")),
}
}
}
impl<S: Connector + Write + Read = SmtpStream> Client<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.quit();
self.stream = None;
}
/// Connects to the configured server
pub fn connect(&mut self) -> SmtpResult {
// Connect should not be called when the client is already connected
if self.stream.is_some() {
return_err!("The connection is already established", self);
}
// Try to connect
self.stream = Some(BufStream::new(try!(Connector::connect(&self.server_addr))));
self.get_reply()
}
/// Checks if the server is connected using the NOOP SMTP command
pub fn is_connected(&mut self) -> bool {
self.noop().is_ok()
}
/// Sends an SMTP command
pub fn command(&mut self, command: &str) -> SmtpResult {
self.send_server(command, CRLF)
}
/// Send a HELO command and fills `server_info`
pub fn helo(&mut self, hostname: &str) -> SmtpResult {
self.command(&format!("HELO {}", hostname))
}
/// Sends a EHLO command and fills `server_info`
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
self.command(&format!("EHLO {}", hostname))
}
/// Sends a MAIL command
pub fn mail(&mut self, address: &str, options: Option<&str>) -> SmtpResult {
match options {
Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)),
None => self.command(&format!("MAIL FROM:<{}>", address)),
}
}
/// Sends a RCPT command
pub fn rcpt(&mut self, address: &str) -> SmtpResult {
self.command(&format!("RCPT TO:<{}>", address))
}
/// Sends a DATA command
pub fn data(&mut self) -> SmtpResult {
self.command("DATA")
}
/// Sends a QUIT command
pub fn quit(&mut self) -> SmtpResult {
self.command("QUIT")
}
/// Sends a NOOP command
pub fn noop(&mut self) -> SmtpResult {
self.command("NOOP")
}
/// Sends a HELP command
pub fn help(&mut self, argument: Option<&str>) -> SmtpResult {
match argument {
Some(ref argument) => self.command(&format!("HELP {}", argument)),
None => self.command("HELP"),
}
}
/// Sends a VRFY command
pub fn vrfy(&mut self, address: &str) -> SmtpResult {
self.command(&format!("VRFY {}", address))
}
/// Sends a EXPN command
pub fn expn(&mut self, address: &str) -> SmtpResult {
self.command(&format!("EXPN {}", address))
}
/// Sends a RSET command
pub fn rset(&mut self) -> SmtpResult {
self.command("RSET")
}
/// Sends an AUTH command with the given mecanism
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
if mecanism.supports_initial_response() {
self.command(&format!("AUTH {} {}", mecanism, try!(mecanism.response(username, password, None))))
} else {
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
Some(challenge) => challenge,
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
};
let cram_response = try!(mecanism.response(username, password, Some(&encoded_challenge)));
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
}
}
/// Sends the message content
pub fn message(&mut self, message_content: &str) -> SmtpResult {
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
}
/// Sends a string to the server and gets the response
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
if self.stream.is_none() {
return Err(From::from("Connection closed"));
}
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
try!(self.stream.as_mut().unwrap().flush());
debug!("Wrote: {}", escape_crlf(string));
self.get_reply()
}
/// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult {
let mut parser = ResponseParser::new();
let mut line = String::new();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
line.clear();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
}
let response = try!(parser.response());
match response.is_positive() {
true => Ok(response),
false => Err(From::from(response)),
}
}
}
#[cfg(test)]
mod test {
use super::{escape_dot, remove_crlf, escape_crlf};
#[test]
fn test_escape_dot() {
assert_eq!(escape_dot(".test"), "..test");
assert_eq!(escape_dot("\r.\n.\r\n"), "\r..\n..\r\n");
assert_eq!(escape_dot("test\r\n.test\r\n"), "test\r\n..test\r\n");
assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest");
}
#[test]
fn test_remove_crlf() {
assert_eq!(remove_crlf("\r\n"), "");
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
assert_eq!(
remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_nameSIZE 42"
);
}
#[test]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
assert_eq!(
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
);
}
}

View File

@@ -1,23 +0,0 @@
//! A trait to represent a stream
use std::io;
use std::net::SocketAddr;
use std::net::TcpStream;
/// A trait for the concept of opening a stream
pub trait Connector {
/// Opens a connection to the given IP socket
fn connect(addr: &SocketAddr) -> io::Result<Self>;
}
impl Connector for SmtpStream {
fn connect(addr: &SocketAddr) -> io::Result<SmtpStream> {
TcpStream::connect(addr)
}
}
/// Represents an atual SMTP network stream
//Used later for ssl
pub type SmtpStream = TcpStream;

View File

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

View File

@@ -1,90 +0,0 @@
//! Error and result type for SMTP clients
use std::error::Error as StdError;
use std::io;
use std::fmt::{Display, Formatter};
use std::fmt;
use response::{Severity, Response};
use serialize::base64::FromBase64Error;
use self::Error::*;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Transient SMTP error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
TransientError(Response),
/// Permanent SMTP error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
PermanentError(Response),
/// Error parsing a response
ResponseParsingError(&'static str),
/// Error parsing a base64 string in response
ChallengeParsingError(FromBase64Error),
/// Internal client error
ClientError(&'static str),
/// DNS resolution error
ResolutionError,
/// IO error
IoError(io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
TransientError(_) => "a transient error occured during the SMTP transaction",
PermanentError(_) => "a permanent error occured during the SMTP transaction",
ResponseParsingError(_) => "an error occured while parsing an SMTP response",
ChallengeParsingError(_) => "an error occured while parsing a CRAM-MD5 challenge",
ResolutionError => "Could no resolve hostname",
ClientError(_) => "an unknown error occured",
IoError(_) => "an I/O error occured",
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
IoError(ref err) => Some(&*err as &StdError),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
IoError(err)
}
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.severity() {
Severity::TransientNegativeCompletion => TransientError(response),
Severity::PermanentNegativeCompletion => PermanentError(response),
_ => ClientError("Unknown error code")
}
}
}
impl From<&'static str> for Error {
fn from(string: &'static str) -> Error {
ClientError(string)
}
}
/// SMTP result type
pub type SmtpResult = Result<Response, Error>;
#[cfg(test)]
mod test {
// TODO
}

View File

@@ -1,197 +0,0 @@
//! ESMTP features
use std::result::Result;
use std::fmt::{Display, Formatter};
use std::fmt;
use std::collections::HashSet;
use response::Response;
use error::Error;
use authentication::Mecanism;
/// Supported ESMTP keywords
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
pub enum Extension {
/// 8BITMIME keyword
///
/// RFC 6152: https://tools.ietf.org/html/rfc6152
EightBitMime,
/// SMTPUTF8 keyword
///
/// RFC 6531: https://tools.ietf.org/html/rfc6531
SmtpUtfEight,
/// STARTTLS keyword
///
/// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls,
/// AUTH mecanism
Authentication(Mecanism),
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
Extension::StartTls => write!(f, "{}", "STARTTLS"),
Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism),
}
}
}
/// Contains information about an SMTP server
#[derive(Clone,Debug,Eq,PartialEq)]
pub struct ServerInfo {
/// Server name
///
/// The name given in the server banner
pub name: String,
/// ESMTP features supported by the server
///
/// It contains the features supported by the server and known by the `Extension` module.
pub features: HashSet<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{} with {}",
self.name,
match self.features.is_empty() {
true => "no supported features".to_string(),
false => format! ("{:?}", self.features),
}
)
}
}
impl ServerInfo {
/// Parses a response to create a `ServerInfo`
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(Error::ResponseParsingError("Could not read server name"))
};
let mut features: HashSet<Extension> = HashSet::new();
for line in response.message() {
let splitted : Vec<&str> = line.split_whitespace().collect();
let _ = match splitted[0] {
"8BITMIME" => {features.insert(Extension::EightBitMime);},
"SMTPUTF8" => {features.insert(Extension::SmtpUtfEight);},
"STARTTLS" => {features.insert(Extension::StartTls);},
"AUTH" => {
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => {features.insert(Extension::Authentication(Mecanism::Plain));},
"CRAM-MD5" => {features.insert(Extension::Authentication(Mecanism::CramMd5));},
_ => (),
}
}
},
_ => (),
};
}
Ok(ServerInfo{
name: name,
features: features,
})
}
/// Checks if the server supports an ESMTP feature
pub fn supports_feature(&self, keyword: &Extension) -> bool {
self.features.contains(keyword)
}
/// Checks if the server supports an ESMTP feature
pub fn supports_auth_mecanism(&self, mecanism: Mecanism) -> bool {
self.features.contains(&Extension::Authentication(mecanism))
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::{ServerInfo, Extension};
use authentication::Mecanism;
use response::{Code, Response, Severity, Category};
#[test]
fn test_extension_fmt() {
assert_eq!(format!("{}", Extension::EightBitMime), "8BITMIME".to_string());
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)), "AUTH PLAIN".to_string());
}
#[test]
fn test_serverinfo_fmt() {
let mut eightbitmime = HashSet::new();
assert!(eightbitmime.insert(Extension::EightBitMime));
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
features: eightbitmime.clone()
}), "name with {EightBitMime}".to_string());
let empty = HashSet::new();
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
features: empty,
}), "name with no supported features".to_string());
let mut plain = HashSet::new();
assert!(plain.insert(Extension::Authentication(Mecanism::Plain)));
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
features: plain.clone()
}), "name with {Authentication(Plain)}".to_string());
}
#[test]
fn test_serverinfo() {
let response = Response::new(
Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
);
let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo {
name: "me".to_string(),
features: features,
};
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
assert!(server_info.supports_feature(&Extension::EightBitMime));
assert!(!server_info.supports_feature(&Extension::StartTls));
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
let response2 = Response::new(
Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
vec!["me".to_string(), "AUTH PLAIN CRAM-MD5 OTHER".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
);
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
let server_info2 = ServerInfo {
name: "me".to_string(),
features: features2,
};
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
assert!(server_info2.supports_feature(&Extension::EightBitMime));
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain));
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5));
assert!(!server_info2.supports_feature(&Extension::StartTls));
}
}

View File

@@ -1,178 +0,0 @@
//! # Rust SMTP client
//!
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
//! a work in progress. It is designed to efficiently send emails from an application to a
//! relay email server, as it relies as much as possible on the relay server for sanity and RFC compliance
//! checks.
//!
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms
//!
//! It will eventually implement the following extensions:
//!
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
//! ## Architecture
//!
//! This client is divided into three main parts:
//!
//! * client: a low level SMTP client providing all SMTP commands
//! * sender: a high level SMTP client providing an easy method to send emails
//! * email: generates the email to be sent with the sender
//!
//! ## Usage
//!
//! ### Simple example
//!
//! This is the most basic example of usage:
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::EmailBuilder;
//!
//! // Create an email
//! let email = EmailBuilder::new()
//! // Addresses can be specified by the couple (email, alias)
//! .to(("user@example.org", "Firstname Lastname"))
//! // ... or by an address only
//! .from("user@example.com")
//! .subject("Hi, Hello world")
//! .body("Hello world.")
//! .build();
//!
//! // Open a local connection on port 25
//! let mut sender = SenderBuilder::localhost().unwrap().build();
//! // Send the email
//! let result = sender.send(email);
//!
//! assert!(result.is_ok());
//! ```
//!
//! ### Complete example
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::EmailBuilder;
//!
//! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name"));
//! builder = builder.cc(("user@example.net", "Alias name"));
//! builder = builder.from("no-reply@example.com");
//! builder = builder.from("no-reply@example.eu");
//! builder = builder.sender("no-reply@example.com");
//! builder = builder.subject("Hello world");
//! builder = builder.body("Hi, Hello world.");
//! builder = builder.reply_to("contact@example.com");
//! builder = builder.add_header(("X-Custom-Header", "my header"));
//!
//! let email = builder.build();
//!
//! // Connect to a remote server on a custom port
//! let mut sender = SenderBuilder::new(("server.tld", 10025)).unwrap()
//! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name("my.hostname.tld")
//! // Add credentials for authentication
//! .credentials("username", "password")
//! // Enable connection reuse
//! .enable_connection_reuse(true).build();
//!
//! let result_1 = sender.send(email.clone());
//! assert!(result_1.is_ok());
//!
//! // The second email will use the same connection
//! let result_2 = sender.send(email);
//! assert!(result_2.is_ok());
//!
//! // Explicitely close the SMTP transaction as we enabled connection reuse
//! sender.close();
//! ```
//!
//! ### Using the client directly
//!
//! If you just want to send an email without using `Email` to provide headers:
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::email::SimpleSendableEmail;
//!
//! // Create a minimal email
//! let email = SimpleSendableEmail::new(
//! "test@example.com",
//! "test@example.org",
//! "Hello world !"
//! );
//!
//! let mut sender = SenderBuilder::localhost().unwrap().build();
//! let result = sender.send(email);
//! assert!(result.is_ok());
//! ```
//!
//! ### Lower level
//!
//! You can also send commands, here is a simple email transaction without error handling:
//!
//! ```rust,no_run
//! use smtp::client::Client;
//! use smtp::client::net::SmtpStream;
//! use smtp::SMTP_PORT;
//! use std::net::TcpStream;
//!
//! let mut email_client: Client<SmtpStream> = Client::new(("localhost", SMTP_PORT)).unwrap();
//! let _ = email_client.connect();
//! let _ = email_client.ehlo("my_hostname");
//! let _ = email_client.mail("user@example.com", None);
//! let _ = email_client.rcpt("user@example.org");
//! let _ = email_client.data();
//! let _ = email_client.message("Test email");
//! let _ = email_client.quit();
//! ```
#![deny(missing_docs)]
#[macro_use] extern crate log;
extern crate rustc_serialize as serialize;
extern crate crypto;
extern crate time;
extern crate uuid;
extern crate email as email_format;
extern crate bufstream;
mod extension;
pub mod client;
pub mod sender;
pub mod response;
pub mod error;
pub mod authentication;
pub mod email;
// Registrated port numbers:
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port
pub static SMTP_PORT: u16 = 25;
/// Default smtps port
pub static SMTPS_PORT: u16 = 465;
/// Default submission port
pub static SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub static SP: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub static CRLF: &'static str = "\r\n";
/// Colon
pub static COLON: &'static str = ":";
/// The ending of message content
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub static NUL: &'static str = "\0";

View File

@@ -1,558 +0,0 @@
//! SMTP response, containing a mandatory return code and an optional text message
use std::str::FromStr;
use std::fmt::{Display, Formatter, Result};
use std::result::Result as RResult;
use self::Severity::*;
use self::Category::*;
use error::{SmtpResult, Error};
/// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Severity {
/// 2yx
PositiveCompletion,
/// 3yz
PositiveIntermediate,
/// 4yz
TransientNegativeCompletion,
/// 5yz
PermanentNegativeCompletion,
}
impl FromStr for Severity {
type Err = Error;
fn from_str(s: &str) -> RResult<Severity, Error> {
match s {
"2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
}
}
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
match *self {
PositiveCompletion => 2,
PositiveIntermediate => 3,
TransientNegativeCompletion => 4,
PermanentNegativeCompletion => 5,
}
)
}
}
/// Second digit
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Category {
/// x0z
Syntax,
/// x1z
Information,
/// x2z
Connections,
/// x3z
Unspecified3,
/// x4z
Unspecified4,
/// x5z
MailSystem,
}
impl FromStr for Category {
type Err = Error;
fn from_str(s: &str) -> RResult<Category, Error> {
match s {
"0" => Ok(Syntax),
"1" => Ok(Information),
"2" => Ok(Connections),
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"5" => Ok(MailSystem),
_ => Err(Error::ResponseParsingError("Second digit must be between 0 and 5")),
}
}
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
match *self {
Syntax => 0,
Information => 1,
Connections => 2,
Unspecified3 => 3,
Unspecified4 => 4,
MailSystem => 5,
}
)
}
}
/// Represents a 3 digit SMTP response code
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Code {
/// First digit of the response code
severity: Severity,
/// Second digit of the response code
category: Category,
/// Third digit
detail: u8,
}
impl FromStr for Code {
type Err = Error;
#[inline]
fn from_str(s: &str) -> RResult<Code, Error> {
if s.len() == 3 {
match (s[0..1].parse::<Severity>(), s[1..2].parse::<Category>(), s[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {severity: severity, category: category, detail: detail}),
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
}
} else {
Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"))
}
}
}
impl Code {
/// Creates a new `Code` structure
pub fn new(severity: Severity, category: Category, detail: u8) -> Code {
Code {
severity: severity,
category: category,
detail: detail,
}
}
/// Returns the reply code
pub fn code(&self) -> String {
format!("{}{}{}", self.severity, self.category, self.detail)
}
}
/// Parses an SMTP response
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct ResponseParser {
/// Response code
code: Option<Code>,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>
}
impl ResponseParser {
/// Creates a new parser
pub fn new() -> ResponseParser {
ResponseParser {
code: None,
message: vec![],
}
}
/// Parses a line and return a `bool` indicating if there are more lines to come
pub fn read_line(&mut self, line: &str) -> RResult<bool, Error> {
if line.len() < 3 {
return Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"));
}
match self.code {
Some(ref code) => {
if code.code() != line[0..3] {
return Err(Error::ResponseParsingError("Response code has changed during a reponse"));
}
},
None => self.code = Some(try!(line[0..3].parse::<Code>()))
}
if line.len() > 4 {
self.message.push(line[4..].to_string());
if line.as_bytes()[3] == '-' as u8 {
Ok(true)
} else {
Ok(false)
}
} else {
Ok(false)
}
}
/// Builds a response from a `ResponseParser`
pub fn response(self) -> SmtpResult {
match self.code {
Some(code) => Ok(Response::new(code, self.message)),
None => Err(Error::ResponseParsingError("Incomplete response, could not read response code"))
}
}
}
/// Contains an SMTP reply, with separed code and message
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Response {
/// Response code
code: Code,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>
}
impl Response {
/// Creates a new `Response`
pub fn new(code: Code, message: Vec<String>) -> Response {
Response {
code: code,
message: message,
}
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.code.severity {
PositiveCompletion => true,
PositiveIntermediate => true,
_ => false,
}
}
/// Returns the message
pub fn message(&self) -> Vec<String> {
self.message.clone()
}
/// Returns the severity (i.e. 1st digit)
pub fn severity(&self) -> Severity {
self.code.severity
}
/// Returns the category (i.e. 2nd digit)
pub fn category(&self) -> Category {
self.code.category
}
/// Returns the detail (i.e. 3rd digit)
pub fn detail(&self) -> u8 {
self.code.detail
}
/// Returns the reply code
fn code(&self) -> String {
self.code.code()
}
/// Tests code equality
pub fn has_code(&self, code: u16) -> bool {
self.code() == format!("{}", code)
}
/// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<String> {
match self.message.is_empty() {
true => None,
false => match self.message[0].split_whitespace().next() {
Some(word) => Some(word.to_string()),
None => None,
}
}
}
}
#[cfg(test)]
mod test {
use super::{Severity, Category, Response, ResponseParser, Code};
#[test]
fn test_severity_from_str() {
assert_eq!("2".parse::<Severity>().unwrap(), Severity::PositiveCompletion);
assert_eq!("4".parse::<Severity>().unwrap(), Severity::TransientNegativeCompletion);
assert!("1".parse::<Severity>().is_err());
}
#[test]
fn test_severity_fmt() {
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
}
#[test]
fn test_category_from_str() {
assert_eq!("2".parse::<Category>().unwrap(), Category::Connections);
assert_eq!("4".parse::<Category>().unwrap(), Category::Unspecified4);
assert!("6".parse::<Category>().is_err());
}
#[test]
fn test_category_fmt() {
assert_eq!(format!("{}", Category::Unspecified4), "4");
}
#[test]
fn test_code_new() {
assert_eq!(
Code::new(Severity::TransientNegativeCompletion, Category::Connections, 0),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 0,
}
);
}
#[test]
fn test_code_from_str() {
assert_eq!(
"421".parse::<Code>().unwrap(),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
}
);
}
#[test]
fn test_code_code() {
let code = Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
};
assert_eq!(code.code(), "421");
}
#[test]
fn test_response_new() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
), Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
});
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![]
), Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec![],
});
}
#[test]
fn test_response_parser() {
let mut parser = ResponseParser::new();
assert!(parser.read_line("250-me").unwrap());
assert!(parser.read_line("250-8BITMIME").unwrap());
assert!(parser.read_line("250-SIZE 42").unwrap());
assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap());
let response = parser.response().unwrap();
assert_eq!(
response,
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: 0,
},
message: vec!["me".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "AUTH PLAIN CRAM-MD5".to_string()],
}
);
}
#[test]
fn test_response_is_positive() {
assert!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
assert!(! Response::new(
Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
}
#[test]
fn test_response_message() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
let empty_message: Vec<String> = vec![];
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![]
).message(), empty_message);
}
#[test]
fn test_response_severity() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PositiveCompletion);
assert_eq!(Response::new(
Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PermanentNegativeCompletion);
}
#[test]
fn test_response_category() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).category(), Category::Unspecified4);
}
#[test]
fn test_response_detail() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).detail(), 1);
}
#[test]
fn test_response_code() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).code(), "241");
}
#[test]
fn test_response_has_code() {
assert!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(241));
assert!(! Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(251));
}
#[test]
fn test_response_first_word() {
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![]
).first_word(), None);
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![" ".to_string()]
).first_word(), None);
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![" ".to_string()]
).first_word(), None);
assert_eq!(Response::new(
Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["".to_string()]
).first_word(), None);
}
}

View File

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

View File

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

1
website/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/_book

16
website/Makefile Normal file
View File

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

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]

19
website/src/README.md Normal file
View File

@@ -0,0 +1,19 @@
# Introduction
Lettre is an email library that allows creating and sending messages. It provides:
* An easy to use email builder
* Pluggable email transports
* Unicode support (for emails and transports, including for sender et recipient addresses when compatible)
* Secure defaults (emails are only sent encrypted by default)
The `lettre_email` crate allows you to compose messages, and the `lettre`
provide transports to send them.
Lettre requires Rust 1.32 or newer. Add the following to your `Cargo.toml`:
```toml
[dependencies]
lettre = "0.9"
lettre_email = "0.9"
```

9
website/src/SUMMARY.md Normal file
View File

@@ -0,0 +1,9 @@
# Summary
* [Introduction](README.md)
* [Creating Messages](creating-messages/email.md)
* [Sending Messages](sending-messages/_index.md)
* [SMTP Transport](sending-messages/smtp.md)
* [Sendmail Transport](sending-messages/sendmail.md)
* [File Transport](sending-messages/file.md)
* [Stub Transport](sending-messages/stub.md)

View File

@@ -0,0 +1,36 @@
### Creating messages
This section explains how to create emails.
#### Simple example
The `email` part builds email messages. For now, it does not support attachments.
An email is built using an `EmailBuilder`. The simplest email could be:
```rust
extern crate lettre_email;
use lettre_email::Email;
fn main() {
// Create an email
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")
.alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
.build();
assert!(email.is_ok());
}
```
When the `build` method is called, the `EmailBuilder` will add the missing headers (like
`Message-ID` or `Date`) and check for missing necessary ones (like `From` or `To`). It will
then generate an `Email` that can be sent.
The `text()` method will create a plain text email, while the `html()` method will create an
HTML email. You can use the `alternative()` method to provide both versions, using plain text
as fallback for the HTML version.

View File

@@ -0,0 +1,17 @@
### Sending Messages
This section explains how to manipulate emails you have created.
This mailer contains several different transports for your emails. To be sendable, the
emails have to implement `SendableEmail`, which is the case for emails created with `lettre_email`.
The following transports are available:
* The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
the preferred way of sending emails.
* The `SendmailTransport` uses the sendmail command to send messages. It is an alternative to
the SMTP transport.
* The `FileTransport` creates a file containing the email content to be sent. It can be used
for debugging or if you want to keep all sent emails.
* The `StubTransport` is useful for debugging, and only prints the content of the email in the
logs.

View File

@@ -0,0 +1,43 @@
#### File Transport
The file transport writes the emails to the given directory. The name of the file will be
`message_id.txt`.
It can be useful for testing purposes, or if you want to keep track of sent messages.
```rust
extern crate lettre;
use std::env::temp_dir;
use lettre::file::FileTransport;
use lettre::{Transport, Envelope, EmailAddress, SendableEmail};
fn main() {
// Write to the local temp directory
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);
assert!(result.is_ok());
}
```
Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.txt`:
```text
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
To: <root@localhost>
From: <user@localhost>
Subject: Hello
Date: Sat, 31 Oct 2015 13:42:19 +0100
Message-ID: <b7c211bc-9811-45ce-8cd9-68eab575d695.lettre@localhost>
Hello World!
```

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>
```

BIN
website/theme/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB