Compare commits

..

181 Commits

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

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

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

24
.appveyor.yml Normal file
View File

@@ -0,0 +1,24 @@
environment:
OPENSSL_INCLUDE_DIR: C:\OpenSSL\include
OPENSSL_LIB_DIR: C:\OpenSSL\lib
OPENSSL_LIBS: ssleay32:libeay32
matrix:
- TARGET: i686-pc-windows-gnu
BITS: 32
# - TARGET: x86_64-pc-windows-msvc
# BITS: 64
install:
- ps: Start-FileDownload "http://slproweb.com/download/Win${env:BITS}OpenSSL-1_0_2g.exe"
- Win%BITS%OpenSSL-1_0_2g.exe /SILENT /VERYSILENT /SP- /DIR="C:\OpenSSL"
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-beta-${env:TARGET}.exe"
- rust-beta-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
- SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
- SET PATH=%PATH%;C:\MinGW\bin
- rustc -V
- cargo -V
build: false
test_script:
- cargo build --verbose

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.project
/target/
/Cargo.lock

View File

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

20
.travis/coverage.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -o errexit
if [ "$TRAVIS_RUST_VERSION" != "stable" ]; then
exit 0
fi
cargo test --no-run
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz
tar xzf master.tar.gz
mkdir kcov-master/build
cd kcov-master/build
cmake ..
make
make install DESTDIR=../tmp
cd ../..
ls target/debug
./kcov-master/tmp/usr/local/bin/kcov --coveralls-id=$TRAVIS_JOB_ID --exclude-pattern=/.cargo target/kcov target/debug/lettre-*

