Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f911dce12 | ||
|
|
47d6870d93 | ||
|
|
51de392086 | ||
|
|
fefb5f7978 | ||
|
|
5d125bdbdb | ||
|
|
a1bf0170db | ||
|
|
5bedba4b24 | ||
|
|
813f09a314 | ||
|
|
6a6023431b | ||
|
|
7f6aa0ffae | ||
|
|
1830f084c0 | ||
|
|
49a995f68d | ||
|
|
2fd5147a0f | ||
|
|
9c34b5a055 | ||
|
|
8d60068831 | ||
|
|
c2506b47fc | ||
|
|
cecc11f74e | ||
|
|
5ecdabbce7 | ||
|
|
bd4680d793 | ||
|
|
56f93bd614 | ||
|
|
c9a657ac44 | ||
|
|
df8c8a18a8 | ||
|
|
e30e96c1ca | ||
|
|
ef8c426cd4 | ||
|
|
0b7e004ac8 | ||
|
|
599cf6c313 | ||
|
|
e6b46faa06 | ||
|
|
75de338409 | ||
|
|
29c9b7661e | ||
|
|
d46bbeebf0 | ||
|
|
ae3fc78e67 | ||
|
|
26dae6ecbd | ||
|
|
e855e37182 | ||
|
|
3195959de8 | ||
|
|
5b70dccfd3 | ||
|
|
3b78455b22 | ||
|
|
cbc9764626 | ||
|
|
8811b32113 | ||
|
|
0b185352f8 | ||
|
|
72390e9bdb | ||
|
|
520db5d21d | ||
|
|
bb0c9c5c5c | ||
|
|
262f3b8d93 | ||
|
|
d1b6b33b44 | ||
|
|
4f249b319c | ||
|
|
2281b9046e | ||
|
|
a4ada1b6ef | ||
|
|
8da3a09140 | ||
|
|
6ea49d770c | ||
|
|
6a5b100098 | ||
|
|
5ed452919c | ||
|
|
30583ef1a6 | ||
|
|
60356c3b52 | ||
|
|
ed06c67258 | ||
|
|
ee8da8196a | ||
|
|
78e88d5bc6 | ||
|
|
8f49ba3b17 | ||
|
|
6d19f80842 | ||
|
|
02a9093bad | ||
|
|
d59672ef26 | ||
|
|
7f9b2c5150 |
23
.appveyor.yml
Normal file
23
.appveyor.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
environment:
|
||||||
|
CARGO_TARGET: x86_64-pc-windows-gnu
|
||||||
|
matrix:
|
||||||
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
|
- TARGET: i686-pc-windows-gnu
|
||||||
|
install:
|
||||||
|
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rustc-nightly-${env:TARGET}.tar.gz"
|
||||||
|
- ps: Start-FileDownload "https://static.rust-lang.org/cargo-dist/cargo-nightly-${env:CARGO_TARGET}.tar.gz"
|
||||||
|
- 7z x rustc-nightly-%TARGET%.tar.gz > nul
|
||||||
|
- 7z x rustc-nightly-%TARGET%.tar > nul
|
||||||
|
- 7z x cargo-nightly-%CARGO_TARGET%.tar.gz > nul
|
||||||
|
- 7z x cargo-nightly-%CARGO_TARGET%.tar > nul
|
||||||
|
- call "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" amd64
|
||||||
|
- set PATH=%PATH%;%cd%/rustc-nightly-%TARGET%/rustc/bin
|
||||||
|
- set PATH=%PATH%;%cd%/cargo-nightly-%CARGO_TARGET%/cargo/bin
|
||||||
|
- SET PATH=%PATH%;C:\MinGW\bin
|
||||||
|
- rustc -V
|
||||||
|
- cargo -V
|
||||||
|
|
||||||
|
build: false
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- cargo test --verbose --no-default-features
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
|
.project
|
||||||
/target/
|
/target/
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
|||||||
25
.travis.yml
25
.travis.yml
@@ -1,14 +1,21 @@
|
|||||||
language: rust
|
language: rust
|
||||||
|
sudo: required
|
||||||
|
|
||||||
after_success: |
|
rust:
|
||||||
[ $TRAVIS_BRANCH = master ] &&
|
- stable
|
||||||
[ $TRAVIS_PULL_REQUEST = false ] &&
|
- beta
|
||||||
cargo doc &&
|
- nightly
|
||||||
echo '<meta http-equiv=refresh content=0;url=smtp/index.html>' > target/doc/index.html &&
|
before_script:
|
||||||
sudo pip install ghp-import &&
|
- pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH
|
||||||
ghp-import -n target/doc &&
|
script:
|
||||||
git push -fq https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
- |
|
||||||
|
travis-cargo build &&
|
||||||
|
travis-cargo test &&
|
||||||
|
travis-cargo doc
|
||||||
|
after_success:
|
||||||
|
- travis-cargo --only stable doc-upload
|
||||||
|
- travis-cargo --only stable coveralls
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
secure: m8BXCWDfBh0Hkajp7+KdSFP65q3gUuJWJSb+W6BtAzWNxIuAwTgtm3Ja3gwwlko+XSOO4IG8+p+ApnduK1znWO+mslhUC7qr8oxIWRS3CzvSr3YeEwfVbWOz34/+I5cGh91ie8pbqxP+buMPVj078W3Cr/STzHhS2ZdQpjR/L8c=
|
secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc="
|
||||||
|
|||||||
@@ -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.
|
|
||||||
29
Cargo.toml
29
Cargo.toml
@@ -1,20 +1,27 @@
|
|||||||
[package]
|
[package]
|
||||||
|
|
||||||
name = "smtp"
|
name = "smtp"
|
||||||
version = "0.0.9"
|
version = "0.3.0"
|
||||||
description = "Simple SMTP client"
|
description = "Simple SMTP client"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
documentation = "http://amousset.github.io/rust-smtp/smtp/"
|
documentation = "http://amousset.me/rust-smtp/smtp/"
|
||||||
repository = "https://github.com/amousset/rust-smtp"
|
repository = "https://github.com/amousset/rust-smtp"
|
||||||
homepage = "https://github.com/amousset/rust-smtp"
|
license = "MIT"
|
||||||
license = "MIT/Apache-2.0"
|
authors = ["Alexis Mousset <alexis.mousset@gmx.fr>"]
|
||||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
|
||||||
keywords = ["email", "smtp", "mailer"]
|
keywords = ["email", "smtp", "mailer"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
time = "*"
|
time = "0.1"
|
||||||
uuid = "*"
|
uuid = "0.1"
|
||||||
log = "*"
|
log = "0.3"
|
||||||
env_logger = "*"
|
rustc-serialize = "0.3"
|
||||||
rustc-serialize = "*"
|
rust-crypto = "0.2"
|
||||||
rust-crypto = "*"
|
bufstream = "0.1"
|
||||||
|
email = "0.0"
|
||||||
|
openssl = "0.6"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.3"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
unstable = []
|
||||||
|
|||||||
201
LICENSE-APACHE
201
LICENSE-APACHE
@@ -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.
|
|
||||||
41
README.md
41
README.md
@@ -1,14 +1,9 @@
|
|||||||
rust-smtp [](https://travis-ci.org/amousset/rust-smtp)
|
rust-smtp [](https://travis-ci.org/amousset/rust-smtp) [](https://coveralls.io/github/amousset/rust-smtp?branch=master) [](https://crates.io/crates/smtp) [](./LICENSE)
|
||||||
=========
|
=========
|
||||||
|
|
||||||
This library implements a simple SMTP client.
|
This library implements a simple SMTP client.
|
||||||
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information.
|
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information.
|
||||||
|
|
||||||
Rust versions
|
|
||||||
-------------
|
|
||||||
|
|
||||||
This library is designed for Rust 1.0.0-nightly (master).
|
|
||||||
|
|
||||||
Install
|
Install
|
||||||
-------
|
-------
|
||||||
|
|
||||||
@@ -16,40 +11,12 @@ To use this library, add the following to your `Cargo.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
smtp = "*"
|
smtp = "0.2"
|
||||||
```
|
```
|
||||||
|
|
||||||
Otherwise, you can clone this repository and run `cargo build`.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
|
|
||||||
There is an example command-line program included:
|
|
||||||
```sh
|
|
||||||
$ cargo test
|
|
||||||
$ env RUST_LOG=info cargo run --example client -- -s "My subject" -r sender@localhost recipient@localhost < email.txt
|
|
||||||
INFO:smtp::sender: connection established to 127.0.0.1:25
|
|
||||||
INFO:smtp::sender: 1d0467fb21b2454f90a85dd1e0eda839: from=<sender@localhost>
|
|
||||||
INFO:smtp::sender: 1d0467fb21b2454f90a85dd1e0eda839: to=<recipient@localhost>
|
|
||||||
INFO:smtp::sender: 1d0467fb21b2454f90a85dd1e0eda839: conn_use=1, size=1889, status=sent (2.0.0 Ok: queued as BAA9C1C0055)
|
|
||||||
INFO:client: Email sent successfully
|
|
||||||
```
|
|
||||||
|
|
||||||
Run `cargo run --example client -- -h` to get a list of available options.
|
|
||||||
|
|
||||||
Tests
|
|
||||||
-----
|
|
||||||
|
|
||||||
You can build and run the tests with `cargo test`.
|
|
||||||
|
|
||||||
Documentation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
You can build the documentation with `cargo doc`. It is also available on [GitHub pages](http://amousset.github.io/rust-smtp/smtp/).
|
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
|
|
||||||
This program is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
|
This program is distributed under the terms of the MIT license.
|
||||||
|
|
||||||
See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.
|
See LICENSE for details.
|
||||||
|
|||||||
@@ -1,156 +1,54 @@
|
|||||||
// 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.
|
|
||||||
|
|
||||||
#![feature(core, old_io, rustc_private, collections)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
extern crate env_logger;
|
extern crate env_logger;
|
||||||
extern crate smtp;
|
extern crate smtp;
|
||||||
extern crate getopts;
|
|
||||||
|
|
||||||
use std::old_io::stdin;
|
use std::sync::{Arc, Mutex};
|
||||||
use std::string::String;
|
use std::thread;
|
||||||
use std::env;
|
|
||||||
use getopts::{optopt, optflag, getopts, OptGroup, usage};
|
|
||||||
use std::net::TcpStream;
|
|
||||||
|
|
||||||
use smtp::sender::{Sender, SenderBuilder};
|
use smtp::sender::SenderBuilder;
|
||||||
use smtp::error::SmtpResult;
|
use smtp::email::EmailBuilder;
|
||||||
use smtp::mailer::EmailBuilder;
|
|
||||||
|
|
||||||
fn sendmail(source_address: String, recipient_addresses: Vec<String>, message: String, subject: String,
|
|
||||||
server: String, port: u16, my_hostname: String, number: u16) -> SmtpResult {
|
|
||||||
|
|
||||||
let mut email_builder = EmailBuilder::new();
|
|
||||||
for destination in recipient_addresses.iter() {
|
|
||||||
email_builder = email_builder.to(destination.as_slice());
|
|
||||||
}
|
|
||||||
let email = email_builder.from(source_address.as_slice())
|
|
||||||
.body(message.as_slice())
|
|
||||||
.subject(subject.as_slice())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let mut sender: Sender<TcpStream> = SenderBuilder::new((server.as_slice(), port)).hello_name(my_hostname.as_slice())
|
|
||||||
.enable_connection_reuse(true).build();
|
|
||||||
|
|
||||||
for _ in (1..number) {
|
|
||||||
let _ = sender.send(email.clone());
|
|
||||||
}
|
|
||||||
let result = sender.send(email);
|
|
||||||
sender.close();
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_usage(description: String, _opts: &[OptGroup]) {
|
|
||||||
println!("{}", usage(description.as_slice(), _opts));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::init().unwrap();
|
env_logger::init().unwrap();
|
||||||
|
|
||||||
let args = env::args();
|
let sender = Arc::new(Mutex::new(SenderBuilder::localhost().unwrap().hello_name("localhost")
|
||||||
|
.enable_connection_reuse(true).build()));
|
||||||
|
|
||||||
let mut args_string = Vec::new();
|
let mut threads = Vec::new();
|
||||||
for arg in args {
|
for _ in 1..5 {
|
||||||
args_string.push(arg.clone());
|
|
||||||
};
|
|
||||||
|
|
||||||
let program = args_string[0].clone();
|
let th_sender = sender.clone();
|
||||||
let description = format!("Usage: {0} [options...] recipients\n\n\
|
threads.push(thread::spawn(move || {
|
||||||
This program reads a message on standard input until it reaches\
|
|
||||||
EOF, then tries to send it using the given paramters.\n\n\
|
|
||||||
Example: {0} -r user@example.org user@example.com < message.txt",
|
|
||||||
program);
|
|
||||||
|
|
||||||
let opts = [
|
let email = EmailBuilder::new()
|
||||||
optopt("n", "number", "set the number of emails to send", "NUMBER"),
|
.to("user@localhost")
|
||||||
optopt("s", "subject", "set the email subject", "SUBJECT"),
|
.from("user@localhost")
|
||||||
optopt("r", "reverse-path", "set the sender address", "FROM_ADDRESS"),
|
.body("Hello World!")
|
||||||
optopt("p", "port", "set the port to use, default is 25", "PORT"),
|
.subject("Hello")
|
||||||
optopt("a", "server", "set the server to use, default is localhost", "SERVER"),
|
.build();
|
||||||
optopt("m", "my-hostname", "set the hostname used by the client", "MY_HOSTNAME"),
|
|
||||||
optflag("h", "help", "print this help menu"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let matches = match getopts(args_string.tail(), &opts) {
|
let _ = th_sender.lock().unwrap().send(email);
|
||||||
Ok(m) => m,
|
}));
|
||||||
Err(f) => panic!("{}", f),
|
|
||||||
};
|
|
||||||
|
|
||||||
if matches.opt_present("h") {
|
|
||||||
print_usage(description, &opts);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matches.opt_present("r") {
|
for thread in threads {
|
||||||
println!("The sender option is required");
|
let _ = thread.join();
|
||||||
print_usage(program, &opts);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let recipients_str: &str = if !matches.free.is_empty() {
|
let email = EmailBuilder::new()
|
||||||
matches.free[0].as_slice()
|
.to("user@localhost")
|
||||||
} else {
|
.from("user@localhost")
|
||||||
print_usage(description, &opts);
|
.body("Hello World!")
|
||||||
return;
|
.subject("Hello Bis")
|
||||||
};
|
.build();
|
||||||
|
|
||||||
let mut recipients = Vec::new();
|
let mut sender = sender.lock().unwrap();
|
||||||
for recipient in recipients_str.split(' ') {
|
let result = sender.send(email);
|
||||||
recipients.push(recipient.to_string());
|
sender.close();
|
||||||
}
|
|
||||||
|
|
||||||
let mut message = String::new();
|
match result {
|
||||||
|
|
||||||
let mut line = stdin().read_line();
|
|
||||||
while line.is_ok() {
|
|
||||||
message.push_str(line.unwrap().as_slice());
|
|
||||||
line = stdin().read_line();
|
|
||||||
}
|
|
||||||
|
|
||||||
match sendmail(
|
|
||||||
// sender
|
|
||||||
matches.opt_str("r").unwrap().clone(),
|
|
||||||
// recipients
|
|
||||||
recipients,
|
|
||||||
// message content
|
|
||||||
message,
|
|
||||||
// subject
|
|
||||||
match matches.opt_str("s") {
|
|
||||||
Some(ref subject) => subject.clone(),
|
|
||||||
None => "(empty subject)".to_string(),
|
|
||||||
},
|
|
||||||
// server
|
|
||||||
match matches.opt_str("a") {
|
|
||||||
Some(ref server) => server.clone(),
|
|
||||||
None => "localhost".to_string(),
|
|
||||||
},
|
|
||||||
// port
|
|
||||||
match matches.opt_str("p") {
|
|
||||||
Some(port) => port.as_slice().parse::<u16>().unwrap(),
|
|
||||||
None => 25,
|
|
||||||
},
|
|
||||||
// my hostname
|
|
||||||
match matches.opt_str("m") {
|
|
||||||
Some(ref my_hostname) => my_hostname.clone(),
|
|
||||||
None => "localhost".to_string(),
|
|
||||||
},
|
|
||||||
// number of copies
|
|
||||||
match matches.opt_str("n") {
|
|
||||||
Some(ref n) => n.as_slice().parse::<u16>().unwrap(),
|
|
||||||
None => 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
{
|
|
||||||
Ok(..) => info!("Email sent successfully"),
|
Ok(..) => info!("Email sent successfully"),
|
||||||
Err(error) => error!("{}", error),
|
Err(error) => error!("{:?}", error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
109
src/authentication.rs
Normal file
109
src/authentication.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
//! Provides authentication mecanisms
|
||||||
|
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use serialize::base64::{self, ToBase64, FromBase64};
|
||||||
|
use serialize::hex::ToHex;
|
||||||
|
use crypto::hmac::Hmac;
|
||||||
|
use crypto::md5::Md5;
|
||||||
|
use crypto::mac::Mac;
|
||||||
|
|
||||||
|
use NUL;
|
||||||
|
use error::Error;
|
||||||
|
|
||||||
|
/// Represents authentication mecanisms
|
||||||
|
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
|
||||||
|
pub enum Mecanism {
|
||||||
|
/// PLAIN authentication mecanism
|
||||||
|
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||||
|
Plain,
|
||||||
|
/// CRAM-MD5 authentication mecanism
|
||||||
|
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||||
|
CramMd5,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Mecanism {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
write!(f,
|
||||||
|
"{}",
|
||||||
|
match *self {
|
||||||
|
Mecanism::Plain => "PLAIN",
|
||||||
|
Mecanism::CramMd5 => "CRAM-MD5",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mecanism {
|
||||||
|
/// Does the mecanism supports initial response
|
||||||
|
pub fn supports_initial_response(&self) -> bool {
|
||||||
|
match *self {
|
||||||
|
Mecanism::Plain => true,
|
||||||
|
Mecanism::CramMd5 => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the string to send to the server, using the provided username, password and
|
||||||
|
/// challenge in some cases
|
||||||
|
pub fn response(&self,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
challenge: Option<&str>)
|
||||||
|
-> Result<String, Error> {
|
||||||
|
match *self {
|
||||||
|
Mecanism::Plain => {
|
||||||
|
match challenge {
|
||||||
|
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")),
|
||||||
|
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
|
||||||
|
.as_bytes()
|
||||||
|
.to_base64(base64::STANDARD)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mecanism::CramMd5 => {
|
||||||
|
let encoded_challenge = match challenge {
|
||||||
|
Some(challenge) => challenge,
|
||||||
|
None => return Err(Error::ClientError("This mecanism does expect a challenge")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let decoded_challenge = match encoded_challenge.from_base64() {
|
||||||
|
Ok(challenge) => challenge,
|
||||||
|
Err(error) => return Err(Error::ChallengeParsingError(error)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
||||||
|
hmac.input(&decoded_challenge);
|
||||||
|
|
||||||
|
Ok(format!("{} {}", username, hmac.result().code().to_hex())
|
||||||
|
.as_bytes()
|
||||||
|
.to_base64(base64::STANDARD))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::Mecanism;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plain() {
|
||||||
|
let mecanism = Mecanism::Plain;
|
||||||
|
|
||||||
|
assert_eq!(mecanism.response("username", "password", None).unwrap(),
|
||||||
|
"AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||||
|
assert!(mecanism.response("username", "password", Some("test")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cram_md5() {
|
||||||
|
let mecanism = Mecanism::CramMd5;
|
||||||
|
|
||||||
|
assert_eq!(mecanism.response("alice",
|
||||||
|
"wonderland",
|
||||||
|
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
|
||||||
|
.unwrap(),
|
||||||
|
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||||
|
assert!(mecanism.response("alice", "wonderland", Some("tést")).is_err());
|
||||||
|
assert!(mecanism.response("alice", "wonderland", None).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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").as_slice(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cram_md5() {
|
|
||||||
assert_eq!(cram_md5("alice", "wonderland",
|
|
||||||
"PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==").as_slice(),
|
|
||||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +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 connected stream
|
|
||||||
|
|
||||||
use std::io;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::net::TcpStream;
|
|
||||||
|
|
||||||
/// A trait for the concept of opening a stream
|
|
||||||
pub trait Connecter {
|
|
||||||
/// Opens a connection to the given IP socket
|
|
||||||
fn connect(addr: &SocketAddr) -> io::Result<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Connecter for TcpStream {
|
|
||||||
fn connect(addr: &SocketAddr) -> io::Result<TcpStream> {
|
|
||||||
TcpStream::connect(addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,21 @@
|
|||||||
// 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
|
//! SMTP client
|
||||||
|
|
||||||
use std::string::String;
|
use std::string::String;
|
||||||
use std::error::FromError;
|
use std::net::ToSocketAddrs;
|
||||||
use std::net::TcpStream;
|
use std::io::{BufRead, Read, Write};
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::io;
|
||||||
use std::io::{BufRead, BufStream, Read, Write};
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use response::{Response, Severity, Category};
|
use bufstream::BufStream;
|
||||||
use error::SmtpResult;
|
use openssl::ssl::SslContext;
|
||||||
use client::connecter::Connecter;
|
|
||||||
use client::authentication::{plain, cram_md5};
|
use response::ResponseParser;
|
||||||
|
use authentication::Mecanism;
|
||||||
|
use error::{Error, SmtpResult};
|
||||||
|
use client::net::{Connector, NetworkStream};
|
||||||
use {CRLF, MESSAGE_ENDING};
|
use {CRLF, MESSAGE_ENDING};
|
||||||
|
|
||||||
pub mod connecter;
|
pub mod net;
|
||||||
mod authentication;
|
|
||||||
|
|
||||||
/// Returns the string after adding a dot at the beginning of each line starting with a dot
|
/// Returns the string after adding a dot at the beginning of each line starting with a dot
|
||||||
///
|
///
|
||||||
@@ -33,7 +26,8 @@ fn escape_dot(string: &str) -> String {
|
|||||||
format!(".{}", string)
|
format!(".{}", string)
|
||||||
} else {
|
} else {
|
||||||
string.to_string()
|
string.to_string()
|
||||||
}.replace("\r.", "\r..")
|
}
|
||||||
|
.replace("\r.", "\r..")
|
||||||
.replace("\n.", "\n..")
|
.replace("\n.", "\n..")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,49 +37,72 @@ fn escape_crlf(string: &str) -> String {
|
|||||||
string.replace(CRLF, "<CR><LF>")
|
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
|
/// Structure that implements the SMTP client
|
||||||
pub struct Client<S = TcpStream> {
|
pub struct Client<S: Write + Read = NetworkStream> {
|
||||||
/// TCP stream between client and server
|
/// TCP stream between client and server
|
||||||
/// Value is None before connection
|
/// Value is None before connection
|
||||||
stream: Option<BufStream<S>>,
|
stream: Option<BufStream<S>>,
|
||||||
/// Socket we are connecting to
|
|
||||||
server_addr: SocketAddr,
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! return_err (
|
macro_rules! return_err (
|
||||||
($err: expr, $client: ident) => ({
|
($err: expr, $client: ident) => ({
|
||||||
return Err(FromError::from_error($err))
|
return Err(From::from($err))
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
impl<S = TcpStream> Client<S> {
|
impl<S: Write + Read = NetworkStream> Client<S> {
|
||||||
/// Creates a new SMTP client
|
/// Creates a new SMTP client
|
||||||
///
|
///
|
||||||
/// It does not connects to the server, but only creates the `Client`
|
/// It does not connects to the server, but only creates the `Client`
|
||||||
pub fn new<A: ToSocketAddrs>(addr: A) -> Client<S> {
|
pub fn new() -> Client<S> {
|
||||||
Client{
|
Client { stream: None }
|
||||||
stream: None,
|
|
||||||
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Connecter + Write + Read = TcpStream> Client<S> {
|
impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||||
/// Closes the SMTP transaction if possible
|
/// Closes the SMTP transaction if possible
|
||||||
pub fn close(&mut self) {
|
pub fn close(&mut self) {
|
||||||
let _ = self.quit();
|
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<()> {
|
||||||
|
//let current_stream = self.stream.clone();
|
||||||
|
if self.stream.is_some() {
|
||||||
|
self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connects to the configured server
|
/// Connects to the configured server
|
||||||
pub fn connect(&mut self) -> SmtpResult {
|
pub fn connect<A: ToSocketAddrs>(&mut self, addr: &A) -> SmtpResult {
|
||||||
// Connect should not be called when the client is already connected
|
// Connect should not be called when the client is already connected
|
||||||
if self.stream.is_some() {
|
if self.stream.is_some() {
|
||||||
return_err!("The connection is already established", self);
|
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),
|
||||||
|
};
|
||||||
|
|
||||||
// Try to connect
|
// Try to connect
|
||||||
self.stream = Some(BufStream::new(try!(Connecter::connect(&self.server_addr))));
|
self.set_stream(try!(Connector::connect(&server_addr, None)));
|
||||||
|
|
||||||
self.get_reply()
|
self.get_reply()
|
||||||
}
|
}
|
||||||
@@ -100,12 +117,7 @@ impl<S: Connecter + Write + Read = TcpStream> Client<S> {
|
|||||||
self.send_server(command, CRLF)
|
self.send_server(command, CRLF)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a HELO command and fills `server_info`
|
/// Sends a EHLO command
|
||||||
pub fn helo(&mut self, hostname: &str) -> SmtpResult {
|
|
||||||
self.command(&format!("HELO {}", hostname))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a EHLO command and fills `server_info`
|
|
||||||
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
|
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
|
||||||
self.command(&format!("EHLO {}", hostname))
|
self.command(&format!("EHLO {}", hostname))
|
||||||
}
|
}
|
||||||
@@ -161,18 +173,35 @@ impl<S: Connecter + Write + Read = TcpStream> Client<S> {
|
|||||||
self.command("RSET")
|
self.command("RSET")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an AUTH command with PLAIN mecanism
|
/// Sends an AUTH command with the given mecanism
|
||||||
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
|
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
|
||||||
self.command(&format!("AUTH PLAIN {}", plain(username, password)))
|
|
||||||
|
if mecanism.supports_initial_response() {
|
||||||
|
self.command(&format!("AUTH {} {}",
|
||||||
|
mecanism,
|
||||||
|
try!(mecanism.response(username, password, None))))
|
||||||
|
} else {
|
||||||
|
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
|
||||||
|
Some(challenge) => challenge,
|
||||||
|
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("CRAM challenge: {}", encoded_challenge);
|
||||||
|
|
||||||
|
let cram_response = try!(mecanism.response(username,
|
||||||
|
password,
|
||||||
|
Some(&encoded_challenge)));
|
||||||
|
|
||||||
|
self.command(&format!("{}", cram_response))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends an AUTH command with CRAM-MD5 mecanism
|
/// Sends a STARTTLS command
|
||||||
pub fn auth_cram_md5(&mut self, username: &str, password: &str) -> SmtpResult {
|
pub fn starttls(&mut self) -> SmtpResult {
|
||||||
let encoded_challenge = try!(self.command("AUTH CRAM-MD5")).first_word().expect("No challenge");
|
self.command("STARTTLS")
|
||||||
self.command(&format!("AUTH CRAM-MD5 {}", cram_md5(username, password, &encoded_challenge)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends the message content and close
|
/// Sends the message content
|
||||||
pub fn message(&mut self, message_content: &str) -> SmtpResult {
|
pub fn message(&mut self, message_content: &str) -> SmtpResult {
|
||||||
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
|
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
|
||||||
}
|
}
|
||||||
@@ -180,7 +209,7 @@ impl<S: Connecter + Write + Read = TcpStream> Client<S> {
|
|||||||
/// Sends a string to the server and gets the response
|
/// Sends a string to the server and gets the response
|
||||||
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
|
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
|
||||||
if self.stream.is_none() {
|
if self.stream.is_none() {
|
||||||
return Err(FromError::from_error("Connection closed"));
|
return Err(From::from("Connection closed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
|
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
|
||||||
@@ -193,61 +222,53 @@ impl<S: Connecter + Write + Read = TcpStream> Client<S> {
|
|||||||
|
|
||||||
/// Gets the SMTP response
|
/// Gets the SMTP response
|
||||||
fn get_reply(&mut self) -> SmtpResult {
|
fn get_reply(&mut self) -> SmtpResult {
|
||||||
|
|
||||||
|
let mut parser = ResponseParser::new();
|
||||||
|
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||||
|
|
||||||
// If the string is too short to be a response code
|
debug!("Read: {}", escape_crlf(line.as_ref()));
|
||||||
if line.len() < 3 {
|
|
||||||
return Err(FromError::from_error("Could not parse reply code, line too short"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (severity, category, detail) = match (line[0..1].parse::<Severity>(), line[1..2].parse::<Category>(), line[2..3].parse::<u8>()) {
|
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
|
||||||
(Ok(severity), Ok(category), Ok(detail)) => (severity, category, detail),
|
|
||||||
_ => return Err(FromError::from_error("Could not parse reply code")),
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
line.clear();
|
||||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||||
} else {
|
|
||||||
line.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = Response::new(severity, category, detail, message);
|
let response = try!(parser.response());
|
||||||
|
|
||||||
match response.is_positive() {
|
match response.is_positive() {
|
||||||
true => Ok(response),
|
true => Ok(response),
|
||||||
false => Err(FromError::from_error(response)),
|
false => Err(From::from(response)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{escape_dot, escape_crlf};
|
use super::{escape_dot, remove_crlf, escape_crlf};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_escape_dot() {
|
fn test_escape_dot() {
|
||||||
assert_eq!(escape_dot(".test").as_slice(), "..test");
|
assert_eq!(escape_dot(".test"), "..test");
|
||||||
assert_eq!(escape_dot("\r.\n.\r\n").as_slice(), "\r..\n..\r\n");
|
assert_eq!(escape_dot("\r.\n.\r\n"), "\r..\n..\r\n");
|
||||||
assert_eq!(escape_dot("test\r\n.test\r\n").as_slice(), "test\r\n..test\r\n");
|
assert_eq!(escape_dot("test\r\n.test\r\n"), "test\r\n..test\r\n");
|
||||||
assert_eq!(escape_dot("test\r\n.\r\ntest").as_slice(), "test\r\n..\r\ntest");
|
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]
|
#[test]
|
||||||
fn test_escape_crlf() {
|
fn test_escape_crlf() {
|
||||||
assert_eq!(escape_crlf("\r\n").as_slice(), "<CR><LF>");
|
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
|
||||||
assert_eq!(escape_crlf("EHLO my_name\r\n").as_slice(), "EHLO my_name<CR><LF>");
|
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
|
||||||
assert_eq!(
|
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||||
escape_crlf("EHLO my_name\r\nSIZE 42\r\n").as_slice(),
|
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
|
||||||
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/client/net.rs
Normal file
94
src/client/net.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
//! A trait to represent a stream
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::io::{Read, Write, ErrorKind};
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::net::TcpStream;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
|
use openssl::ssl::{SslContext, SslStream};
|
||||||
|
|
||||||
|
/// A trait for the concept of opening a stream
|
||||||
|
pub trait Connector {
|
||||||
|
/// 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<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 SslStream::new(&context, tcp_stream) {
|
||||||
|
Ok(stream) => Ok(NetworkStream::Ssl(stream)),
|
||||||
|
Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
|
||||||
|
},
|
||||||
|
None => Ok(NetworkStream::Plain(tcp_stream)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||||
|
*self = match self.clone() {
|
||||||
|
NetworkStream::Plain(stream) => match SslStream::new(ssl_context, stream) {
|
||||||
|
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
|
||||||
|
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||||
|
},
|
||||||
|
NetworkStream::Ssl(stream) => NetworkStream::Ssl(stream),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Represents the different types of underlying network streams
|
||||||
|
pub enum NetworkStream {
|
||||||
|
/// Plain TCP
|
||||||
|
Plain(TcpStream),
|
||||||
|
/// SSL over TCP
|
||||||
|
Ssl(SslStream<TcpStream>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for NetworkStream {
|
||||||
|
#[inline]
|
||||||
|
fn clone(&self) -> NetworkStream {
|
||||||
|
match self {
|
||||||
|
&NetworkStream::Plain(ref stream) => NetworkStream::Plain(stream.try_clone().unwrap()),
|
||||||
|
&NetworkStream::Ssl(ref stream) => NetworkStream::Ssl(stream.try_clone().unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for NetworkStream {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
f.write_str("NetworkStream(_)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
348
src/email.rs
Normal file
348
src/email.rs
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
//! Simple email (very incomplete)
|
||||||
|
|
||||||
|
use std::fmt::{Display, Formatter, Result};
|
||||||
|
|
||||||
|
use email_format::{MimeMessage, Header, Mailbox};
|
||||||
|
use time::{now, Tm};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Converts an adress or an address with an alias to a `Address`
|
||||||
|
pub trait ToHeader {
|
||||||
|
/// Converts to a `Header` struct
|
||||||
|
fn to_header(&self) -> Header;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToHeader for Header {
|
||||||
|
fn to_header(&self) -> Header {
|
||||||
|
(*self).clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ToHeader for (&'a str, &'a str) {
|
||||||
|
fn to_header(&self) -> Header {
|
||||||
|
let (name, value) = *self;
|
||||||
|
Header::new(name.to_string(), value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an adress or an address with an alias to a `Mailbox`
|
||||||
|
pub trait ToMailbox {
|
||||||
|
/// Converts to a `Mailbox` struct
|
||||||
|
fn to_mailbox(&self) -> Mailbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToMailbox for Mailbox {
|
||||||
|
fn to_mailbox(&self) -> Mailbox {
|
||||||
|
(*self).clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ToMailbox for &'a str {
|
||||||
|
fn to_mailbox(&self) -> Mailbox {
|
||||||
|
Mailbox::new(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ToMailbox for (&'a str, &'a str) {
|
||||||
|
fn to_mailbox(&self) -> Mailbox {
|
||||||
|
let (address, alias) = *self;
|
||||||
|
Mailbox::new_with_name(alias.to_string(), address.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds an `Email` structure
|
||||||
|
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||||
|
pub struct EmailBuilder {
|
||||||
|
/// Email content
|
||||||
|
content: Email,
|
||||||
|
/// Date issued
|
||||||
|
date_issued: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple email representation
|
||||||
|
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||||
|
pub struct Email {
|
||||||
|
/// Message
|
||||||
|
message: MimeMessage,
|
||||||
|
/// The enveloppe recipients addresses
|
||||||
|
to: Vec<String>,
|
||||||
|
/// The enveloppe sender address
|
||||||
|
from: Option<String>,
|
||||||
|
/// Message-ID
|
||||||
|
message_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Email {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
write!(f, "{}", self.message.as_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailBuilder {
|
||||||
|
/// Creates a new empty email
|
||||||
|
pub fn new() -> EmailBuilder {
|
||||||
|
let current_message = Uuid::new_v4();
|
||||||
|
|
||||||
|
let mut email = Email {
|
||||||
|
message: MimeMessage::new_blank_message(),
|
||||||
|
to: vec![],
|
||||||
|
from: None,
|
||||||
|
message_id: current_message,
|
||||||
|
};
|
||||||
|
|
||||||
|
match Header::new_with_value("Message-ID".to_string(),
|
||||||
|
format!("<{}@rust-smtp>", current_message)) {
|
||||||
|
Ok(header) => email.message.headers.insert(header),
|
||||||
|
Err(_) => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailBuilder {
|
||||||
|
content: email,
|
||||||
|
date_issued: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the email body
|
||||||
|
pub fn body(mut self, body: &str) -> EmailBuilder {
|
||||||
|
self.content.message.body = body.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a generic header
|
||||||
|
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
|
||||||
|
self.insert_header(header);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_header<A: ToHeader>(&mut self, header: A) {
|
||||||
|
self.content.message.headers.insert(header.to_header());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `From` header and store the sender address
|
||||||
|
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||||
|
let mailbox = address.to_mailbox();
|
||||||
|
self.insert_header(("From", mailbox.to_string().as_ref()));
|
||||||
|
self.content.from = Some(mailbox.address);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `To` header and store the recipient address
|
||||||
|
pub fn to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||||
|
let mailbox = address.to_mailbox();
|
||||||
|
self.insert_header(("To", mailbox.to_string().as_ref()));
|
||||||
|
self.content.to.push(mailbox.address);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `Cc` header and store the recipient address
|
||||||
|
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||||
|
let mailbox = address.to_mailbox();
|
||||||
|
self.insert_header(("Cc", mailbox.to_string().as_ref()));
|
||||||
|
self.content.to.push(mailbox.address);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `Reply-To` header
|
||||||
|
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||||
|
let mailbox = address.to_mailbox();
|
||||||
|
self.insert_header(("Reply-To", mailbox.to_string().as_ref()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `Sender` header
|
||||||
|
pub fn sender<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||||
|
let mailbox = address.to_mailbox();
|
||||||
|
self.insert_header(("Sender", mailbox.to_string().as_ref()));
|
||||||
|
self.content.from = Some(mailbox.address);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `Subject` header
|
||||||
|
pub fn subject(mut self, subject: &str) -> EmailBuilder {
|
||||||
|
self.insert_header(("Subject", subject));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `Date` header with the given date
|
||||||
|
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
||||||
|
self.insert_header(("Date", Tm::rfc822z(date).to_string().as_ref()));
|
||||||
|
self.date_issued = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the Email
|
||||||
|
pub fn build(mut self) -> Email {
|
||||||
|
if !self.date_issued {
|
||||||
|
self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
|
||||||
|
}
|
||||||
|
self.content.message.update_headers();
|
||||||
|
self.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Email sendable by an SMTP client
|
||||||
|
pub trait SendableEmail {
|
||||||
|
/// From address
|
||||||
|
fn from_address(&self) -> Option<String>;
|
||||||
|
/// To addresses
|
||||||
|
fn to_addresses(&self) -> Option<Vec<String>>;
|
||||||
|
/// Message content
|
||||||
|
fn message(&self) -> Option<String>;
|
||||||
|
/// Message ID
|
||||||
|
fn message_id(&self) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimal email structure
|
||||||
|
pub struct SimpleSendableEmail {
|
||||||
|
/// From address
|
||||||
|
from: String,
|
||||||
|
/// To addresses
|
||||||
|
to: Vec<String>,
|
||||||
|
/// Message
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleSendableEmail {
|
||||||
|
/// Returns a new email
|
||||||
|
pub fn new(from_address: &str, to_address: &str, message: &str) -> SimpleSendableEmail {
|
||||||
|
SimpleSendableEmail {
|
||||||
|
from: from_address.to_string(),
|
||||||
|
to: vec![to_address.to_string()],
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendableEmail for SimpleSendableEmail {
|
||||||
|
fn from_address(&self) -> Option<String> {
|
||||||
|
Some(self.from.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_addresses(&self) -> Option<Vec<String>> {
|
||||||
|
Some(self.to.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(&self) -> Option<String> {
|
||||||
|
Some(self.message.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message_id(&self) -> Option<String> {
|
||||||
|
Some(format!("<{}@rust-smtp>", Uuid::new_v4()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendableEmail for Email {
|
||||||
|
/// Return the to addresses, and fails if it is not set
|
||||||
|
fn to_addresses(&self) -> Option<Vec<String>> {
|
||||||
|
if self.to.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.to.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the from address, and fails if it is not set
|
||||||
|
fn from_address(&self) -> Option<String> {
|
||||||
|
match self.from {
|
||||||
|
Some(ref from_address) => Some(from_address.clone()),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message(&self) -> Option<String> {
|
||||||
|
Some(format!("{}", self))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message_id(&self) -> Option<String> {
|
||||||
|
Some(format!("{}", self.message_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use time::now;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
use email_format::{MimeMessage, Header};
|
||||||
|
|
||||||
|
use super::{SendableEmail, EmailBuilder, Email};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_email_display() {
|
||||||
|
let current_message = Uuid::new_v4();
|
||||||
|
|
||||||
|
let mut email = Email {
|
||||||
|
message: MimeMessage::new_blank_message(),
|
||||||
|
to: vec![],
|
||||||
|
from: None,
|
||||||
|
message_id: current_message,
|
||||||
|
};
|
||||||
|
|
||||||
|
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
|
||||||
|
format!("<{}@rust-smtp>",
|
||||||
|
current_message))
|
||||||
|
.unwrap());
|
||||||
|
|
||||||
|
email.message
|
||||||
|
.headers
|
||||||
|
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
|
||||||
|
.unwrap());
|
||||||
|
|
||||||
|
email.message.body = "body".to_string();
|
||||||
|
|
||||||
|
assert_eq!(format!("{}", email),
|
||||||
|
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
|
||||||
|
current_message));
|
||||||
|
assert_eq!(current_message.to_string(), email.message_id().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_email_builder() {
|
||||||
|
let email_builder = EmailBuilder::new();
|
||||||
|
let date_now = now();
|
||||||
|
|
||||||
|
let email = email_builder.to("user@localhost")
|
||||||
|
.from("user@localhost")
|
||||||
|
.cc(("cc@localhost", "Alias"))
|
||||||
|
.reply_to("reply@localhost")
|
||||||
|
.sender("sender@localhost")
|
||||||
|
.body("Hello World!")
|
||||||
|
.date(&date_now)
|
||||||
|
.subject("Hello")
|
||||||
|
.add_header(("X-test", "value"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(format!("{}", email),
|
||||||
|
format!("Message-ID: <{}@rust-smtp>\r\nTo: <user@localhost>\r\nFrom: \
|
||||||
|
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
|
||||||
|
<reply@localhost>\r\nSender: <sender@localhost>\r\nDate: \
|
||||||
|
{}\r\nSubject: Hello\r\nX-test: value\r\n\r\nHello World!\r\n",
|
||||||
|
email.message_id().unwrap(),
|
||||||
|
date_now.rfc822z()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_email_sendable() {
|
||||||
|
let email_builder = EmailBuilder::new();
|
||||||
|
let date_now = now();
|
||||||
|
|
||||||
|
let email = email_builder.to("user@localhost")
|
||||||
|
.from("user@localhost")
|
||||||
|
.cc(("cc@localhost", "Alias"))
|
||||||
|
.reply_to("reply@localhost")
|
||||||
|
.sender("sender@localhost")
|
||||||
|
.body("Hello World!")
|
||||||
|
.date(&date_now)
|
||||||
|
.subject("Hello")
|
||||||
|
.add_header(("X-test", "value"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assert_eq!(email.from_address().unwrap(),
|
||||||
|
"sender@localhost".to_string());
|
||||||
|
assert_eq!(email.to_addresses().unwrap(),
|
||||||
|
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
|
||||||
|
assert_eq!(email.message().unwrap(), format!("{}", email));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
62
src/error.rs
62
src/error.rs
@@ -1,88 +1,88 @@
|
|||||||
// 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
|
//! Error and result type for SMTP clients
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error as StdError;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::error::FromError;
|
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use response::{Severity, Response};
|
use response::{Severity, Response};
|
||||||
use self::SmtpError::*;
|
use serialize::base64::FromBase64Error;
|
||||||
|
use self::Error::*;
|
||||||
|
|
||||||
/// An enum of all error kinds.
|
/// An enum of all error kinds.
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SmtpError {
|
pub enum Error {
|
||||||
/// Transient error, 4xx reply code
|
/// Transient SMTP error, 4xx reply code
|
||||||
///
|
///
|
||||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||||
TransientError(Response),
|
TransientError(Response),
|
||||||
/// Permanent error, 5xx reply code
|
/// Permanent SMTP error, 5xx reply code
|
||||||
///
|
///
|
||||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||||
PermanentError(Response),
|
PermanentError(Response),
|
||||||
/// TODO
|
/// Error parsing a response
|
||||||
ClientError(String),
|
ResponseParsingError(&'static str),
|
||||||
|
/// Error parsing a base64 string in response
|
||||||
|
ChallengeParsingError(FromBase64Error),
|
||||||
|
/// Internal client error
|
||||||
|
ClientError(&'static str),
|
||||||
|
/// DNS resolution error
|
||||||
|
ResolutionError,
|
||||||
/// IO error
|
/// IO error
|
||||||
IoError(io::Error),
|
IoError(io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for SmtpError {
|
impl Display for Error {
|
||||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||||
fmt.write_str(self.description())
|
fmt.write_str(self.description())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for SmtpError {
|
impl StdError for Error {
|
||||||
fn description(&self) -> &str {
|
fn description(&self) -> &str {
|
||||||
match *self {
|
match *self {
|
||||||
TransientError(_) => "a transient error occured during the SMTP transaction",
|
TransientError(_) => "a transient error occured during the SMTP transaction",
|
||||||
PermanentError(_) => "a permanent error occured during the SMTP transaction",
|
PermanentError(_) => "a permanent error occured during the SMTP transaction",
|
||||||
|
ResponseParsingError(_) => "an error occured while parsing an SMTP response",
|
||||||
|
ChallengeParsingError(_) => "an error occured while parsing a CRAM-MD5 challenge",
|
||||||
|
ResolutionError => "Could no resolve hostname",
|
||||||
ClientError(_) => "an unknown error occured",
|
ClientError(_) => "an unknown error occured",
|
||||||
IoError(_) => "an I/O error occured",
|
IoError(_) => "an I/O error occured",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cause(&self) -> Option<&Error> {
|
fn cause(&self) -> Option<&StdError> {
|
||||||
match *self {
|
match *self {
|
||||||
IoError(ref err) => Some(&*err as &Error),
|
IoError(ref err) => Some(&*err as &StdError),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromError<io::Error> for SmtpError {
|
impl From<io::Error> for Error {
|
||||||
fn from_error(err: io::Error) -> SmtpError {
|
fn from(err: io::Error) -> Error {
|
||||||
IoError(err)
|
IoError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromError<Response> for SmtpError {
|
impl From<Response> for Error {
|
||||||
fn from_error(response: Response) -> SmtpError {
|
fn from(response: Response) -> Error {
|
||||||
match response.severity() {
|
match response.severity() {
|
||||||
Severity::TransientNegativeCompletion => TransientError(response),
|
Severity::TransientNegativeCompletion => TransientError(response),
|
||||||
Severity::PermanentNegativeCompletion => PermanentError(response),
|
Severity::PermanentNegativeCompletion => PermanentError(response),
|
||||||
_ => ClientError("Unknown error code".to_string())
|
_ => ClientError("Unknown error code"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromError<&'static str> for SmtpError {
|
impl From<&'static str> for Error {
|
||||||
fn from_error(string: &'static str) -> SmtpError {
|
fn from(string: &'static str) -> Error {
|
||||||
ClientError(string.to_string())
|
ClientError(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SMTP result type
|
/// SMTP result type
|
||||||
pub type SmtpResult = Result<Response, SmtpError>;
|
pub type SmtpResult = Result<Response, Error>;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
|||||||
242
src/extension.rs
242
src/extension.rs
@@ -1,22 +1,16 @@
|
|||||||
// 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
|
//! ESMTP features
|
||||||
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::fmt;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use response::Response;
|
use response::Response;
|
||||||
use self::Extension::*;
|
use error::Error;
|
||||||
|
use authentication::Mecanism;
|
||||||
|
|
||||||
/// Supported ESMTP keywords
|
/// Supported ESMTP keywords
|
||||||
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
|
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
|
||||||
pub enum Extension {
|
pub enum Extension {
|
||||||
/// 8BITMIME keyword
|
/// 8BITMIME keyword
|
||||||
///
|
///
|
||||||
@@ -30,79 +24,197 @@ pub enum Extension {
|
|||||||
///
|
///
|
||||||
/// RFC 2487: https://tools.ietf.org/html/rfc2487
|
/// RFC 2487: https://tools.ietf.org/html/rfc2487
|
||||||
StartTls,
|
StartTls,
|
||||||
/// AUTH PLAIN mecanism
|
/// AUTH mecanism
|
||||||
///
|
Authentication(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 {
|
impl Display for Extension {
|
||||||
fn from_str(s: &str) -> Result<Vec<Extension>, &'static str> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
let splitted : Vec<&str> = s.split(' ').collect();
|
match *self {
|
||||||
match (splitted[0], splitted.len()) {
|
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
|
||||||
("8BITMIME", 1) => Ok(vec![EightBitMime]),
|
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
|
||||||
("SMTPUTF8", 1) => Ok(vec![SmtpUtfEight]),
|
Extension::StartTls => write!(f, "{}", "STARTTLS"),
|
||||||
("STARTTLS", 1) => Ok(vec![StartTls]),
|
Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism),
|
||||||
("AUTH", _) => {
|
}
|
||||||
let mut mecanisms: Vec<Extension> = vec![];
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains information about an SMTP server
|
||||||
|
#[derive(Clone,Debug,Eq,PartialEq)]
|
||||||
|
pub struct ServerInfo {
|
||||||
|
/// Server name
|
||||||
|
///
|
||||||
|
/// The name given in the server banner
|
||||||
|
pub name: String,
|
||||||
|
/// ESMTP features supported by the server
|
||||||
|
///
|
||||||
|
/// It contains the features supported by the server and known by the `Extension` module.
|
||||||
|
pub features: HashSet<Extension>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ServerInfo {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
write!(f,
|
||||||
|
"{} with {}",
|
||||||
|
self.name,
|
||||||
|
match self.features.is_empty() {
|
||||||
|
true => "no supported features".to_string(),
|
||||||
|
false => format!("{:?}", self.features),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerInfo {
|
||||||
|
/// Parses a response to create a `ServerInfo`
|
||||||
|
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||||
|
let name = match response.first_word() {
|
||||||
|
Some(name) => name,
|
||||||
|
None => return Err(Error::ResponseParsingError("Could not read server name")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut features: HashSet<Extension> = HashSet::new();
|
||||||
|
|
||||||
|
for line in response.message() {
|
||||||
|
|
||||||
|
let splitted: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
let _ = match splitted[0] {
|
||||||
|
"8BITMIME" => {
|
||||||
|
features.insert(Extension::EightBitMime);
|
||||||
|
}
|
||||||
|
"SMTPUTF8" => {
|
||||||
|
features.insert(Extension::SmtpUtfEight);
|
||||||
|
}
|
||||||
|
"STARTTLS" => {
|
||||||
|
features.insert(Extension::StartTls);
|
||||||
|
}
|
||||||
|
"AUTH" => {
|
||||||
for &mecanism in &splitted[1..] {
|
for &mecanism in &splitted[1..] {
|
||||||
match mecanism {
|
match mecanism {
|
||||||
"PLAIN" => mecanisms.push(PlainAuthentication),
|
"PLAIN" => {
|
||||||
"CRAM-MD5" => mecanisms.push(CramMd5Authentication),
|
features.insert(Extension::Authentication(Mecanism::Plain));
|
||||||
|
}
|
||||||
|
"CRAM-MD5" => {
|
||||||
|
features.insert(Extension::Authentication(Mecanism::CramMd5));
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
|
||||||
esmtp_features.push_all(&keywords);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
esmtp_features
|
Ok(ServerInfo {
|
||||||
|
name: name,
|
||||||
|
features: features,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the server supports an ESMTP feature
|
||||||
|
pub fn supports_feature(&self, keyword: &Extension) -> bool {
|
||||||
|
self.features.contains(keyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the server supports an ESMTP feature
|
||||||
|
pub fn supports_auth_mecanism(&self, mecanism: Mecanism) -> bool {
|
||||||
|
self.features.contains(&Extension::Authentication(mecanism))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::Extension;
|
use std::collections::HashSet;
|
||||||
use response::{Severity, Category, Response};
|
|
||||||
|
use super::{ServerInfo, Extension};
|
||||||
|
use authentication::Mecanism;
|
||||||
|
use response::{Code, Response, Severity, Category};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_str() {
|
fn test_extension_fmt() {
|
||||||
assert_eq!(Extension::from_str("8BITMIME"), Ok(vec![Extension::EightBitMime]));
|
assert_eq!(format!("{}", Extension::EightBitMime),
|
||||||
assert_eq!(Extension::from_str("AUTH PLAIN"), Ok(vec![Extension::PlainAuthentication]));
|
"8BITMIME".to_string());
|
||||||
assert_eq!(Extension::from_str("AUTH PLAIN LOGIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
|
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)),
|
||||||
assert_eq!(Extension::from_str("AUTH CRAM-MD5 PLAIN"), Ok(vec![Extension::CramMd5Authentication, Extension::PlainAuthentication]));
|
"AUTH PLAIN".to_string());
|
||||||
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_esmtp_response() {
|
fn test_serverinfo_fmt() {
|
||||||
assert_eq!(Extension::parse_esmtp_response(&Response::new(
|
let mut eightbitmime = HashSet::new();
|
||||||
"2".parse::<Severity>().unwrap(),
|
assert!(eightbitmime.insert(Extension::EightBitMime));
|
||||||
"2".parse::<Category>().unwrap(),
|
|
||||||
1,
|
assert_eq!(format!("{}",
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
ServerInfo {
|
||||||
)), vec![Extension::EightBitMime]);
|
name: "name".to_string(),
|
||||||
assert_eq!(Extension::parse_esmtp_response(&Response::new(
|
features: eightbitmime.clone(),
|
||||||
"4".parse::<Severity>().unwrap(),
|
}),
|
||||||
"3".parse::<Category>().unwrap(),
|
"name with {EightBitMime}".to_string());
|
||||||
3,
|
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "AUTH PLAIN CRAM-MD5".to_string()]
|
let empty = HashSet::new();
|
||||||
)), vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
|
|
||||||
|
assert_eq!(format!("{}",
|
||||||
|
ServerInfo {
|
||||||
|
name: "name".to_string(),
|
||||||
|
features: empty,
|
||||||
|
}),
|
||||||
|
"name with no supported features".to_string());
|
||||||
|
|
||||||
|
let mut plain = HashSet::new();
|
||||||
|
assert!(plain.insert(Extension::Authentication(Mecanism::Plain)));
|
||||||
|
|
||||||
|
assert_eq!(format!("{}",
|
||||||
|
ServerInfo {
|
||||||
|
name: "name".to_string(),
|
||||||
|
features: plain.clone(),
|
||||||
|
}),
|
||||||
|
"name with {Authentication(Plain)}".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serverinfo() {
|
||||||
|
let response = Response::new(Code::new(Severity::PositiveCompletion,
|
||||||
|
Category::Unspecified4,
|
||||||
|
1),
|
||||||
|
vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()]);
|
||||||
|
|
||||||
|
let mut features = HashSet::new();
|
||||||
|
assert!(features.insert(Extension::EightBitMime));
|
||||||
|
|
||||||
|
let server_info = ServerInfo {
|
||||||
|
name: "me".to_string(),
|
||||||
|
features: features,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
|
||||||
|
|
||||||
|
assert!(server_info.supports_feature(&Extension::EightBitMime));
|
||||||
|
assert!(!server_info.supports_feature(&Extension::StartTls));
|
||||||
|
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
|
||||||
|
|
||||||
|
let response2 = Response::new(Code::new(Severity::PositiveCompletion,
|
||||||
|
Category::Unspecified4,
|
||||||
|
1),
|
||||||
|
vec!["me".to_string(),
|
||||||
|
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()]);
|
||||||
|
|
||||||
|
let mut features2 = HashSet::new();
|
||||||
|
assert!(features2.insert(Extension::EightBitMime));
|
||||||
|
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
|
||||||
|
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
|
||||||
|
|
||||||
|
let server_info2 = ServerInfo {
|
||||||
|
name: "me".to_string(),
|
||||||
|
features: features2,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
|
||||||
|
|
||||||
|
assert!(server_info2.supports_feature(&Extension::EightBitMime));
|
||||||
|
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain));
|
||||||
|
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5));
|
||||||
|
assert!(!server_info2.supports_feature(&Extension::StartTls));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
src/lib.rs
63
src/lib.rs
@@ -1,35 +1,27 @@
|
|||||||
// 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
|
//! # Rust SMTP client
|
||||||
//!
|
//!
|
||||||
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
|
//! 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
|
//! a work in progress. It is designed to efficiently send emails from an application to a
|
||||||
//! relay email server.
|
//! 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:
|
//! It implements the following extensions:
|
||||||
//!
|
//!
|
||||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954))
|
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms
|
||||||
|
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||||
//!
|
//!
|
||||||
//! It will eventually implement the following extensions:
|
//! It will eventually implement the following extensions:
|
||||||
//!
|
//!
|
||||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
|
||||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||||
//!
|
//!
|
||||||
//! ## Architecture
|
//! ## Architecture
|
||||||
//!
|
//!
|
||||||
//! This client is divided into three parts:
|
//! This client is divided into three main parts:
|
||||||
//!
|
//!
|
||||||
//! * client: a low level SMTP client providing all SMTP commands
|
//! * client: a low level SMTP client providing all SMTP commands
|
||||||
//! * sender: a high level SMTP client providing an easy method to send emails
|
//! * sender: a high level SMTP client providing an easy method to send emails
|
||||||
//! * mailer: generates the email to be sent with the sender
|
//! * email: generates the email to be sent with the sender
|
||||||
//!
|
//!
|
||||||
//! ## Usage
|
//! ## Usage
|
||||||
//!
|
//!
|
||||||
@@ -39,8 +31,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use smtp::sender::{Sender, SenderBuilder};
|
//! use smtp::sender::{Sender, SenderBuilder};
|
||||||
//! use smtp::mailer::EmailBuilder;
|
//! use smtp::email::EmailBuilder;
|
||||||
//! use std::net::TcpStream;
|
|
||||||
//!
|
//!
|
||||||
//! // Create an email
|
//! // Create an email
|
||||||
//! let email = EmailBuilder::new()
|
//! let email = EmailBuilder::new()
|
||||||
@@ -53,7 +44,7 @@
|
|||||||
//! .build();
|
//! .build();
|
||||||
//!
|
//!
|
||||||
//! // Open a local connection on port 25
|
//! // Open a local connection on port 25
|
||||||
//! let mut sender: Sender<TcpStream> = SenderBuilder::localhost().build();
|
//! let mut sender = SenderBuilder::localhost().unwrap().build();
|
||||||
//! // Send the email
|
//! // Send the email
|
||||||
//! let result = sender.send(email);
|
//! let result = sender.send(email);
|
||||||
//!
|
//!
|
||||||
@@ -64,8 +55,9 @@
|
|||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use smtp::sender::{Sender, SenderBuilder};
|
//! use smtp::sender::{Sender, SenderBuilder};
|
||||||
//! use smtp::mailer::EmailBuilder;
|
//! use smtp::email::EmailBuilder;
|
||||||
//! use std::net::TcpStream;
|
//! use smtp::authentication::Mecanism;
|
||||||
|
//! use smtp::SUBMISSION_PORT;
|
||||||
//!
|
//!
|
||||||
//! let mut builder = EmailBuilder::new();
|
//! let mut builder = EmailBuilder::new();
|
||||||
//! builder = builder.to(("user@example.org", "Alias name"));
|
//! builder = builder.to(("user@example.org", "Alias name"));
|
||||||
@@ -81,11 +73,16 @@
|
|||||||
//! let email = builder.build();
|
//! let email = builder.build();
|
||||||
//!
|
//!
|
||||||
//! // Connect to a remote server on a custom port
|
//! // Connect to a remote server on a custom port
|
||||||
//! let mut sender: Sender<TcpStream> = SenderBuilder::new(("server.tld", 10025))
|
//! let mut sender = SenderBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap()
|
||||||
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
||||||
//! .hello_name("my.hostname.tld")
|
//! .hello_name("my.hostname.tld")
|
||||||
//! // Add credentials for authentication
|
//! // Add credentials for authentication
|
||||||
//! .credentials("username", "password")
|
//! .credentials("username", "password")
|
||||||
|
//! // Use TLS with STARTTLS, you can also specify a specific SSL context
|
||||||
|
//! // with `.ssl_context(context)`
|
||||||
|
//! .starttls()
|
||||||
|
//! // Configure accepted authetication mecanisms
|
||||||
|
//! .authentication_mecanisms(vec![Mecanism::CramMd5])
|
||||||
//! // Enable connection reuse
|
//! // Enable connection reuse
|
||||||
//! .enable_connection_reuse(true).build();
|
//! .enable_connection_reuse(true).build();
|
||||||
//!
|
//!
|
||||||
@@ -106,8 +103,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use smtp::sender::{Sender, SenderBuilder};
|
//! use smtp::sender::{Sender, SenderBuilder};
|
||||||
//! use smtp::sendable_email::SimpleSendableEmail;
|
//! use smtp::email::SimpleSendableEmail;
|
||||||
//! use std::net::TcpStream;
|
|
||||||
//!
|
//!
|
||||||
//! // Create a minimal email
|
//! // Create a minimal email
|
||||||
//! let email = SimpleSendableEmail::new(
|
//! let email = SimpleSendableEmail::new(
|
||||||
@@ -116,7 +112,7 @@
|
|||||||
//! "Hello world !"
|
//! "Hello world !"
|
||||||
//! );
|
//! );
|
||||||
//!
|
//!
|
||||||
//! let mut sender: Sender<TcpStream> = SenderBuilder::localhost().build();
|
//! let mut sender = SenderBuilder::localhost().unwrap().build();
|
||||||
//! let result = sender.send(email);
|
//! let result = sender.send(email);
|
||||||
//! assert!(result.is_ok());
|
//! assert!(result.is_ok());
|
||||||
//! ```
|
//! ```
|
||||||
@@ -128,10 +124,10 @@
|
|||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use smtp::client::Client;
|
//! use smtp::client::Client;
|
||||||
//! use smtp::SMTP_PORT;
|
//! use smtp::SMTP_PORT;
|
||||||
//! use std::net::TcpStream;
|
//! use smtp::client::net::NetworkStream;
|
||||||
//!
|
//!
|
||||||
//! let mut email_client: Client<TcpStream> = Client::new(("localhost", SMTP_PORT));
|
//! let mut email_client: Client<NetworkStream> = Client::new();
|
||||||
//! let _ = email_client.connect();
|
//! let _ = email_client.connect(&("localhost", SMTP_PORT));
|
||||||
//! let _ = email_client.ehlo("my_hostname");
|
//! let _ = email_client.ehlo("my_hostname");
|
||||||
//! let _ = email_client.mail("user@example.com", None);
|
//! let _ = email_client.mail("user@example.com", None);
|
||||||
//! let _ = email_client.rcpt("user@example.org");
|
//! let _ = email_client.rcpt("user@example.org");
|
||||||
@@ -140,22 +136,25 @@
|
|||||||
//! let _ = email_client.quit();
|
//! let _ = email_client.quit();
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
#![feature(plugin, core, collections, str_words)]
|
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
#[macro_use] extern crate log;
|
#[macro_use]
|
||||||
extern crate "rustc-serialize" as serialize;
|
extern crate log;
|
||||||
|
extern crate rustc_serialize as serialize;
|
||||||
extern crate crypto;
|
extern crate crypto;
|
||||||
extern crate time;
|
extern crate time;
|
||||||
extern crate uuid;
|
extern crate uuid;
|
||||||
|
extern crate email as email_format;
|
||||||
|
extern crate bufstream;
|
||||||
|
extern crate openssl;
|
||||||
|
|
||||||
mod extension;
|
mod extension;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod sender;
|
pub mod sender;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod sendable_email;
|
pub mod authentication;
|
||||||
pub mod mailer;
|
pub mod email;
|
||||||
|
|
||||||
// Registrated port numbers:
|
// Registrated port numbers:
|
||||||
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||||
|
|||||||
@@ -1,104 +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 SMTP "address" (very incomplete)
|
|
||||||
|
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
use SP;
|
|
||||||
|
|
||||||
/// 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(*self, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ToAddress for (&'a str, &'a str) {
|
|
||||||
fn to_address(&self) -> Address {
|
|
||||||
let (address, alias) = *self;
|
|
||||||
Address::new(address, Some(alias))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains an address with an optionnal alias
|
|
||||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
|
||||||
pub struct Address {
|
|
||||||
/// The address
|
|
||||||
address: String,
|
|
||||||
/// The alias
|
|
||||||
alias: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Address {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
write! (f, "{}", match self.alias {
|
|
||||||
Some(ref alias_string) => format!("{}{}<{}>", alias_string, SP, &self.address),
|
|
||||||
None => self.address.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Address {
|
|
||||||
/// Creates an address
|
|
||||||
pub fn new(address: &str, alias: Option<&str>) -> Address {
|
|
||||||
Address {
|
|
||||||
address: address.to_string(),
|
|
||||||
alias: match alias {
|
|
||||||
Some(ref alias_string) => Some(alias_string.to_string()),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return only the address
|
|
||||||
pub fn get_address(&self) -> String {
|
|
||||||
self.address.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::Address;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new() {
|
|
||||||
assert_eq!(
|
|
||||||
Address::new("address", Some("alias")),
|
|
||||||
Address{address: "address".to_string(), alias: Some("alias".to_string())}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Address::new("address", None),
|
|
||||||
Address{address: "address".to_string(), alias: None}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fmt() {
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", Address::new("address", None)),
|
|
||||||
"address".to_string()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", Address::new("address", Some("alias"))),
|
|
||||||
"alias <address>".to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +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 SMTP headers
|
|
||||||
|
|
||||||
use time::Tm;
|
|
||||||
|
|
||||||
use std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
use mailer::address::Address;
|
|
||||||
use {COLON, SP};
|
|
||||||
|
|
||||||
/// Converts to an `Header`
|
|
||||||
pub trait ToHeader {
|
|
||||||
/// Converts to an `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, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains a header
|
|
||||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
|
||||||
pub enum Header {
|
|
||||||
/// `To`
|
|
||||||
To(Address),
|
|
||||||
/// `From`
|
|
||||||
From(Address),
|
|
||||||
/// `Cc`
|
|
||||||
Cc(Address),
|
|
||||||
/// `Reply-To`
|
|
||||||
ReplyTo(Address),
|
|
||||||
/// `Sender`
|
|
||||||
Sender(Address),
|
|
||||||
/// `Date`
|
|
||||||
Date(Tm),
|
|
||||||
/// `Subject`
|
|
||||||
Subject(String),
|
|
||||||
/// `MIME-Version`
|
|
||||||
MimeVersion,
|
|
||||||
/// `Content-Type`
|
|
||||||
ContentType(String),
|
|
||||||
/// `Message-Id`
|
|
||||||
MessageId(String),
|
|
||||||
/// Any header (name, value)
|
|
||||||
Other(String, String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Header {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
write! (f, "{}{}{}{}",
|
|
||||||
match *self {
|
|
||||||
Header::To(_) => "To",
|
|
||||||
Header::From(_) => "From",
|
|
||||||
Header::Cc(_) => "Cc",
|
|
||||||
Header::ReplyTo(_) => "Reply-To",
|
|
||||||
Header::Sender(_) => "Sender",
|
|
||||||
Header::Date(_) => "Date",
|
|
||||||
Header::Subject(_) => "Subject",
|
|
||||||
Header::MimeVersion => "MIME-Version",
|
|
||||||
Header::ContentType(_) => "Content-Type",
|
|
||||||
Header::MessageId(_) => "Message-Id",
|
|
||||||
Header::Other(ref name, _) => name.as_slice(),
|
|
||||||
},
|
|
||||||
COLON, SP,
|
|
||||||
match *self {
|
|
||||||
Header::To(ref address) => format! ("{}", address),
|
|
||||||
Header::From(ref address) => format! ("{}", address),
|
|
||||||
Header::Cc(ref address) => format! ("{}", address),
|
|
||||||
Header::ReplyTo(ref address) => format! ("{}", address),
|
|
||||||
Header::Sender(ref address) => format! ("{}", address),
|
|
||||||
Header::Date(ref date) => Tm::rfc822(date).to_string(),
|
|
||||||
Header::Subject(ref subject) => subject.clone(),
|
|
||||||
Header::MimeVersion => "1.0".to_string(),
|
|
||||||
Header::ContentType(ref string) => string.clone(),
|
|
||||||
Header::MessageId(ref string) => string.clone(),
|
|
||||||
Header::Other(_, ref value) => value.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Header {
|
|
||||||
/// Creates ah `Header`
|
|
||||||
pub fn new(name: &str, value: &str) -> Header {
|
|
||||||
Header::Other(name.to_string(), value.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::Header;
|
|
||||||
use mailer::address::Address;
|
|
||||||
|
|
||||||
use time::{at_utc, Timespec};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new() {
|
|
||||||
assert_eq!(
|
|
||||||
Header::new("From", "me"),
|
|
||||||
Header::Other("From".to_string(), "me".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fmt() {
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", Header::new("From", "me")),
|
|
||||||
"From: me".to_string()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", Header::To(Address::new("me@example.com", Some("My Name")))),
|
|
||||||
"To: My Name <me@example.com>".to_string()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", Header::Subject("Test subject".to_string())),
|
|
||||||
"Subject: Test subject".to_string()
|
|
||||||
);
|
|
||||||
let time = at_utc(Timespec::new(1234567890, 54321));
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", Header::Date(time)),
|
|
||||||
"Date: Fri, 13 Feb 2009 23:31:30 GMT".to_string()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,255 +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 std::fmt::{Display, Formatter, Result};
|
|
||||||
|
|
||||||
use time::{now, Tm};
|
|
||||||
|
|
||||||
use mailer::header::{ToHeader, Header};
|
|
||||||
use mailer::address::ToAddress;
|
|
||||||
use sendable_email::SendableEmail;
|
|
||||||
use CRLF;
|
|
||||||
|
|
||||||
pub mod header;
|
|
||||||
pub mod address;
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
/// Array of headers
|
|
||||||
headers: Vec<Header>,
|
|
||||||
/// Message body
|
|
||||||
body: String,
|
|
||||||
/// The enveloppe recipients addresses
|
|
||||||
to: Vec<String>,
|
|
||||||
/// The enveloppe sender address
|
|
||||||
from: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Email {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
let mut formatted_headers = String::new();
|
|
||||||
for header in self.headers.iter() {
|
|
||||||
formatted_headers.push_str(&format! ("{}", header));
|
|
||||||
formatted_headers.push_str(CRLF);
|
|
||||||
}
|
|
||||||
write! (f, "{}{}{}", formatted_headers, CRLF, self.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EmailBuilder {
|
|
||||||
/// Creates a new empty email
|
|
||||||
pub fn new() -> EmailBuilder {
|
|
||||||
EmailBuilder {
|
|
||||||
content: Email {
|
|
||||||
headers: vec![],
|
|
||||||
body: "".to_string(),
|
|
||||||
to: vec![],
|
|
||||||
from: None,
|
|
||||||
},
|
|
||||||
date_issued: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the email body
|
|
||||||
pub fn body(mut self, body: &str) -> EmailBuilder {
|
|
||||||
self.content.body = body.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a generic header
|
|
||||||
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
|
|
||||||
self.content.headers.push(header.to_header());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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());
|
|
||||||
self.content.headers.push(
|
|
||||||
Header::From(address.to_address())
|
|
||||||
);
|
|
||||||
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());
|
|
||||||
self.content.headers.push(
|
|
||||||
Header::To(address.to_address())
|
|
||||||
);
|
|
||||||
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());
|
|
||||||
self.content.headers.push(
|
|
||||||
Header::Cc(address.to_address())
|
|
||||||
);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a `Reply-To` header
|
|
||||||
pub fn reply_to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
|
|
||||||
self.content.headers.push(
|
|
||||||
Header::ReplyTo(address.to_address())
|
|
||||||
);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a `Sender` header
|
|
||||||
pub fn sender<A: ToAddress>(mut self, address: A) -> EmailBuilder {
|
|
||||||
self.content.from = Some(address.to_address().get_address());
|
|
||||||
self.content.headers.push(
|
|
||||||
Header::Sender(address.to_address())
|
|
||||||
);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a `Subject` header
|
|
||||||
pub fn subject(mut self, subject: &str) -> EmailBuilder {
|
|
||||||
self.content.headers.push(
|
|
||||||
Header::Subject(subject.to_string())
|
|
||||||
);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a `Date` header with the given date
|
|
||||||
pub fn date(mut self, date: Tm) -> EmailBuilder {
|
|
||||||
self.content.headers.push(
|
|
||||||
Header::Date(date)
|
|
||||||
);
|
|
||||||
self.date_issued = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build the Email
|
|
||||||
pub fn build(mut self) -> Email {
|
|
||||||
if !self.date_issued {
|
|
||||||
self.content.headers.push(
|
|
||||||
Header::Date(now())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a `Message-ID` header
|
|
||||||
fn set_message_id(&mut self, string: String) {
|
|
||||||
self.headers.push(
|
|
||||||
Header::MessageId(string)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::{Email, EmailBuilder};
|
|
||||||
use mailer::header::Header;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new() {
|
|
||||||
assert_eq!(
|
|
||||||
EmailBuilder::new(),
|
|
||||||
EmailBuilder{content: Email{headers: vec![], body: "".to_string(), to: vec![], from: None}, date_issued: false}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_body() {
|
|
||||||
let email = EmailBuilder::new().body("test message");
|
|
||||||
assert_eq!(
|
|
||||||
email,
|
|
||||||
EmailBuilder{content: Email {headers: vec![], body: "test message".to_string(), to: vec![], from: None}, date_issued: false}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_add_header() {
|
|
||||||
let mut email = EmailBuilder::new()
|
|
||||||
.add_header(("X-My-Header", "value"));
|
|
||||||
assert_eq!(
|
|
||||||
email,
|
|
||||||
EmailBuilder{
|
|
||||||
content: Email {
|
|
||||||
headers: vec![Header::new("X-My-Header", "value")],
|
|
||||||
body: "".to_string(),
|
|
||||||
to: vec![],
|
|
||||||
from: None
|
|
||||||
},
|
|
||||||
date_issued: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
email = email.add_header(("X-My-Header-2", "value-2"));
|
|
||||||
assert_eq!(
|
|
||||||
email,
|
|
||||||
EmailBuilder{
|
|
||||||
content: Email {
|
|
||||||
headers: vec![Header::new("X-My-Header", "value"),
|
|
||||||
Header::new("X-My-Header-2", "value-2")],
|
|
||||||
body: "".to_string(),
|
|
||||||
to: vec![],
|
|
||||||
from: None
|
|
||||||
},
|
|
||||||
date_issued: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
email = email.add_header(("X-My-Header-3", "value-3")).add_header(("X-My-Header-4", "value-4"));
|
|
||||||
assert_eq!(
|
|
||||||
email,
|
|
||||||
EmailBuilder{
|
|
||||||
content: Email {
|
|
||||||
headers: vec![Header::new("X-My-Header", "value"),
|
|
||||||
Header::new("X-My-Header-2", "value-2"),
|
|
||||||
Header::new("X-My-Header-3", "value-3"),
|
|
||||||
Header::new("X-My-Header-4", "value-4")],
|
|
||||||
body: "".to_string(),
|
|
||||||
to: vec![],
|
|
||||||
from: None
|
|
||||||
},
|
|
||||||
date_issued: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO test Email
|
|
||||||
}
|
|
||||||
534
src/response.rs
534
src/response.rs
@@ -1,20 +1,12 @@
|
|||||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
//! SMTP response, containing a mandatory return code and an optional text message
|
||||||
// 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::str::FromStr;
|
||||||
use std::fmt::{Display, Formatter, Result};
|
use std::fmt::{Display, Formatter, Result};
|
||||||
use std::result::Result as RResult;
|
use std::result;
|
||||||
|
|
||||||
use self::Severity::*;
|
use self::Severity::*;
|
||||||
use self::Category::*;
|
use self::Category::*;
|
||||||
|
use error::{SmtpResult, Error};
|
||||||
|
|
||||||
/// First digit indicates severity
|
/// First digit indicates severity
|
||||||
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
|
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
|
||||||
@@ -30,28 +22,28 @@ pub enum Severity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Severity {
|
impl FromStr for Severity {
|
||||||
type Err = &'static str;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> RResult<Severity, &'static str> {
|
fn from_str(s: &str) -> result::Result<Severity, Error> {
|
||||||
match s {
|
match s {
|
||||||
"2" => Ok(PositiveCompletion),
|
"2" => Ok(PositiveCompletion),
|
||||||
"3" => Ok(PositiveIntermediate),
|
"3" => Ok(PositiveIntermediate),
|
||||||
"4" => Ok(TransientNegativeCompletion),
|
"4" => Ok(TransientNegativeCompletion),
|
||||||
"5" => Ok(PermanentNegativeCompletion),
|
"5" => Ok(PermanentNegativeCompletion),
|
||||||
_ => Err("First digit must be between 2 and 5"),
|
_ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Severity {
|
impl Display for Severity {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
write!(f, "{}",
|
write!(f,
|
||||||
|
"{}",
|
||||||
match *self {
|
match *self {
|
||||||
PositiveCompletion => 2,
|
PositiveCompletion => 2,
|
||||||
PositiveIntermediate => 3,
|
PositiveIntermediate => 3,
|
||||||
TransientNegativeCompletion => 4,
|
TransientNegativeCompletion => 4,
|
||||||
PermanentNegativeCompletion => 5,
|
PermanentNegativeCompletion => 5,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +65,8 @@ pub enum Category {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Category {
|
impl FromStr for Category {
|
||||||
type Err = &'static str;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> RResult<Category, &'static str> {
|
fn from_str(s: &str) -> result::Result<Category, Error> {
|
||||||
match s {
|
match s {
|
||||||
"0" => Ok(Syntax),
|
"0" => Ok(Syntax),
|
||||||
"1" => Ok(Information),
|
"1" => Ok(Information),
|
||||||
@@ -82,14 +74,15 @@ impl FromStr for Category {
|
|||||||
"3" => Ok(Unspecified3),
|
"3" => Ok(Unspecified3),
|
||||||
"4" => Ok(Unspecified4),
|
"4" => Ok(Unspecified4),
|
||||||
"5" => Ok(MailSystem),
|
"5" => Ok(MailSystem),
|
||||||
_ => Err("Second digit must be between 0 and 5"),
|
_ => Err(Error::ResponseParsingError("Second digit must be between 0 and 5")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Category {
|
impl Display for Category {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
write!(f, "{}",
|
write!(f,
|
||||||
|
"{}",
|
||||||
match *self {
|
match *self {
|
||||||
Syntax => 0,
|
Syntax => 0,
|
||||||
Information => 1,
|
Information => 1,
|
||||||
@@ -97,8 +90,114 @@ impl Display for Category {
|
|||||||
Unspecified3 => 3,
|
Unspecified3 => 3,
|
||||||
Unspecified4 => 4,
|
Unspecified4 => 4,
|
||||||
MailSystem => 5,
|
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,
|
||||||
|
}),
|
||||||
|
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Code {
|
||||||
|
/// Creates a new `Code` structure
|
||||||
|
pub fn new(severity: Severity, category: Category, detail: u8) -> Code {
|
||||||
|
Code {
|
||||||
|
severity: severity,
|
||||||
|
category: category,
|
||||||
|
detail: detail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the reply code
|
||||||
|
pub fn code(&self) -> String {
|
||||||
|
format!("{}{}{}", self.severity, self.category, self.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses an SMTP response
|
||||||
|
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||||
|
pub struct ResponseParser {
|
||||||
|
/// Response code
|
||||||
|
code: Option<Code>,
|
||||||
|
/// Server response string (optional)
|
||||||
|
/// Handle multiline responses
|
||||||
|
message: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseParser {
|
||||||
|
/// Creates a new parser
|
||||||
|
pub fn new() -> ResponseParser {
|
||||||
|
ResponseParser {
|
||||||
|
code: None,
|
||||||
|
message: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses a line and return a `bool` indicating if there are more lines to come
|
||||||
|
pub fn read_line(&mut self, line: &str) -> result::Result<bool, Error> {
|
||||||
|
|
||||||
|
if line.len() < 3 {
|
||||||
|
return Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.code {
|
||||||
|
Some(ref code) => {
|
||||||
|
if code.code() != line[0..3] {
|
||||||
|
return Err(Error::ResponseParsingError("Response code has changed during a \
|
||||||
|
reponse"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => self.code = Some(try!(line[0..3].parse::<Code>())),
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.len() > 4 {
|
||||||
|
self.message.push(line[4..].to_string());
|
||||||
|
if line.as_bytes()[3] == '-' as u8 {
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a response from a `ResponseParser`
|
||||||
|
pub fn response(self) -> SmtpResult {
|
||||||
|
match self.code {
|
||||||
|
Some(code) => Ok(Response::new(code, self.message)),
|
||||||
|
None => Err(Error::ResponseParsingError("Incomplete response, could not read \
|
||||||
|
response code")),
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,48 +206,25 @@ impl Display for Category {
|
|||||||
/// The text message is optional, only the code is mandatory
|
/// The text message is optional, only the code is mandatory
|
||||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
/// First digit of the response code
|
/// Response code
|
||||||
severity: Severity,
|
code: Code,
|
||||||
/// Second digit of the response code
|
|
||||||
category: Category,
|
|
||||||
/// Third digit
|
|
||||||
detail: u8,
|
|
||||||
/// Server response string (optional)
|
/// Server response string (optional)
|
||||||
/// Handle multiline responses
|
/// Handle multiline responses
|
||||||
message: Vec<String>
|
message: Vec<String>,
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Response {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
let code = self.code();
|
|
||||||
for line in self.message[..-1].iter() {
|
|
||||||
let _ = write!(f, "{}-{}",
|
|
||||||
code,
|
|
||||||
line
|
|
||||||
);
|
|
||||||
}
|
|
||||||
write!(f, "{} {}",
|
|
||||||
code,
|
|
||||||
self.message[-1]
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Response {
|
impl Response {
|
||||||
/// Creates a new `Response`
|
/// Creates a new `Response`
|
||||||
pub fn new(severity: Severity, category: Category, detail: u8, message: Vec<String>) -> Response {
|
pub fn new(code: Code, message: Vec<String>) -> Response {
|
||||||
Response {
|
Response {
|
||||||
severity: severity,
|
code: code,
|
||||||
category: category,
|
message: message,
|
||||||
detail: detail,
|
|
||||||
message: message
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tells if the response is positive
|
/// Tells if the response is positive
|
||||||
pub fn is_positive(&self) -> bool {
|
pub fn is_positive(&self) -> bool {
|
||||||
match self.severity {
|
match self.code.severity {
|
||||||
PositiveCompletion => true,
|
PositiveCompletion => true,
|
||||||
PositiveIntermediate => true,
|
PositiveIntermediate => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
@@ -162,25 +238,25 @@ impl Response {
|
|||||||
|
|
||||||
/// Returns the severity (i.e. 1st digit)
|
/// Returns the severity (i.e. 1st digit)
|
||||||
pub fn severity(&self) -> Severity {
|
pub fn severity(&self) -> Severity {
|
||||||
self.severity
|
self.code.severity
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the category (i.e. 2nd digit)
|
/// Returns the category (i.e. 2nd digit)
|
||||||
pub fn category(&self) -> Category {
|
pub fn category(&self) -> Category {
|
||||||
self.category
|
self.code.category
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the detail (i.e. 3rd digit)
|
/// Returns the detail (i.e. 3rd digit)
|
||||||
pub fn detail(&self) -> u8 {
|
pub fn detail(&self) -> u8 {
|
||||||
self.detail
|
self.code.detail
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the reply code
|
/// Returns the reply code
|
||||||
fn code(&self) -> String {
|
fn code(&self) -> String {
|
||||||
format!("{}{}{}", self.severity, self.category, self.detail)
|
self.code.code()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checls code equality
|
/// Tests code equality
|
||||||
pub fn has_code(&self, code: u16) -> bool {
|
pub fn has_code(&self, code: u16) -> bool {
|
||||||
self.code() == format!("{}", code)
|
self.code() == format!("{}", code)
|
||||||
}
|
}
|
||||||
@@ -189,156 +265,324 @@ impl Response {
|
|||||||
pub fn first_word(&self) -> Option<String> {
|
pub fn first_word(&self) -> Option<String> {
|
||||||
match self.message.is_empty() {
|
match self.message.is_empty() {
|
||||||
true => None,
|
true => None,
|
||||||
false => Some(self.message[0].words().next().unwrap().to_string())
|
false => match self.message[0].split_whitespace().next() {
|
||||||
|
Some(word) => Some(word.to_string()),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{Severity, Category, Response};
|
use super::{Severity, Category, Response, ResponseParser, Code};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_severity_from_str() {
|
fn test_severity_from_str() {
|
||||||
assert_eq!("2".parse::<Severity>(), Ok(Severity::PositiveCompletion));
|
assert_eq!("2".parse::<Severity>().unwrap(),
|
||||||
assert_eq!("4".parse::<Severity>(), Ok(Severity::TransientNegativeCompletion));
|
Severity::PositiveCompletion);
|
||||||
|
assert_eq!("4".parse::<Severity>().unwrap(),
|
||||||
|
Severity::TransientNegativeCompletion);
|
||||||
assert!("1".parse::<Severity>().is_err());
|
assert!("1".parse::<Severity>().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_severity_fmt() {
|
fn test_severity_fmt() {
|
||||||
assert_eq!(format!("{}", Severity::PositiveCompletion).as_slice(), "2");
|
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_category_from_str() {
|
fn test_category_from_str() {
|
||||||
assert_eq!("2".parse::<Category>(), Ok(Category::Connections));
|
assert_eq!("2".parse::<Category>().unwrap(), Category::Connections);
|
||||||
assert_eq!("4".parse::<Category>(), Ok(Category::Unspecified4));
|
assert_eq!("4".parse::<Category>().unwrap(), Category::Unspecified4);
|
||||||
assert!("6".parse::<Category>().is_err());
|
assert!("6".parse::<Category>().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_category_fmt() {
|
fn test_category_fmt() {
|
||||||
assert_eq!(format!("{}", Category::Unspecified4).as_slice(), "4");
|
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]
|
#[test]
|
||||||
fn test_response_new() {
|
fn test_response_new() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
), Response {
|
vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()]),
|
||||||
|
Response {
|
||||||
|
code: Code {
|
||||||
severity: Severity::PositiveCompletion,
|
severity: Severity::PositiveCompletion,
|
||||||
category: Category::Unspecified4,
|
category: Category::Unspecified4,
|
||||||
detail: 1,
|
detail: 1,
|
||||||
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
|
},
|
||||||
|
message: vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()],
|
||||||
|
});
|
||||||
|
assert_eq!(Response::new(Code {
|
||||||
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
|
category: "4".parse::<Category>().unwrap(),
|
||||||
|
detail: 1,
|
||||||
|
},
|
||||||
|
vec![]),
|
||||||
|
Response {
|
||||||
|
code: Code {
|
||||||
|
severity: Severity::PositiveCompletion,
|
||||||
|
category: Category::Unspecified4,
|
||||||
|
detail: 1,
|
||||||
|
},
|
||||||
|
message: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_parser() {
|
||||||
|
let mut parser = ResponseParser::new();
|
||||||
|
|
||||||
|
assert!(parser.read_line("250-me").unwrap());
|
||||||
|
assert!(parser.read_line("250-8BITMIME").unwrap());
|
||||||
|
assert!(parser.read_line("250-SIZE 42").unwrap());
|
||||||
|
assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap());
|
||||||
|
|
||||||
|
let response = parser.response().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response,
|
||||||
|
Response {
|
||||||
|
code: Code {
|
||||||
|
severity: Severity::PositiveCompletion,
|
||||||
|
category: Category::MailSystem,
|
||||||
|
detail: 0,
|
||||||
|
},
|
||||||
|
message: vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string(),
|
||||||
|
"AUTH PLAIN CRAM-MD5".to_string()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_is_positive() {
|
fn test_response_is_positive() {
|
||||||
assert!(Response::new(
|
assert!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).is_positive());
|
vec!["me".to_string(),
|
||||||
assert!(! Response::new(
|
"8BITMIME".to_string(),
|
||||||
"4".parse::<Severity>().unwrap(),
|
"SIZE 42".to_string()])
|
||||||
"4".parse::<Category>().unwrap(),
|
.is_positive());
|
||||||
1,
|
assert!(!Response::new(Code {
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
severity: "5".parse::<Severity>().unwrap(),
|
||||||
).is_positive());
|
category: "4".parse::<Category>().unwrap(),
|
||||||
|
detail: 1,
|
||||||
|
},
|
||||||
|
vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()])
|
||||||
|
.is_positive());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_message() {
|
fn test_response_message() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
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()]);
|
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]
|
#[test]
|
||||||
fn test_response_severity() {
|
fn test_response_severity() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).severity(), Severity::PositiveCompletion);
|
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]
|
#[test]
|
||||||
fn test_response_category() {
|
fn test_response_category() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).category(), Category::Unspecified4);
|
vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()])
|
||||||
|
.category(),
|
||||||
|
Category::Unspecified4);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_detail() {
|
fn test_response_detail() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).detail(), 1);
|
vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()])
|
||||||
|
.detail(),
|
||||||
|
1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_code() {
|
fn test_response_code() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).code(), "241");
|
vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()])
|
||||||
|
.code(),
|
||||||
|
"241");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_has_code() {
|
fn test_response_has_code() {
|
||||||
assert!(Response::new(
|
assert!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).has_code(241));
|
vec!["me".to_string(),
|
||||||
assert!(! Response::new(
|
"8BITMIME".to_string(),
|
||||||
"2".parse::<Severity>().unwrap(),
|
"SIZE 42".to_string()])
|
||||||
"4".parse::<Category>().unwrap(),
|
.has_code(241));
|
||||||
1,
|
assert!(!Response::new(Code {
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
).has_code(251));
|
category: "4".parse::<Category>().unwrap(),
|
||||||
|
detail: 1,
|
||||||
|
},
|
||||||
|
vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()])
|
||||||
|
.has_code(251));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_first_word() {
|
fn test_response_first_word() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
"2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
"4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
1,
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).first_word(), Some("me".to_string()));
|
vec!["me".to_string(),
|
||||||
assert_eq!(Response::new(
|
"8BITMIME".to_string(),
|
||||||
"2".parse::<Severity>().unwrap(),
|
"SIZE 42".to_string()])
|
||||||
"4".parse::<Category>().unwrap(),
|
.first_word(),
|
||||||
1,
|
Some("me".to_string()));
|
||||||
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
assert_eq!(Response::new(Code {
|
||||||
).first_word(), Some("me".to_string()));
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
assert_eq!(Response::new(
|
category: "4".parse::<Category>().unwrap(),
|
||||||
"2".parse::<Severity>().unwrap(),
|
detail: 1,
|
||||||
"4".parse::<Category>().unwrap(),
|
},
|
||||||
1,
|
vec!["me mo".to_string(),
|
||||||
vec![]
|
"8BITMIME".to_string(),
|
||||||
).first_word(), None);
|
"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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +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
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
/// Set message-ID header
|
|
||||||
fn set_message_id(&mut self, id: 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 set_message_id(&mut self, id: String) {
|
|
||||||
let _ = id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,16 @@
|
|||||||
// 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
|
//! Sends an email using the client
|
||||||
|
|
||||||
use std::string::String;
|
use std::string::String;
|
||||||
use std::net::TcpStream;
|
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
use std::io::{Read, Write};
|
|
||||||
|
|
||||||
use uuid::Uuid;
|
use openssl::ssl::{SslMethod, SslContext};
|
||||||
|
|
||||||
use SMTP_PORT;
|
use SMTP_PORT;
|
||||||
use extension::Extension;
|
use extension::{Extension, ServerInfo};
|
||||||
use error::{SmtpResult, SmtpError};
|
use error::{SmtpResult, Error};
|
||||||
use sendable_email::SendableEmail;
|
use email::SendableEmail;
|
||||||
use sender::server_info::ServerInfo;
|
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use client::connecter::Connecter;
|
use authentication::Mecanism;
|
||||||
|
|
||||||
mod server_info;
|
|
||||||
|
|
||||||
/// Contains client configuration
|
/// Contains client configuration
|
||||||
pub struct SenderBuilder {
|
pub struct SenderBuilder {
|
||||||
@@ -40,26 +26,48 @@ pub struct SenderBuilder {
|
|||||||
credentials: Option<(String, String)>,
|
credentials: Option<(String, String)>,
|
||||||
/// Socket we are connecting to
|
/// Socket we are connecting to
|
||||||
server_addr: SocketAddr,
|
server_addr: SocketAddr,
|
||||||
|
/// SSL contexyt to use
|
||||||
|
ssl_context: Option<SslContext>,
|
||||||
|
/// List of authentication mecanism, sorted by priority
|
||||||
|
authentication_mecanisms: Vec<Mecanism>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for the SMTP Sender
|
/// Builder for the SMTP Sender
|
||||||
impl SenderBuilder {
|
impl SenderBuilder {
|
||||||
/// Creates a new local SMTP client
|
/// Creates a new local SMTP client
|
||||||
pub fn new<A: ToSocketAddrs>(addr: A) -> SenderBuilder {
|
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SenderBuilder, Error> {
|
||||||
SenderBuilder {
|
let mut addresses = try!(addr.to_socket_addrs());
|
||||||
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
|
|
||||||
|
match addresses.next() {
|
||||||
|
Some(addr) => Ok(SenderBuilder {
|
||||||
|
server_addr: addr,
|
||||||
|
ssl_context: None,
|
||||||
credentials: None,
|
credentials: None,
|
||||||
connection_reuse_count_limit: 100,
|
connection_reuse_count_limit: 100,
|
||||||
enable_connection_reuse: false,
|
enable_connection_reuse: false,
|
||||||
hello_name: "localhost".to_string(),
|
hello_name: "localhost".to_string(),
|
||||||
|
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
|
||||||
|
}),
|
||||||
|
None => Err(From::from("Could nor resolve hostname")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new local SMTP client to port 25
|
/// Creates a new local SMTP client to port 25
|
||||||
pub fn localhost() -> SenderBuilder {
|
pub fn localhost() -> Result<SenderBuilder, Error> {
|
||||||
SenderBuilder::new(("localhost", SMTP_PORT))
|
SenderBuilder::new(("localhost", SMTP_PORT))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use STARTTLS with a specific context
|
||||||
|
pub fn ssl_context(mut self, ssl_context: SslContext) -> SenderBuilder {
|
||||||
|
self.ssl_context = Some(ssl_context);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Require SSL/TLS using STARTTLS
|
||||||
|
pub fn starttls(self) -> SenderBuilder {
|
||||||
|
self.ssl_context(SslContext::new(SslMethod::Tlsv1).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the name used during HELO or EHLO
|
/// Set the name used during HELO or EHLO
|
||||||
pub fn hello_name(mut self, name: &str) -> SenderBuilder {
|
pub fn hello_name(mut self, name: &str) -> SenderBuilder {
|
||||||
self.hello_name = name.to_string();
|
self.hello_name = name.to_string();
|
||||||
@@ -84,10 +92,16 @@ impl SenderBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the authentication mecanisms
|
||||||
|
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SenderBuilder {
|
||||||
|
self.authentication_mecanisms = mecanisms;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the SMTP client
|
/// Build the SMTP client
|
||||||
///
|
///
|
||||||
/// It does not connects to the server, but only creates the `Sender`
|
/// It does not connects to the server, but only creates the `Sender`
|
||||||
pub fn build<S: Connecter + Read + Write>(self) -> Sender<S> {
|
pub fn build(self) -> Sender {
|
||||||
Sender::new(self)
|
Sender::new(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +116,7 @@ struct State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Structure that implements the high level SMTP client
|
/// Structure that implements the high level SMTP client
|
||||||
pub struct Sender<S = TcpStream> {
|
pub struct Sender {
|
||||||
/// Information about the server
|
/// Information about the server
|
||||||
/// Value is None before HELO/EHLO
|
/// Value is None before HELO/EHLO
|
||||||
server_info: Option<ServerInfo>,
|
server_info: Option<ServerInfo>,
|
||||||
@@ -111,7 +125,7 @@ pub struct Sender<S = TcpStream> {
|
|||||||
/// Information about the client
|
/// Information about the client
|
||||||
client_info: SenderBuilder,
|
client_info: SenderBuilder,
|
||||||
/// Low level client
|
/// Low level client
|
||||||
client: Client<S>,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! try_smtp (
|
macro_rules! try_smtp (
|
||||||
@@ -121,7 +135,7 @@ macro_rules! try_smtp (
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
if !$client.state.panic {
|
if !$client.state.panic {
|
||||||
$client.state.panic = true;
|
$client.state.panic = true;
|
||||||
$client.client.close();
|
$client.reset();
|
||||||
}
|
}
|
||||||
return Err(err)
|
return Err(err)
|
||||||
},
|
},
|
||||||
@@ -129,12 +143,14 @@ macro_rules! try_smtp (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
impl<S = TcpStream> Sender<S> {
|
impl Sender {
|
||||||
/// Creates a new SMTP client
|
/// Creates a new SMTP client
|
||||||
///
|
///
|
||||||
/// It does not connects to the server, but only creates the `Sender`
|
/// It does not connects to the server, but only creates the `Sender`
|
||||||
pub fn new(builder: SenderBuilder) -> Sender<S> {
|
pub fn new(builder: SenderBuilder) -> Sender {
|
||||||
let client: Client<S> = Client::new(builder.server_addr);
|
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
Sender {
|
Sender {
|
||||||
client: client,
|
client: client,
|
||||||
server_info: None,
|
server_info: None,
|
||||||
@@ -145,9 +161,7 @@ impl<S = TcpStream> Sender<S> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: Connecter + Write + Read = TcpStream> Sender<S> {
|
|
||||||
/// Reset the client state
|
/// Reset the client state
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
// Close the SMTP transaction if needed
|
// Close the SMTP transaction if needed
|
||||||
@@ -164,8 +178,21 @@ impl<S: Connecter + Write + Read = TcpStream> Sender<S> {
|
|||||||
self.client.close();
|
self.client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the EHLO response and updates server information
|
||||||
|
pub fn get_ehlo(&mut self) -> SmtpResult {
|
||||||
|
// Extended Hello
|
||||||
|
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
|
||||||
|
|
||||||
|
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
|
||||||
|
|
||||||
|
// Print server information
|
||||||
|
debug!("server {}", self.server_info.as_ref().unwrap());
|
||||||
|
|
||||||
|
Ok(ehlo_response)
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends an email
|
/// Sends an email
|
||||||
pub fn send<T: SendableEmail>(&mut self, mut email: T) -> SmtpResult {
|
pub fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
|
||||||
// Check if the connection is still available
|
// Check if the connection is still available
|
||||||
if self.state.connection_reuse_count > 0 {
|
if self.state.connection_reuse_count > 0 {
|
||||||
if !self.client.is_connected() {
|
if !self.client.is_connected() {
|
||||||
@@ -175,68 +202,51 @@ impl<S: Connecter + Write + Read = TcpStream> Sender<S> {
|
|||||||
|
|
||||||
// If there is a usable connection, test if the server answers and hello has been sent
|
// If there is a usable connection, test if the server answers and hello has been sent
|
||||||
if self.state.connection_reuse_count == 0 {
|
if self.state.connection_reuse_count == 0 {
|
||||||
try!(self.client.connect());
|
try!(self.client.connect(&self.client_info.server_addr));
|
||||||
|
|
||||||
// Log the connection
|
// Log the connection
|
||||||
info!("connection established to {}", self.client_info.server_addr);
|
info!("connection established to {}", self.client_info.server_addr);
|
||||||
|
|
||||||
// Extended Hello or Hello if needed
|
try!(self.get_ehlo());
|
||||||
match self.client.ehlo(&self.client_info.hello_name) {
|
|
||||||
Ok(response) => {self.server_info = Some(
|
if self.client_info.ssl_context.is_some() {
|
||||||
ServerInfo{
|
try_smtp!(self.client.starttls(), self);
|
||||||
name: response.first_word().expect("Server announced no hostname"),
|
|
||||||
esmtp_features: Extension::parse_esmtp_response(&response),
|
try!(self.client
|
||||||
});
|
.upgrade_tls_stream(self.client_info.ssl_context.as_ref().unwrap()));
|
||||||
},
|
|
||||||
Err(error) => match error {
|
try!(self.get_ehlo());
|
||||||
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 {
|
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
|
||||||
|
|
||||||
let (username, password) = self.client_info.credentials.clone().unwrap();
|
let (username, password) = self.client_info.credentials.clone().unwrap();
|
||||||
|
|
||||||
if self.server_info.as_ref().unwrap().supports_feature(Extension::CramMd5Authentication) {
|
let mut found = false;
|
||||||
let result = self.client.auth_cram_md5(&username, &password);
|
|
||||||
|
for mecanism in self.client_info.authentication_mecanisms.clone() {
|
||||||
|
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
|
||||||
|
found = true;
|
||||||
|
let result = self.client.auth(mecanism, &username, &password);
|
||||||
try_smtp!(result, self);
|
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 {
|
if !found {
|
||||||
debug!("No supported authentication mecanisms available");
|
debug!("No supported authentication mecanisms available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let current_message = Uuid::new_v4();
|
let current_message = try!(email.message_id().ok_or("Missing Message-ID"));
|
||||||
email.set_message_id(format!("<{}@{}>", current_message,
|
let from_address = try!(email.from_address().ok_or("Missing From address"));
|
||||||
self.client_info.hello_name.clone()));
|
let to_addresses = try!(email.to_addresses().ok_or("Missing To address"));
|
||||||
|
let message = try!(email.message().ok_or("Missing message"));
|
||||||
let from_address = email.from_address();
|
|
||||||
let to_addresses = email.to_addresses();
|
|
||||||
let message = email.message();
|
|
||||||
|
|
||||||
// Mail
|
// Mail
|
||||||
let mail_options = match self.server_info.as_ref().unwrap().supports_feature(Extension::EightBitMime) {
|
let mail_options = match self.server_info
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.supports_feature(&Extension::EightBitMime) {
|
||||||
true => Some("BODY=8BITMIME"),
|
true => Some("BODY=8BITMIME"),
|
||||||
false => None,
|
false => None,
|
||||||
};
|
};
|
||||||
@@ -264,12 +274,17 @@ impl<S: Connecter + Write + Read = TcpStream> Sender<S> {
|
|||||||
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
|
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
|
||||||
|
|
||||||
// Log the message
|
// Log the message
|
||||||
info!("{}: conn_use={}, size={}, status=sent ({})", current_message,
|
info!("{}: conn_use={}, size={}, status=sent ({})",
|
||||||
self.state.connection_reuse_count, message.len(), match result.as_ref().ok().unwrap().message().as_slice() {
|
current_message,
|
||||||
[ref line, ..] => line.as_slice(),
|
self.state.connection_reuse_count,
|
||||||
[] => "no response",
|
message.len(),
|
||||||
}
|
result.as_ref()
|
||||||
);
|
.ok()
|
||||||
|
.unwrap()
|
||||||
|
.message()
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or(&"no response".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if we can reuse the existing connection
|
// Test if we can reuse the existing connection
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user