38
.travis/doc.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
set -o errexit
if [ "$TRAVIS_RUST_VERSION" != "stable" ] || [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
exit 0
fi
cargo clean
cargo doc --no-deps
git clone --branch gh-pages "https://$GH_TOKEN@github.com/${TRAVIS_REPO_SLUG}.git" deploy_docs
cd deploy_docs
git config user.email "contact@amousset.me"
git config user.name "Alexis Mousset"
if [ "$TRAVIS_BRANCH" == "master" ]; then
rm -rf master
mv ../target/doc ./master
echo "<meta http-equiv=refresh content=0;url=lettre/index.html>" > ./master/index.html
elif [ "$TRAVIS_TAG" != "" ]; then
rm -rf $TRAVIS_TAG
mv ../target/doc ./$TRAVIS_TAG
echo "<meta http-equiv=refresh content=0;url=lettre/index.html>" > ./$TRAVIS_TAG/index.html
latest=$(echo * | tr " " "\n" | sort -V -r | head -n1)
if [ "$TRAVIS_TAG" == "$latest" ]; then
echo "<meta http-equiv=refresh content=0;url=$latest/lettre/index.html>" > index.html
fi
else
exit 0
fi
git add -A .
git commit -m "Rebuild pages at ${TRAVIS_COMMIT}"
git push --quiet origin gh-pages

43
CHANGELOG.md Normal file
View File

@@ -0,0 +1,43 @@
### v0.6.3 (2022-1--21)
* **transport**: Allow using openssl 0.10
### v0.6.2 (2017-02-18)
#### Features
* **all**
* Update uuid crate to 0.4
* Update env-logger crate to 0.4
* Update openssl crate to 0.9
### v0.6.1 (2016-10-19)
#### Features
* **documentation**
* #91: Build seperate docs for each release
* #96: Add complete documentation information to README
#### Bugfixes
* **email**
* #85: Use address-list for "To", "From" etc.
* **tests**
* #93: Force building tests before coverage computing
### v0.6.0 (2016-05-05)
#### Features
* **email**
* multipart support
* add non-consuming methods for Email builders
#### Beaking Change
* **email**
* `add_header` does not return the builder anymore,
for consistency with other methods. Use the `header`
method instead

38
CONTRIBUTING.md Normal file
View File

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

View File

@@ -1,7 +0,0 @@
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. All files in the project carrying such
notice may not be copied, modified, or distributed except
according to those terms.

View File

@@ -1,29 +1,28 @@
[package]
name = "smtp"
version = "0.0.13"
description = "Simple SMTP client"
name = "lettre"
version = "0.6.3"
description = "Email client"
readme = "README.md"
documentation = "http://amousset.github.io/rust-smtp/smtp/"
repository = "https://github.com/amousset/rust-smtp"
homepage = "https://github.com/amousset/rust-smtp"
license = "MIT/Apache-2.0"
documentation = "https://lettre.github.io/lettre/"
repository = "https://github.com/lettre/lettre"
license = "MIT"
authors = ["Alexis Mousset <contact@amousset.me>"]
keywords = ["email", "smtp", "mailer"]
[dependencies]
time = "0.1"
uuid = "0.1"
log = "0.3"
rustc-serialize = "0.3"
rust-crypto = "0.2"
bufstream = "0.1"
[dependencies.email]
git = "https://github.com/amousset/rust-email.git"
bufstream = "^0.1"
email = "^0.0"
log = "^0.3"
mime = "^0.2"
openssl = "> 0.9, < 0.11"
rustc-serialize = "^0.3"
rust-crypto = "^0.2"
time = "^0.1"
uuid = { version = "^0.4", features = ["v4"] }
[dev-dependencies]
env_logger = "*"
env_logger = "^0.4"
[features]
unstable = []

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,32 +1,52 @@
rust-smtp [![Build Status](https://travis-ci.org/amousset/rust-smtp.svg?branch=master)](https://travis-ci.org/amousset/rust-smtp) [![Coverage Status](https://coveralls.io/repos/amousset/rust-smtp/badge.svg?branch=master)](https://coveralls.io/r/amousset/rust-smtp?branch=master) [![](https://meritbadge.herokuapp.com/smtp)](https://crates.io/crates/smtp)
=========
# lettre
[![Build Status](https://travis-ci.org/lettre/lettre.svg?branch=master)](https://travis-ci.org/lettre/lettre)
[![Build status](https://ci.appveyor.com/api/projects/status/mpwglemugjtkps2d/branch/master?svg=true)](https://ci.appveyor.com/project/amousset/lettre/branch/master)
[![Coverage Status](https://coveralls.io/repos/github/lettre/lettre/badge.svg?branch=master)](https://coveralls.io/github/lettre/lettre?branch=master)
[![Crate](https://img.shields.io/crates/v/lettre.svg)](https://crates.io/crates/lettre)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
[![Gitter](https://badges.gitter.im/lettre/lettre.svg)](https://gitter.im/lettre/lettre?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
This library implements a simple SMTP client.
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information.
This is an email library written in Rust.
Install
-------
## Features
Lettre provides the following features:
* Multiple transport methods
* Unicode support (for email content and addresses)
* Secure delivery with SMTP using encryption and authentication
* Easy email builders
## Documentation
Released versions:
* [latest](https://lettre.github.io/lettre/)
* [v0.6.3](https://lettre.github.io/lettre/v0.6.3/lettre/)
* [v0.6.2](https://lettre.github.io/lettre/v0.6.2/lettre/)
* [v0.6.1](https://lettre.github.io/lettre/v0.6.1/lettre/)
* [v0.6.0](https://lettre.github.io/lettre/v0.6.0/lettre/)
* [v0.5.1](https://lettre.github.io/lettre/v0.5.1/lettre/)
Development version:
* [master](https://lettre.github.io/lettre/master/lettre/)
## Install
To use this library, add the following to your `Cargo.toml`:
```toml
[dependencies]
smtp = "*"
lettre = "0.6"
```
Tests
-----
## Testing
You can build and run the tests with `cargo test`.
The tests require an open mail server listening locally on port 25.
Documentation
-------------
## License
You can build the documentation with `cargo doc`. It is also available on [GitHub pages](http://amousset.github.io/rust-smtp/smtp/).
This program is distributed under the terms of the MIT license.
License
-------
This program is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.
See [LICENSE](./LICENSE) for details.

44
benches/transport_smtp.rs Normal file
View File

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

View File

@@ -1,42 +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.
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate smtp;
use smtp::sender::{Sender, SenderBuilder};
use smtp::mailer::EmailBuilder;
fn main() {
env_logger::init().unwrap();
let mut email_builder = EmailBuilder::new();
email_builder = email_builder.to("user@localhost");
let email = email_builder.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build();
let mut sender: Sender = SenderBuilder::localhost().hello_name("localhost")
.enable_connection_reuse(true).build();
for _ in (1..5) {
let _ = sender.send(email.clone());
}
let result = sender.send(email);
sender.close();
match result {
Ok(..) => info!("Email sent successfully"),
Err(error) => error!("{:?}", error),
}
}

1
rustfmt.toml Normal file
View File

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

View File

@@ -1,51 +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.
//! Provides authentication functions
use serialize::base64::{self, ToBase64, FromBase64};
use serialize::hex::ToHex;
use crypto::hmac::Hmac;
use crypto::md5::Md5;
use crypto::mac::Mac;
use NUL;
/// Returns a PLAIN mecanism response
pub fn plain(username: &str, password: &str) -> String {
format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD)
}
/// Returns a CRAM-MD5 mecanism response
pub fn cram_md5(username: &str, password: &str, encoded_challenge: &str) -> String {
// TODO manage errors
let challenge = encoded_challenge.from_base64().unwrap();
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
hmac.input(&challenge);
format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD)
}
#[cfg(test)]
mod test {
use super::{plain, cram_md5};
#[test]
fn test_plain() {
assert_eq!(plain("username", "password"), "AHVzZXJuYW1lAHBhc3N3b3Jk");
}
#[test]
fn test_cram_md5() {
assert_eq!(cram_md5("alice", "wonderland",
"PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
}
}

View File

@@ -1,30 +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.
//! 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;

31
src/email/error.rs Normal file
View File

@@ -0,0 +1,31 @@
//! Error and result type for emails
use self::Error::*;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
/// An enum of all error kinds.
#[derive(Debug)]
pub enum Error {
/// Missinf sender
MissingFrom,
/// Missing recipient
MissingTo,
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
MissingFrom => "the sender is missing",
MissingTo => "the recipient is missing",
}
}
}

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

@@ -0,0 +1,799 @@
//! Simple email representation
pub mod error;
use email::error::Error;
use email_format::{Header, Mailbox, Address, MimeMessage, MimeMultipartType};
use mime::Mime;
use std::fmt;
use std::fmt::{Display, Formatter};
use time::{Tm, now};
use uuid::Uuid;
/// Converts an address or an address with an alias to a `Header`
pub trait ToHeader {
/// Converts to a `Header` struct
fn to_header(&self) -> Header;
}
impl ToHeader for Header {
fn to_header(&self) -> Header {
self.clone()
}
}
impl<'a> ToHeader for (&'a str, &'a str) {
fn to_header(&self) -> Header {
let (name, value) = *self;
Header::new(name.to_string(), value.to_string())
}
}
/// Converts an adress or an address with an alias to a `Mailbox`
pub trait ToMailbox {
/// Converts to a `Mailbox` struct
fn to_mailbox(&self) -> Mailbox;
}
impl ToMailbox for Mailbox {
fn to_mailbox(&self) -> Mailbox {
(*self).clone()
}
}
impl<'a> ToMailbox for &'a str {
fn to_mailbox(&self) -> Mailbox {
Mailbox::new(self.to_string())
}
}
impl<'a> ToMailbox for (&'a str, &'a str) {
fn to_mailbox(&self) -> Mailbox {
let (address, alias) = *self;
Mailbox::new_with_name(alias.to_string(), address.to_string())
}
}
/// Can be transformed to a sendable email
pub trait IntoEmail {
/// Builds an email
fn into_email(&self) -> Result<Email, Error>;
}
impl IntoEmail for SimpleEmail {
fn into_email(&self) -> Result<Email, Error> {
let mut builder = EmailBuilder::new();
if self.from.is_some() {
builder.add_from(self.from.as_ref().unwrap().as_str().to_mailbox());
}
for to_address in self.to.as_slice() {
builder.add_to(to_address.as_str().to_mailbox());
}
for cc_address in self.cc.as_slice() {
builder.add_cc(cc_address.as_str().to_mailbox());
}
// No bcc for now
if self.reply_to.is_some() {
builder.add_reply_to(self.reply_to.as_ref().unwrap().as_str().to_mailbox());
}
if self.subject.is_some() {
builder.set_subject(self.subject.as_ref().unwrap().as_str());
}
// No date for now
match (self.text.as_ref(), self.html.as_ref()) {
(Some(text), Some(html)) => builder.set_alternative(html.as_str(), text.as_str()),
(Some(text), None) => builder.set_text(text.as_str()),
(None, Some(html)) => builder.set_html(html.as_str()),
(None, None) => (),
}
for header in self.headers.as_slice() {
builder.add_header(header.to_header());
}
Ok(builder.build().unwrap())
}
}
/// Simple representation of an email, useful for some transports
#[derive(PartialEq,Eq,Clone,Debug,Default)]
pub struct SimpleEmail {
from: Option<String>,
to: Vec<String>,
cc: Vec<String>,
// bcc: Vec<String>,
reply_to: Option<String>,
subject: Option<String>,
date: Option<Tm>,
html: Option<String>,
text: Option<String>,
// attachments: Vec<String>,
headers: Vec<Header>,
}
impl SimpleEmail {
/// Adds a generic header
pub fn header<A: ToHeader>(mut self, header: A) -> SimpleEmail {
self.add_header(header);
self
}
/// Adds a generic header
pub fn add_header<A: ToHeader>(&mut self, header: A) {
self.headers.push(header.to_header());
}
/// Adds a `From` header and stores the sender address
pub fn from<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
self.add_from(address);
self
}
/// Adds a `From` header and stores the sender address
pub fn add_from<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.from = Some(mailbox.address);
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
self.add_to(address);
self
}
/// Adds a `To` header and stores the recipient address
pub fn add_to<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.to.push(mailbox.address);
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
self.add_cc(address);
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn add_cc<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.cc.push(mailbox.address);
}
/// Adds a `Reply-To` header
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> SimpleEmail {
self.add_reply_to(address);
self
}
/// Adds a `Reply-To` header
pub fn add_reply_to<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.reply_to = Some(mailbox.address);
}
/// Adds a `Subject` header
pub fn subject(mut self, subject: &str) -> SimpleEmail {
self.set_subject(subject);
self
}
/// Adds a `Subject` header
pub fn set_subject(&mut self, subject: &str) {
self.subject = Some(subject.to_string());
}
/// Adds a `Date` header with the given date
pub fn date(mut self, date: &Tm) -> SimpleEmail {
self.set_date(date);
self
}
/// Adds a `Date` header with the given date
pub fn set_date(&mut self, date: &Tm) {
self.date = Some(date.clone());
}
/// Sets the email body to plain text content
pub fn text(mut self, body: &str) -> SimpleEmail {
self.set_text(body);
self
}
/// Sets the email body to plain text content
pub fn set_text(&mut self, body: &str) {
self.text = Some(body.to_string());
}
/// Sets the email body to HTML content
pub fn html(mut self, body: &str) -> SimpleEmail {
self.set_html(body);
self
}
/// Sets the email body to HTML content
pub fn set_html(&mut self, body: &str) {
self.html = Some(body.to_string());
}
}
/// Builds a `MimeMessage` structure
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct PartBuilder {
/// Message
message: MimeMessage,
}
/// Builds an `Email` structure
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct EmailBuilder {
/// Message
message: PartBuilder,
/// The recipients' addresses for the mail header
to_header: Vec<Address>,
/// The sender addresses for the mail header
from_header: Vec<Address>,
/// The Cc addresses for the mail header
cc_header: Vec<Address>,
/// The Reply-To addresses for the mail header
reply_to_header: Vec<Address>,
/// The sender address for the mail header
sender_header: Option<Mailbox>,
/// The envelope recipients' addresses
to: Vec<String>,
/// The envelope sender address
from: Option<String>,
/// Date issued
date_issued: bool,
}
/// Simple email enveloppe representation
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Envelope {
/// The envelope recipients' addresses
to: Vec<String>,
/// The envelope sender address
from: String,
}
/// Simple email representation
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Email {
/// Message
message: MimeMessage,
/// Envelope
envelope: Envelope,
/// Message-ID
message_id: Uuid,
}
impl Display for Email {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.message.as_string())
}
}
impl PartBuilder {
/// Creates a new empty part
pub fn new() -> PartBuilder {
PartBuilder { message: MimeMessage::new_blank_message() }
}
/// Adds a generic header
pub fn header<A: ToHeader>(mut self, header: A) -> PartBuilder {
self.add_header(header);
self
}
/// Adds a generic header
pub fn add_header<A: ToHeader>(&mut self, header: A) {
self.message.headers.insert(header.to_header());
}
/// Sets the body
pub fn body(mut self, body: &str) -> PartBuilder {
self.set_body(body);
self
}
/// Sets the body
pub fn set_body(&mut self, body: &str) {
self.message.body = body.to_string();
}
/// Defines a `MimeMultipartType` value
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
self.set_message_type(mime_type);
self
}
/// Defines a `MimeMultipartType` value
pub fn set_message_type(&mut self, mime_type: MimeMultipartType) {
self.message.message_type = Some(mime_type);
}
/// Adds a `ContentType` header with the given MIME type
pub fn content_type(mut self, content_type: Mime) -> PartBuilder {
self.set_content_type(content_type);
self
}
/// Adds a `ContentType` header with the given MIME type
pub fn set_content_type(&mut self, content_type: Mime) {
self.add_header(("Content-Type", format!("{}", content_type).as_ref()));
}
/// Adds a child part
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
self.add_child(child);
self
}
/// Adds a child part
pub fn add_child(&mut self, child: MimeMessage) {
self.message.children.push(child);
}
/// Gets builded `MimeMessage`
pub fn build(mut self) -> MimeMessage {
self.message.update_headers();
self.message
}
}
impl EmailBuilder {
/// Creates a new empty email
pub fn new() -> EmailBuilder {
EmailBuilder {
message: PartBuilder::new(),
to_header: vec![],
from_header: vec![],
cc_header: vec![],
reply_to_header: vec![],
sender_header: None,
to: vec![],
from: None,
date_issued: false,
}
}
/// Sets the email body
pub fn body(mut self, body: &str) -> EmailBuilder {
self.message.set_body(body);
self
}
/// Sets the email body
pub fn set_body(&mut self, body: &str) {
self.message.set_body(body);
}
/// Add a generic header
pub fn header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
self.message.add_header(header);
self
}
/// Add a generic header
pub fn add_header<A: ToHeader>(&mut self, header: A) {
self.message.add_header(header);
}
/// Adds a `From` header and stores the sender address
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.add_from(address);
self
}
/// Adds a `From` header and stores the sender address
pub fn add_from<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.from = Some(mailbox.address.clone());
self.from_header.push(Address::Mailbox(mailbox));
}
/// Adds a `To` header and stores the recipient address
pub fn to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.add_to(address);
self
}
/// Adds a `To` header and stores the recipient address
pub fn add_to<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.to.push(mailbox.address.clone());
self.to_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Cc` header and stores the recipient address
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.add_cc(address);
self
}
/// Adds a `Cc` header and stores the recipient address
pub fn add_cc<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.to.push(mailbox.address.clone());
self.cc_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Reply-To` header
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.add_reply_to(address);
self
}
/// Adds a `Reply-To` header
pub fn add_reply_to<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.reply_to_header.push(Address::Mailbox(mailbox));
}
/// Adds a `Sender` header
pub fn sender<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
self.set_sender(address);
self
}
/// Adds a `Sender` header
pub fn set_sender<A: ToMailbox>(&mut self, address: A) {
let mailbox = address.to_mailbox();
self.from = Some(mailbox.address.clone());
self.sender_header = Some(mailbox);
}
/// Adds a `Subject` header
pub fn subject(mut self, subject: &str) -> EmailBuilder {
self.set_subject(subject);
self
}
/// Adds a `Subject` header
pub fn set_subject(&mut self, subject: &str) {
self.message.add_header(("Subject", subject));
}
/// Adds a `Date` header with the given date
pub fn date(mut self, date: &Tm) -> EmailBuilder {
self.set_date(date);
self
}
/// Adds a `Date` header with the given date
pub fn set_date(&mut self, date: &Tm) {
self.message.add_header(("Date", Tm::rfc822z(date).to_string().as_ref()));
self.date_issued = true;
}
/// Set the message type
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
self.set_message_type(message_type);
self
}
/// Set the message type
pub fn set_message_type(&mut self, message_type: MimeMultipartType) {
self.message.set_message_type(message_type);
}
/// Adds a child
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
self.add_child(child);
self
}
/// Adds a child
pub fn add_child(&mut self, child: MimeMessage) {
self.message.add_child(child);
}
/// Sets the email body to plain text content
pub fn text(mut self, body: &str) -> EmailBuilder {
self.set_text(body);
self
}
/// Sets the email body to plain text content
pub fn set_text(&mut self, body: &str) {
self.message.set_body(body);
self.message
.add_header(("Content-Type", format!("{}", mime!(Text/Plain; Charset=Utf8)).as_ref()));
}
/// Sets the email body to HTML content
pub fn html(mut self, body: &str) -> EmailBuilder {
self.set_html(body);
self
}
/// Sets the email body to HTML content
pub fn set_html(&mut self, body: &str) {
self.message.set_body(body);
self.message
.add_header(("Content-Type", format!("{}", mime!(Text/Html; Charset=Utf8)).as_ref()));
}
/// Sets the email content
pub fn alternative(mut self, body_html: &str, body_text: &str) -> EmailBuilder {
self.set_alternative(body_html, body_text);
self
}
/// Sets the email content
pub fn set_alternative(&mut self, body_html: &str, body_text: &str) {
let mut alternate = PartBuilder::new();
alternate.set_message_type(MimeMultipartType::Alternative);
let text = PartBuilder::new()
.body(body_text)
.header(("Content-Type", format!("{}", mime!(Text/Plain; Charset=Utf8)).as_ref()))
.build();
let html = PartBuilder::new()
.body(body_html)
.header(("Content-Type", format!("{}", mime!(Text/Html; Charset=Utf8)).as_ref()))
.build();
alternate.add_child(text);
alternate.add_child(html);
self.set_message_type(MimeMultipartType::Mixed);
self.add_child(alternate.build());
}
/// Builds the Email
pub fn build(mut self) -> Result<Email, Error> {
if self.from.is_none() {
return Err(Error::MissingFrom);
}
if self.to.is_empty() {
return Err(Error::MissingTo);
}
// If there are multiple addresses in "From", the "Sender" is required.
if self.from_header.len() >= 2 && self.sender_header.is_none() {
// So, we must find something to put as Sender.
for possible_sender in self.from_header.iter() {
// Only a mailbox can be used as sender, not Address::Group.
if let &Address::Mailbox(ref mbx) = possible_sender {
self.sender_header = Some(mbx.clone());
break;
}
}
// Address::Group is not yet supported, so the line below will never panic.
// If groups are supported one day, add another Error for this case
// and return it here, if sender_header is still None at this point.
assert!(self.sender_header.is_some());
}
// Add the sender header, if any.
if let Some(v) = self.sender_header {
self.message.add_header(("Sender", v.to_string().as_ref()));
}
// Add the collected addresses as mailbox-list all at once.
// The unwraps are fine because the conversions for Vec<Address> never errs.
self.message.add_header(Header::new_with_value("To".into(), self.to_header).unwrap());
self.message.add_header(Header::new_with_value("From".into(), self.from_header).unwrap());
if !self.cc_header.is_empty() {
self.message.add_header(Header::new_with_value("Cc".into(), self.cc_header).unwrap());
}
if !self.reply_to_header.is_empty() {
self.message.add_header(Header::new_with_value("Reply-To".into(),
self.reply_to_header)
.unwrap());
}
if !self.date_issued {
self.message.add_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
}
self.message.add_header(("MIME-Version", "1.0"));
let message_id = Uuid::new_v4();
if let Ok(header) = Header::new_with_value("Message-ID".to_string(),
format!("<{}.lettre@localhost>", message_id)) {
self.message.add_header(header)
}
Ok(Email {
message: self.message.build(),
envelope: Envelope {
to: self.to,
from: self.from.unwrap(),
},
message_id: message_id,
})
}
}
/// Email sendable by an SMTP client
pub trait SendableEmail {
/// From address
fn from_address(&self) -> String;
/// To addresses
fn to_addresses(&self) -> Vec<String>;
/// Message content
fn message(&self) -> String;
/// Message ID
fn message_id(&self) -> String;
}
/// Minimal email structure
pub struct SimpleSendableEmail {
/// From address
from: String,
/// To addresses
to: Vec<String>,
/// Message
message: String,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(from_address: &str, to_address: Vec<String>, message: &str) -> SimpleSendableEmail {
SimpleSendableEmail {
from: from_address.to_string(),
to: to_address,
message: message.to_string(),
}
}
}
impl SendableEmail for SimpleSendableEmail {
fn from_address(&self) -> String {
self.from.clone()
}
fn to_addresses(&self) -> Vec<String> {
self.to.clone()
}
fn message(&self) -> String {
self.message.clone()
}
fn message_id(&self) -> String {
format!("{}", Uuid::new_v4())
}
}
impl SendableEmail for Email {
fn to_addresses(&self) -> Vec<String> {
self.envelope.to.clone()
}
fn from_address(&self) -> String {
self.envelope.from.clone()
}
fn message(&self) -> String {
format!("{}", self)
}
fn message_id(&self) -> String {
format!("{}", self.message_id)
}
}
#[cfg(test)]
mod test {
use email_format::{Header, MimeMessage};
use super::{Email, EmailBuilder, Envelope, SendableEmail};
use time::now;
use uuid::Uuid;
#[test]
fn test_email_display() {
let current_message = Uuid::new_v4();
let mut email = Email {
message: MimeMessage::new_blank_message(),
envelope: Envelope {
to: vec![],
from: "".to_string(),
},
message_id: current_message,
};
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>",
current_message))
.unwrap());
email.message
.headers
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
.unwrap());
email.message.body = "body".to_string();
assert_eq!(format!("{}", email),
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
current_message));
assert_eq!(current_message.to_string(), email.message_id());
}
#[test]
fn test_multiple_from() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = email_builder.to("anna@example.com")
.from("dieter@example.com")
.from("joachim@example.com")
.date(&date_now)
.subject("Invitation")
.body("We invite you!")
.build()
.unwrap();
assert_eq!(format!("{}", email),
format!("Date: {}\r\nSubject: Invitation\r\nSender: \
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
date_now.rfc822z(),
email.message_id()));
}
#[test]
fn test_simple_email_builder() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = email_builder.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap();
assert_eq!(format!("{}", email),
format!("Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
<reply@localhost>\r\nMIME-Version: 1.0\r\nMessage-ID: \
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
date_now.rfc822z(),
email.message_id()));
}
#[test]
fn test_email_sendable() {
let email_builder = EmailBuilder::new();
let date_now = now();
let email = email_builder.to("user@localhost")
.from("user@localhost")
.cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost")
.sender("sender@localhost")
.body("Hello World!")
.date(&date_now)
.subject("Hello")
.header(("X-test", "value"))
.build()
.unwrap();
assert_eq!(email.from_address(), "sender@localhost".to_string());
assert_eq!(email.to_addresses(),
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
assert_eq!(email.message(), format!("{}", email));
}
}

View File

@@ -1,89 +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.
//! Error and result type for SMTP clients
use std::error::Error;
use std::io;
use std::fmt::{Display, Formatter};
use std::fmt;
use response::{Severity, Response};
use self::SmtpError::*;
/// An enum of all error kinds.
#[derive(Debug)]
pub enum SmtpError {
/// Transient error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
TransientError(Response),
/// Permanent error, 5xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
PermanentError(Response),
/// TODO
ClientError(String),
/// IO error
IoError(io::Error),
}
impl Display for SmtpError {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl Error for SmtpError {
fn description(&self) -> &str {
match *self {
TransientError(_) => "a transient error occured during the SMTP transaction",
PermanentError(_) => "a permanent error occured during the SMTP transaction",
ClientError(_) => "an unknown error occured",
IoError(_) => "an I/O error occured",
}
}
fn cause(&self) -> Option<&Error> {
match *self {
IoError(ref err) => Some(&*err as &Error),
_ => None,
}
}
}
impl From<io::Error> for SmtpError {
fn from(err: io::Error) -> SmtpError {
IoError(err)
}
}
impl From<Response> for SmtpError {
fn from(response: Response) -> SmtpError {
match response.severity() {
Severity::TransientNegativeCompletion => TransientError(response),
Severity::PermanentNegativeCompletion => PermanentError(response),
_ => ClientError("Unknown error code".to_string())
}
}
}
impl From<&'static str> for SmtpError {
fn from(string: &'static str) -> SmtpError {
ClientError(string.to_string())
}
}
/// SMTP result type
pub type SmtpResult = Result<Response, SmtpError>;
#[cfg(test)]
mod test {
// TODO
}

View File

@@ -1,110 +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.
//! ESMTP features
use std::str::FromStr;
use std::result::Result;
use response::Response;
use self::Extension::*;
/// Supported ESMTP keywords
#[derive(PartialEq,Eq,Copy,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 PLAIN mecanism
///
/// RFC 4616: https://tools.ietf.org/html/rfc4616
PlainAuthentication,
/// AUTH CRAM-MD5 mecanism
///
/// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5Authentication,
}
impl Extension {
fn from_str(s: &str) -> Result<Vec<Extension>, &'static str> {
let splitted : Vec<&str> = s.split(' ').collect();
match (splitted[0], splitted.len()) {
("8BITMIME", 1) => Ok(vec![EightBitMime]),
("SMTPUTF8", 1) => Ok(vec![SmtpUtfEight]),
("STARTTLS", 1) => Ok(vec![StartTls]),
("AUTH", _) => {
let mut mecanisms: Vec<Extension> = vec![];
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => mecanisms.push(PlainAuthentication),
"CRAM-MD5" => mecanisms.push(CramMd5Authentication),
_ => (),
}
}
Ok(mecanisms)
},
_ => Err("Unknown extension"),
}
}
/// Parses supported ESMTP features
pub fn parse_esmtp_response(response: &Response) -> Vec<Extension> {
let mut esmtp_features: Vec<Extension> = Vec::new();
for line in response.message() {
if let Ok(keywords) = Extension::from_str(&line) {
for keyword in keywords {
esmtp_features.push(keyword);
}
};
}
esmtp_features
}
}
#[cfg(test)]
mod test {
use super::Extension;
use response::{Severity, Category, Response};
#[test]
fn test_from_str() {
assert_eq!(Extension::from_str("8BITMIME"), Ok(vec![Extension::EightBitMime]));
assert_eq!(Extension::from_str("AUTH PLAIN"), Ok(vec![Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH PLAIN LOGIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
assert_eq!(Extension::from_str("AUTH CRAM-MD5 PLAIN"), Ok(vec![Extension::CramMd5Authentication, Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
}
#[test]
fn test_parse_esmtp_response() {
assert_eq!(Extension::parse_esmtp_response(&Response::new(
"2".parse::<Severity>().unwrap(),
"2".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
)), vec![Extension::EightBitMime]);
assert_eq!(Extension::parse_esmtp_response(&Response::new(
"4".parse::<Severity>().unwrap(),
"3".parse::<Category>().unwrap(),
3,
vec!["me".to_string(), "8BITMIME".to_string(), "AUTH PLAIN CRAM-MD5".to_string()]
)), vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
}
}

View File

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

View File

@@ -1,212 +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.
//! Simple email (very incomplete)
use email::{MimeMessage, Header, Address};
use time::{now, Tm};
use uuid::Uuid;
use sendable_email::SendableEmail;
/// Converts an adress or an address with an alias to an `Address`
pub trait ToHeader {
/// Converts to an `Address` 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 an `Address`
pub trait ToAddress {
/// Converts to an `Address` struct
fn to_address(&self) -> Address;
}
impl ToAddress for Address {
fn to_address(&self) -> Address {
(*self).clone()
}
}
impl<'a> ToAddress for &'a str {
fn to_address(&self) -> Address {
Address::new_mailbox(self.to_string())
}
}
impl<'a> ToAddress for (&'a str, &'a str) {
fn to_address(&self) -> Address {
let (address, alias) = *self;
Address::new_mailbox_with_name(address.to_string(), alias.to_string())
}
}
/// TODO
#[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 Email {
/// Displays the formatted email content
pub fn as_string(&self) -> String {
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,
};
email.message.headers.insert(
Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>", current_message)
).unwrap()
);
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: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.from = Some(address.to_address().get_address().unwrap());
self.insert_header(("From", address.to_address().to_string().as_ref()));
self
}
/// Adds a `To` header and store the recipient address
pub fn to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.to.push(address.to_address().get_address().unwrap());
self.insert_header(("To", address.to_address().to_string().as_ref()));
self
}
/// Adds a `Cc` header and store the recipient address
pub fn cc<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.to.push(address.to_address().get_address().unwrap());
self.insert_header(("Cc", address.to_address().to_string().as_ref()));
self
}
/// Adds a `Reply-To` header
pub fn reply_to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.insert_header(("Reply-To", address.to_address().to_string().as_ref()));
self
}
/// Adds a `Sender` header
pub fn sender<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.from = Some(address.to_address().get_address().unwrap());
self.insert_header(("Sender", address.to_address().to_string().as_ref()));
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
}
}
impl SendableEmail for Email {
/// Return the to addresses, and fails if it is not set
fn to_addresses(&self) -> Vec<String> {
if self.to.is_empty() {
panic!("The To field is empty")
}
self.to.clone()
}
/// Return the from address, and fails if it is not set
fn from_address(&self) -> String {
match self.from {
Some(ref from_address) => from_address.clone(),
None => panic!("The From field is empty"),
}
}
fn message(&self) -> String {
format! ("{}", self.as_string())
}
fn message_id(&self) -> String {
format!("{}", self.message_id)
}
}

View File

@@ -1,345 +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.
//! 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::*;
/// 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 = &'static str;
fn from_str(s: &str) -> RResult<Severity, &'static str> {
match s {
"2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err("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 = &'static str;
fn from_str(s: &str) -> RResult<Category, &'static str> {
match s {
"0" => Ok(Syntax),
"1" => Ok(Information),
"2" => Ok(Connections),
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"5" => Ok(MailSystem),
_ => Err("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,
}
)
}
}
/// 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 {
/// First digit of the response code
severity: Severity,
/// Second digit of the response code
category: Category,
/// Third digit
detail: u8,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>
}
impl Response {
/// Creates a new `Response`
pub fn new(severity: Severity, category: Category, detail: u8, message: Vec<String>) -> Response {
Response {
severity: severity,
category: category,
detail: detail,
message: message
}
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.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.severity
}
/// Returns the category (i.e. 2nd digit)
pub fn category(&self) -> Category {
self.category
}
/// Returns the detail (i.e. 3rd digit)
pub fn detail(&self) -> u8 {
self.detail
}
/// Returns the reply code
fn code(&self) -> String {
format!("{}{}{}", self.severity, self.category, self.detail)
}
/// Checls 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 => Some(self.message[0].split(" ").next().unwrap().to_string()),
}
}
}
#[cfg(test)]
mod test {
use super::{Severity, Category, Response};
#[test]
fn test_severity_from_str() {
assert_eq!("2".parse::<Severity>(), Ok(Severity::PositiveCompletion));
assert_eq!("4".parse::<Severity>(), Ok(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>(), Ok(Category::Connections));
assert_eq!("4".parse::<Category>(), Ok(Category::Unspecified4));
assert!("6".parse::<Category>().is_err());
}
#[test]
fn test_category_fmt() {
assert_eq!(format!("{}", Category::Unspecified4), "4");
}
#[test]
fn test_response_new() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
), Response {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
});
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec![]
), Response {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
message: vec![],
});
}
#[test]
fn test_response_is_positive() {
assert!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
assert!(! Response::new(
"4".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
}
#[test]
fn test_response_message() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
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(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec![]
).message(), empty_message);
}
#[test]
fn test_response_severity() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PositiveCompletion);
}
#[test]
fn test_response_category() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).category(), Category::Unspecified4);
}
#[test]
fn test_response_detail() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).detail(), 1);
}
#[test]
fn test_response_code() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).code(), "241");
}
#[test]
fn test_response_has_code() {
assert!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(241));
assert!(! Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
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(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec![]
).first_word(), None);
}
}

View File

@@ -1,63 +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.
//! SMTP sendable email
use uuid::Uuid;
/// Email sendable by an SMTP client
pub trait SendableEmail {
/// From address
fn from_address(&self) -> String;
/// To addresses
fn to_addresses(&self) -> Vec<String>;
/// Message content
fn message(&self) -> String;
/// Message ID
fn message_id(&self) -> String;
}
/// Minimal email structure
pub struct SimpleSendableEmail {
/// From address
from: String,
/// To addresses
to: Vec<String>,
/// Message
message: String,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(from_address: &str, to_address: &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) -> String {
self.from.clone()
}
fn to_addresses(&self) -> Vec<String> {
self.to.clone()
}
fn message(&self) -> String {
self.message.clone()
}
fn message_id(&self) -> String {
format!("<{}@rust-smtp>", Uuid::new_v4())
}
}

View File

@@ -1,272 +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.
//! Sends an email using the client
use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use SMTP_PORT;
use extension::Extension;
use error::{SmtpResult, SmtpError};
use sendable_email::SendableEmail;
use sender::server_info::ServerInfo;
use client::Client;
use client::net::SmtpStream;
mod server_info;
/// 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) -> SenderBuilder {
SenderBuilder {
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
credentials: None,
connection_reuse_count_limit: 100,
enable_connection_reuse: false,
hello_name: "localhost".to_string(),
}
}
/// Creates a new local SMTP client to port 25
pub fn localhost() -> SenderBuilder {
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);
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
match self.client.ehlo(&self.client_info.hello_name) {
Ok(response) => {self.server_info = Some(
ServerInfo{
name: response.first_word().expect("Server announced no hostname"),
esmtp_features: Extension::parse_esmtp_response(&response),
});
},
Err(error) => match error {
SmtpError::PermanentError(ref response) if response.has_code(550) => {
match self.client.helo(&self.client_info.hello_name) {
Ok(response) => {self.server_info = Some(
ServerInfo{
name: response.first_word().expect("Server announced no hostname"),
esmtp_features: vec!(),
});
},
Err(error) => try_smtp!(Err(error), self)
}
},
_ => {
try_smtp!(Err(error), 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_feature(Extension::CramMd5Authentication) {
let result = self.client.auth_cram_md5(&username, &password);
try_smtp!(result, self);
} else if self.server_info.as_ref().unwrap().supports_feature(Extension::PlainAuthentication) {
let result = self.client.auth_plain(&username, &password);
try_smtp!(result, self);
} else {
debug!("No supported authentication mecanisms available");
}
}
let current_message = email.message_id();
let from_address = email.from_address();
let to_addresses = email.to_addresses();
let message = email.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,85 +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.
//! Information about a server
use std::fmt;
use std::fmt::{Display, Formatter};
use extension::Extension;
/// Contains information about an SMTP server
#[derive(Clone,Debug)]
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 esmtp_features: Vec<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{} with {}",
self.name,
match self.esmtp_features.is_empty() {
true => "no supported features".to_string(),
false => format! ("{:?}", self.esmtp_features),
}
)
}
}
impl ServerInfo {
/// Checks if the server supports an ESMTP feature
pub fn supports_feature(&self, keyword: Extension) -> bool {
self.esmtp_features.contains(&keyword)
}
}
#[cfg(test)]
mod test {
use super::ServerInfo;
use extension::Extension;
#[test]
fn test_fmt() {
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::EightBitMime]
}), "name with [EightBitMime]".to_string());
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::EightBitMime]
}), "name with [EightBitMime]".to_string());
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: vec![]
}), "name with no supported features".to_string());
}
#[test]
fn test_supports_feature() {
assert!(ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::EightBitMime]
}.supports_feature(Extension::EightBitMime));
assert!(ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::PlainAuthentication, Extension::EightBitMime]
}.supports_feature(Extension::EightBitMime));
assert_eq!(ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::EightBitMime]
}.supports_feature(Extension::PlainAuthentication), false);
}
}

View File

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

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

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

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

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

View File

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

View File

@@ -1,40 +1,34 @@
// 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.
//! SMTP client
use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use std::io::{BufRead, Read, Write};
use bufstream::BufStream;
use openssl::ssl::SslContext;
use std::fmt::Debug;
use std::io;
use std::io::{BufRead, Read, Write};
use std::net::ToSocketAddrs;
use std::string::String;
use transport::smtp::{CRLF, MESSAGE_ENDING};
use transport::smtp::authentication::Mechanism;
use transport::smtp::client::net::{Connector, NetworkStream};
use response::{Response, Severity, Category};
use error::SmtpResult;
use client::net::{Connector, SmtpStream};
use client::authentication::{plain, cram_md5};
use {CRLF, MESSAGE_ENDING};
use transport::smtp::error::{SmtpResult, Error};
use transport::smtp::response::ResponseParser;
pub mod net;
mod authentication;
/// 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..")
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\>"
@@ -43,14 +37,18 @@ 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> {
#[derive(Debug)]
pub struct Client<S: Write + Read = NetworkStream> {
/// 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 (
@@ -59,34 +57,64 @@ macro_rules! return_err (
})
);
impl<S: Write + Read = SmtpStream> Client<S> {
impl<S: Write + Read> Client<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn new<A: ToSocketAddrs>(addr: A) -> Client<S> {
Client{
stream: None,
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
}
pub fn new() -> Client<S> {
Client { stream: None }
}
}
impl<S: Connector + Write + Read = SmtpStream> Client<S> {
impl<S: Connector + Write + Read + Debug> Client<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.quit();
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, ssl_context: &SslContext) -> io::Result<()> {
match self.stream {
Some(ref mut stream) => stream.get_mut().upgrade_tls(ssl_context),
None => Ok(()),
}
}
/// Tells if the underlying stream is currently encrypted
pub fn is_encrypted(&self) -> bool {
match self.stream {
Some(ref stream) => stream.get_ref().is_encrypted(),
None => false,
}
}
/// Connects to the configured server
pub fn connect(&mut self) -> SmtpResult {
pub fn connect<A: ToSocketAddrs>(&mut self,
addr: &A,
ssl_context: Option<&SslContext>)
-> SmtpResult {
// Connect should not be called when the client is already connected
if self.stream.is_some() {
return_err!("The connection is already established", self);
}
let mut addresses = try!(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.stream = Some(BufStream::new(try!(Connector::connect(&self.server_addr))));
self.set_stream(try!(Connector::connect(&server_addr, ssl_context)));
self.get_reply()
}
@@ -101,12 +129,7 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
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`
/// Sends a EHLO command
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
self.command(&format!("EHLO {}", hostname))
}
@@ -162,15 +185,32 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
self.command("RSET")
}
/// Sends an AUTH command with PLAIN mecanism
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
self.command(&format!("AUTH PLAIN {}", plain(username, password)))
/// Sends an AUTH command with the given mechanism
pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> SmtpResult {
if mechanism.supports_initial_response() {
self.command(&format!("AUTH {} {}",
mechanism,
try!(mechanism.response(username, password, None))))
} else {
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
Some(challenge) => challenge,
None => return Err(Error::ResponseParsing("Could not read CRAM challenge")),
};
debug!("CRAM challenge: {}", encoded_challenge);
let cram_response = try!(mechanism.response(username,
password,
Some(&encoded_challenge)));
self.command(&cram_response.clone())
}
}
/// Sends an AUTH command with CRAM-MD5 mecanism
pub fn auth_cram_md5(&mut self, username: &str, password: &str) -> SmtpResult {
let encoded_challenge = try!(self.command("AUTH CRAM-MD5")).first_word().expect("No challenge");
self.command(&format!("AUTH CRAM-MD5 {}", cram_md5(username, password, &encoded_challenge)))
/// Sends a STARTTLS command
pub fn starttls(&mut self) -> SmtpResult {
self.command("STARTTLS")
}
/// Sends the message content
@@ -194,45 +234,33 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
/// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult {
let mut parser = ResponseParser::default();
let mut line = String::new();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
// If the string is too short to be a response code
if line.len() < 3 {
return Err(From::from("Could not parse reply code, line too short"));
debug!("Read: {}", escape_crlf(line.as_ref()));
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 (severity, category, detail) = match (line[0..1].parse::<Severity>(), line[1..2].parse::<Category>(), line[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => (severity, category, detail),
_ => return Err(From::from("Could not parse reply code")),
};
let response = try!(parser.response());
let mut message = Vec::new();
// 3 chars for code + space + CRLF
while line.len() > 6 {
let end = line.len() - 2;
message.push(line[4..end].to_string());
if line.as_bytes()[3] == '-' as u8 {
line.clear();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
} else {
line.clear();
}
if response.is_positive() {
Ok(response)
} else {
Err(From::from(response))
}
let response = Response::new(severity, category, detail, message);
match response.is_positive() {
true => Ok(response),
false => Err(From::from(response)),
}
}
}
#[cfg(test)]
mod test {
use super::{escape_dot, escape_crlf};
use super::{escape_crlf, escape_dot, remove_crlf};
#[test]
fn test_escape_dot() {
@@ -242,13 +270,19 @@ mod test {
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>"
);
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
}
}

View File

@@ -0,0 +1,108 @@
//! A trait to represent a stream
use openssl::ssl::{Ssl, SslContext, SslStream};
use std::fmt;
use std::fmt::{Debug, Formatter};
use std::io;
use std::io::{ErrorKind, Read, Write};
use std::net::{SocketAddr, TcpStream};
/// A trait for the concept of opening a stream
pub trait Connector: Sized {
/// Opens a connection to the given IP socket
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<Self>;
/// Upgrades to TLS connection
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()>;
/// Is the NetworkStream encrypted
fn is_encrypted(&self) -> bool;
}
impl Connector for NetworkStream {
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<NetworkStream> {
let tcp_stream = try!(TcpStream::connect(addr));
match ssl_context {
Some(context) => {
match Ssl::new(context) {
Ok(ssl) => {
ssl.connect(tcp_stream)
.map(|s| NetworkStream::Ssl(s))
.map_err(|e| io::Error::new(ErrorKind::Other, e))
}
Err(e) => Err(io::Error::new(ErrorKind::Other, e)),
}
}
None => Ok(NetworkStream::Plain(tcp_stream)),
}
}
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
*self = match *self {
NetworkStream::Plain(ref mut stream) => {
match Ssl::new(ssl_context) {
Ok(ssl) => {
match ssl.connect(stream.try_clone().unwrap()) {
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
}
}
Err(e) => return Err(io::Error::new(ErrorKind::Other, e)),
}
}
NetworkStream::Ssl(_) => return Ok(()),
};
Ok(())
}
fn is_encrypted(&self) -> bool {
match *self {
NetworkStream::Plain(_) => false,
NetworkStream::Ssl(_) => true,
}
}
}
/// Represents the different types of underlying network streams
pub enum NetworkStream {
/// Plain TCP
Plain(TcpStream),
/// SSL over TCP
Ssl(SslStream<TcpStream>),
}
impl Debug for NetworkStream {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NetworkStream(_)")
}
}
impl Read for NetworkStream {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.read(buf),
NetworkStream::Ssl(ref mut stream) => stream.read(buf),
}
}
}
impl Write for NetworkStream {
#[inline]
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.write(msg),
NetworkStream::Ssl(ref mut stream) => stream.write(msg),
}
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
match *self {
NetworkStream::Plain(ref mut stream) => stream.flush(),
NetworkStream::Ssl(ref mut stream) => stream.flush(),
}
}
}

View File

@@ -0,0 +1,85 @@
//! Error and result type for SMTP clients
use rustc_serialize::base64::FromBase64Error;
use self::Error::*;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use transport::smtp::response::{Response, Severity};
/// An enum of all error kinds.
#[derive(Debug)]
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(FromBase64Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
Resolution,
/// IO error
Io(io::Error),
}
impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl StdError for Error {
fn description(&self) -> &str {
match *self {
Transient(_) => "a transient error occured during the SMTP transaction",
Permanent(_) => "a permanent error occured during the SMTP transaction",
ResponseParsing(_) => "an error occured while parsing an SMTP response",
ChallengeParsing(_) => "an error occured while parsing a CRAM-MD5 challenge",
Resolution => "could not resolve hostname",
Client(_) => "an unknown error occured",
Io(_) => "an I/O error occured",
}
}
fn cause(&self) -> Option<&StdError> {
match *self {
Io(ref err) => Some(&*err as &StdError),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Io(err)
}
}
impl From<Response> for Error {
fn from(response: Response) -> Error {
match response.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,216 @@
//! ESMTP features
use std::collections::HashSet;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::result::Result;
use transport::smtp::authentication::Mechanism;
use transport::smtp::error::Error;
use transport::smtp::response::Response;
/// 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 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)]
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::ResponseParsing("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();
match splitted[0] {
"8BITMIME" => {
features.insert(Extension::EightBitMime);
}
"SMTPUTF8" => {
features.insert(Extension::SmtpUtfEight);
}
"STARTTLS" => {
features.insert(Extension::StartTls);
}
"AUTH" => {
for &mechanism in &splitted[1..] {
match mechanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
}
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mechanism::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_mechanism(&self, mechanism: Mechanism) -> bool {
self.features.contains(&Extension::Authentication(mechanism))
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::{Extension, ServerInfo};
use transport::smtp::authentication::Mechanism;
use transport::smtp::response::{Category, Code, Response, Severity};
#[test]
fn test_extension_fmt() {
assert_eq!(format!("{}", Extension::EightBitMime),
"8BITMIME".to_string());
assert_eq!(format!("{}", Extension::Authentication(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, 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_mechanism(Mechanism::CramMd5));
let response2 =
Response::new(Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
vec!["me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]);
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mechanism::Plain)));
assert!(features2.insert(Extension::Authentication(Mechanism::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_mechanism(Mechanism::Plain));
assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
assert!(!server_info2.supports_feature(&Extension::StartTls));
}
}

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

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

View File

@@ -0,0 +1,584 @@
//! SMTP response, containing a mandatory return code and an optional text
//! message
use self::Category::*;
use self::Severity::*;
use std::fmt::{Display, Formatter, Result};
use std::result;
use std::str::FromStr;
use transport::smtp::error::{Error, SmtpResult};
/// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Severity {
/// 2yx
PositiveCompletion,
/// 3yz
PositiveIntermediate,
/// 4yz
TransientNegativeCompletion,
/// 5yz
PermanentNegativeCompletion,
}
impl FromStr for Severity {
type Err = Error;
fn from_str(s: &str) -> result::Result<Severity, Error> {
match s {
"2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err(Error::ResponseParsing("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) -> result::Result<Category, Error> {
match s {
"0" => Ok(Syntax),
"1" => Ok(Information),
"2" => Ok(Connections),
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"5" => Ok(MailSystem),
_ => Err(Error::ResponseParsing("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) -> result::Result<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,
})
}
_ => Err(Error::ResponseParsing("Could not parse response code")),
}
} else {
Err(Error::ResponseParsing("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,Default)]
pub struct ResponseParser {
/// Response code
code: Option<Code>,
/// Server response string (optional)
/// Handle multiline responses
message: Vec<String>,
}
impl ResponseParser {
/// Parses a line and return a `bool` indicating if there are more lines to come
pub fn read_line(&mut self, line: &str) -> result::Result<bool, Error> {
if line.len() < 3 {
return Err(Error::ResponseParsing("Wrong code length (should be 3 digit)"));
}
match self.code {
Some(ref code) => {
if code.code() != line[0..3] {
return Err(Error::ResponseParsing("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());
Ok(line.as_bytes()[3] == b'-')
} 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::ResponseParsing("Incomplete response, could not read response \
code"))
}
}
}
}
/// Contains an SMTP reply, with separated 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> {
if self.message.is_empty() {
None
} else {
match self.message[0].split_whitespace().next() {
Some(word) => Some(word.to_string()),
None => None,
}
}
}
}
#[cfg(test)]
mod test {
use super::{Category, Code, Response, ResponseParser, Severity};
#[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::default();
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

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

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

@@ -0,0 +1,28 @@
//! This transport is a stub that only logs the message, and always returns
//! success
use email::SendableEmail;
use transport::EmailTransport;
pub mod error;
/// This transport does nothing except logging the message envelope
pub struct StubEmailTransport;
/// SMTP result type
pub type StubResult = Result<(), error::Error>;
impl EmailTransport<StubResult> for StubEmailTransport {
fn send<T: SendableEmail>(&mut self, email: T) -> StubResult {
info!("{}: from=<{}> to=<{:?}>",
email.message_id(),
email.from_address(),
email.to_addresses());
Ok(())
}
fn close(&mut self) {
()
}
}

View File

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

37
tests/transport_file.rs Normal file
View File

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

19
tests/transport_smtp.rs Normal file
View File

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

19
tests/transport_stub.rs Normal file
View File

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