Compare commits

...

61 Commits

Author SHA1 Message Date
Alexis Mousset
5f911dce12 Formatting with rustfmt 2015-10-12 02:19:33 +02:00
Alexis Mousset
47d6870d93 Add STARTTLS support instead of SMTPS 2015-10-12 00:59:39 +02:00
Alexis Mousset
51de392086 Add documentation 2015-10-11 21:21:31 +02:00
Alexis Mousset
fefb5f7978 smtps with rust-openssl 2015-10-11 19:56:02 +02:00
Alexis Mousset
5d125bdbdb Format code with rustfmt 2015-10-08 20:12:07 +02:00
Alexis Mousset
a1bf0170db Version 0.2.0 2015-10-06 18:42:35 +02:00
Alexis Mousset
5bedba4b24 Use Tm::rfc822z to support local timezones (workaround for time crate incomplete feature) 2015-10-06 18:42:23 +02:00
Alexis Mousset
813f09a314 v0.1.2 2015-08-02 22:05:55 +02:00
Alexis Mousset
6a6023431b Add test cases for authentication 2015-08-02 21:24:24 +02:00
Alexis Mousset
7f6aa0ffae Document authentication mecanism configuration 2015-08-02 19:23:29 +02:00
Alexis Mousset
1830f084c0 Let the user configure the authentication mecanisms 2015-08-02 19:12:59 +02:00
Alexis Mousset
49a995f68d Merge branch 'master' of github.com:amousset/rust-smtp 2015-07-23 01:01:43 +02:00
Alexis Mousset
2fd5147a0f Update Cargo.toml information 2015-07-23 01:01:02 +02:00
Alexis Mousset
9c34b5a055 Fix badges formatting 2015-07-23 00:53:10 +02:00
Alexis Mousset
8d60068831 Remove last panics 2015-07-23 00:48:49 +02:00
Alexis Mousset
c2506b47fc License modified to MIT only 2015-07-15 22:53:12 +02:00
Alexis Mousset
cecc11f74e Prepare 0.1.0 2015-07-15 21:40:19 +02:00
Alexis Mousset
5ecdabbce7 Threads in example client 2015-07-15 20:10:34 +02:00
Alexis Mousset
bd4680d793 Remove openssl for now 2015-07-14 23:52:06 +02:00
Alexis Mousset
56f93bd614 Rename esmtp_features to features 2015-07-14 23:49:54 +02:00
Alexis Mousset
c9a657ac44 Add tests for ServerInfo 2015-07-14 23:45:12 +02:00
Alexis Mousset
df8c8a18a8 Refactoring 2015-07-14 22:49:47 +02:00
Alexis Mousset
e30e96c1ca Refactoring 2015-07-14 22:07:05 +02:00
Alexis Mousset
ef8c426cd4 Refactoring 2015-07-14 21:52:55 +02:00
Alexis Mousset
0b7e004ac8 Add an authentication trait 2015-07-14 14:16:17 +02:00
Alexis Mousset
599cf6c313 Add appveyor configuration 2015-07-14 11:27:02 +02:00
Alexis Mousset
e6b46faa06 Error management for authentication 2015-07-14 11:18:49 +02:00
Alexis Mousset
75de338409 Add error types 2015-07-14 10:32:12 +02:00
Alexis Mousset
29c9b7661e Rename SmtpError to Error 2015-07-14 01:10:28 +02:00
Alexis Mousset
d46bbeebf0 Improve SMTP response parsing 2015-07-08 21:16:41 +02:00
Alexis Mousset
ae3fc78e67 Fix coveralls.io badge 2015-07-07 09:31:42 +02:00
Alexis Mousset
26dae6ecbd Preparing 0.0.13 release 2015-07-05 10:26:27 +02:00
Alexis Mousset
e855e37182 Add tests on response 2015-06-28 20:46:05 +02:00
Alexis Mousset
3195959de8 rename mailer to email 2015-06-27 16:14:14 +02:00
Alexis Mousset
5b70dccfd3 Fix git url for rust-email 2015-06-27 14:43:30 +02:00
Alexis Mousset
3b78455b22 Add tests for Email 2015-06-27 14:37:13 +02:00
Alexis Mousset
cbc9764626 version 0.0.13 2015-06-27 11:12:19 +02:00
Alexis Mousset
8811b32113 Beginning of rust-email integration 2015-06-27 02:31:24 +02:00
Alexis Mousset
0b185352f8 Fix GH_TOKEN 2015-06-12 23:48:51 +02:00
Alexis Mousset
72390e9bdb Add empty unstable feature 2015-06-12 23:33:01 +02:00
Alexis Mousset
520db5d21d Test to fix travis-cargo 2015-06-12 22:55:22 +02:00
Alexis Mousset
bb0c9c5c5c Add travis-cargo to master 2015-06-12 22:17:46 +02:00
Alexis Mousset
262f3b8d93 Merge pull request #18 from nevdelap/master
Add #![feature(buf_stream)] to compile with current nightly.
2015-06-05 14:38:20 +02:00
Nev Delap
d1b6b33b44 Removed stable and beta builds. 2015-06-05 22:13:54 +10:00
Nev Delap
4f249b319c Add stable and beta builds. 2015-06-05 22:10:43 +10:00
Nev Delap
2281b9046e Add #![feature(buf_stream)] to compile with current nightly. 2015-06-05 22:05:10 +10:00
Alexis Mousset
a4ada1b6ef Remove uneeded use 2015-05-06 21:39:22 +02:00
Alexis Mousset
8da3a09140 Beta build status 2015-05-06 15:48:50 +02:00
Alexis Mousset
6ea49d770c Try travis-cargo 2015-05-06 15:48:15 +02:00
Alexis Mousset
6a5b100098 Do not build the doc on nightly 2015-05-06 14:45:29 +02:00
Alexis Mousset
5ed452919c Build on nightly only 2015-05-06 14:44:59 +02:00
Alexis Mousset
30583ef1a6 Build on beta only 2015-05-06 14:44:40 +02:00
Alexis Mousset
60356c3b52 Test on rust 1.0.0-beta.4 2015-05-06 14:26:09 +02:00
Alexis Mousset
ed06c67258 Test on rust 1.0.0-beta.3 2015-04-28 12:31:47 +02:00
Alexis Mousset
ee8da8196a Start refactoring streams to introduce a mock server 2015-04-22 21:52:03 +02:00
Alexis Mousset
78e88d5bc6 test beta2 on travis 2015-04-22 12:07:53 +02:00
Alexis Mousset
8f49ba3b17 add crates.io badge 2015-04-18 23:00:30 +02:00
Alexis Mousset
6d19f80842 Add tests for responses with empty message 2015-04-10 19:27:49 +02:00
Alexis Mousset
02a9093bad test beta on travis and bump to 0.0.11 2015-04-04 23:47:21 +02:00
Alexis Mousset
d59672ef26 Get ready for rust 1.0.0-beta 2015-04-04 23:32:14 +02:00
Alexis Mousset
7f9b2c5150 Fix for latest nightly 2015-04-04 22:34:01 +02:00
25 changed files with 1502 additions and 1587 deletions

23
.appveyor.yml Normal file
View 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
View File

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

View File

@@ -1,14 +1,21 @@
language: rust
sudo: required
after_success: |
[ $TRAVIS_BRANCH = master ] &&
[ $TRAVIS_PULL_REQUEST = false ] &&
cargo doc &&
echo '<meta http-equiv=refresh content=0;url=smtp/index.html>' > target/doc/index.html &&
sudo pip install ghp-import &&
ghp-import -n target/doc &&
git push -fq https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
rust:
- stable
- beta
- nightly
before_script:
- pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH
script:
- |
travis-cargo build &&
travis-cargo test &&
travis-cargo doc
after_success:
- travis-cargo --only stable doc-upload
- travis-cargo --only stable coveralls
env:
global:
secure: m8BXCWDfBh0Hkajp7+KdSFP65q3gUuJWJSb+W6BtAzWNxIuAwTgtm3Ja3gwwlko+XSOO4IG8+p+ApnduK1znWO+mslhUC7qr8oxIWRS3CzvSr3YeEwfVbWOz34/+I5cGh91ie8pbqxP+buMPVj078W3Cr/STzHhS2ZdQpjR/L8c=
secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc="

View File

@@ -1,7 +0,0 @@
Licensed under the Apache License, Version 2.0
<LICENSE-APACHE or
http://www.apache.org/licenses/LICENSE-2.0> or the MIT
license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
at your option. All files in the project carrying such
notice may not be copied, modified, or distributed except
according to those terms.

View File

@@ -1,20 +1,27 @@
[package]
name = "smtp"
version = "0.0.9"
version = "0.3.0"
description = "Simple SMTP client"
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"
homepage = "https://github.com/amousset/rust-smtp"
license = "MIT/Apache-2.0"
authors = ["Alexis Mousset <contact@amousset.me>"]
license = "MIT"
authors = ["Alexis Mousset <alexis.mousset@gmx.fr>"]
keywords = ["email", "smtp", "mailer"]
[dependencies]
time = "*"
uuid = "*"
log = "*"
env_logger = "*"
rustc-serialize = "*"
rust-crypto = "*"
time = "0.1"
uuid = "0.1"
log = "0.3"
rustc-serialize = "0.3"
rust-crypto = "0.2"
bufstream = "0.1"
email = "0.0"
openssl = "0.6"
[dev-dependencies]
env_logger = "0.3"
[features]
unstable = []

View File

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

View File

@@ -1,14 +1,9 @@
rust-smtp [![Build Status](https://travis-ci.org/amousset/rust-smtp.svg?branch=master)](https://travis-ci.org/amousset/rust-smtp)
rust-smtp [![Build Status](https://travis-ci.org/amousset/rust-smtp.svg?branch=master)](https://travis-ci.org/amousset/rust-smtp) [![Coverage Status](https://coveralls.io/repos/github/amousset/rust-smtp/badge.svg?branch=master)](https://coveralls.io/github/amousset/rust-smtp?branch=master) [![Crate](https://meritbadge.herokuapp.com/smtp)](https://crates.io/crates/smtp) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
=========
This library implements a simple SMTP client.
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
-------
@@ -16,40 +11,12 @@ To use this library, add the following to your `Cargo.toml`:
```toml
[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
-------
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.

View File

@@ -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]
extern crate log;
extern crate env_logger;
extern crate smtp;
extern crate getopts;
use std::old_io::stdin;
use std::string::String;
use std::env;
use getopts::{optopt, optflag, getopts, OptGroup, usage};
use std::net::TcpStream;
use std::sync::{Arc, Mutex};
use std::thread;
use smtp::sender::{Sender, SenderBuilder};
use smtp::error::SmtpResult;
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));
}
use smtp::sender::SenderBuilder;
use smtp::email::EmailBuilder;
fn main() {
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();
for arg in args {
args_string.push(arg.clone());
};
let mut threads = Vec::new();
for _ in 1..5 {
let program = args_string[0].clone();
let description = format!("Usage: {0} [options...] recipients\n\n\
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 th_sender = sender.clone();
threads.push(thread::spawn(move || {
let opts = [
optopt("n", "number", "set the number of emails to send", "NUMBER"),
optopt("s", "subject", "set the email subject", "SUBJECT"),
optopt("r", "reverse-path", "set the sender address", "FROM_ADDRESS"),
optopt("p", "port", "set the port to use, default is 25", "PORT"),
optopt("a", "server", "set the server to use, default is localhost", "SERVER"),
optopt("m", "my-hostname", "set the hostname used by the client", "MY_HOSTNAME"),
optflag("h", "help", "print this help menu"),
];
let email = EmailBuilder::new()
.to("user@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello")
.build();
let matches = match getopts(args_string.tail(), &opts) {
Ok(m) => m,
Err(f) => panic!("{}", f),
};
if matches.opt_present("h") {
print_usage(description, &opts);
return;
let _ = th_sender.lock().unwrap().send(email);
}));
}
if !matches.opt_present("r") {
println!("The sender option is required");
print_usage(program, &opts);
return;
for thread in threads {
let _ = thread.join();
}
let recipients_str: &str = if !matches.free.is_empty() {
matches.free[0].as_slice()
} else {
print_usage(description, &opts);
return;
};
let email = EmailBuilder::new()
.to("user@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello Bis")
.build();
let mut recipients = Vec::new();
for recipient in recipients_str.split(' ') {
recipients.push(recipient.to_string());
}
let mut sender = sender.lock().unwrap();
let result = sender.send(email);
sender.close();
let mut message = String::new();
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,
},
)
{
match result {
Ok(..) => info!("Email sent successfully"),
Err(error) => error!("{}", error),
Err(error) => error!("{:?}", error),
}
}

109
src/authentication.rs Normal file
View 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());
}
}

View File

@@ -1,51 +0,0 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Provides authentication functions
use serialize::base64::{self, ToBase64, FromBase64};
use serialize::hex::ToHex;
use crypto::hmac::Hmac;
use crypto::md5::Md5;
use crypto::mac::Mac;
use NUL;
/// Returns a PLAIN mecanism response
pub fn plain(username: &str, password: &str) -> String {
format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD)
}
/// Returns a CRAM-MD5 mecanism response
pub fn cram_md5(username: &str, password: &str, encoded_challenge: &str) -> String {
// TODO manage errors
let challenge = encoded_challenge.from_base64().unwrap();
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
hmac.input(&challenge);
format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD)
}
#[cfg(test)]
mod test {
use super::{plain, cram_md5};
#[test]
fn test_plain() {
assert_eq!(plain("username", "password").as_slice(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
}
#[test]
fn test_cram_md5() {
assert_eq!(cram_md5("alice", "wonderland",
"PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==").as_slice(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
}
}

View File

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

View File

@@ -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
use std::string::String;
use std::error::FromError;
use std::net::TcpStream;
use std::net::{SocketAddr, ToSocketAddrs};
use std::io::{BufRead, BufStream, Read, Write};
use std::net::ToSocketAddrs;
use std::io::{BufRead, Read, Write};
use std::io;
use std::fmt::Debug;
use response::{Response, Severity, Category};
use error::SmtpResult;
use client::connecter::Connecter;
use client::authentication::{plain, cram_md5};
use bufstream::BufStream;
use openssl::ssl::SslContext;
use response::ResponseParser;
use authentication::Mecanism;
use error::{Error, SmtpResult};
use client::net::{Connector, NetworkStream};
use {CRLF, MESSAGE_ENDING};
pub mod connecter;
mod authentication;
pub mod net;
/// Returns the string after adding a dot at the beginning of each line starting with a dot
///
@@ -33,8 +26,9 @@ fn escape_dot(string: &str) -> String {
format!(".{}", string)
} else {
string.to_string()
}.replace("\r.", "\r..")
.replace("\n.", "\n..")
}
.replace("\r.", "\r..")
.replace("\n.", "\n..")
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
@@ -43,49 +37,72 @@ fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CR><LF>")
}
/// Returns the string removing all the CRLF
#[inline]
fn remove_crlf(string: &str) -> String {
string.replace(CRLF, "")
}
/// Structure that implements the SMTP client
pub struct Client<S = TcpStream> {
pub struct Client<S: Write + Read = NetworkStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<BufStream<S>>,
/// Socket we are connecting to
server_addr: SocketAddr,
}
macro_rules! return_err (
($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
///
/// It does not connects to the server, but only creates the `Client`
pub fn new<A: ToSocketAddrs>(addr: A) -> Client<S> {
Client{
stream: None,
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
}
pub fn new() -> Client<S> {
Client { stream: None }
}
}
impl<S: Connecter + Write + Read = TcpStream> Client<S> {
impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.quit();
self.stream = None;
}
/// Sets the underlying stream
pub fn set_stream(&mut self, stream: S) {
self.stream = Some(BufStream::new(stream));
}
/// Upgrades the underlying connection to SSL/TLS
pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> {
//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
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
if self.stream.is_some() {
return_err!("The connection is already established", self);
}
let mut addresses = try!(addr.to_socket_addrs());
let server_addr = match addresses.next() {
Some(addr) => addr,
None => return_err!("Could not resolve hostname", self),
};
// 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()
}
@@ -100,12 +117,7 @@ impl<S: Connecter + Write + Read = TcpStream> Client<S> {
self.send_server(command, CRLF)
}
/// Send a HELO command and fills `server_info`
pub fn helo(&mut self, hostname: &str) -> SmtpResult {
self.command(&format!("HELO {}", hostname))
}
/// Sends a EHLO command and fills `server_info`
/// Sends a EHLO command
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
self.command(&format!("EHLO {}", hostname))
}
@@ -161,18 +173,35 @@ impl<S: Connecter + Write + Read = TcpStream> Client<S> {
self.command("RSET")
}
/// Sends an AUTH command with PLAIN mecanism
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
self.command(&format!("AUTH PLAIN {}", plain(username, password)))
/// Sends an AUTH command with the given mecanism
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
if mecanism.supports_initial_response() {
self.command(&format!("AUTH {} {}",
mecanism,
try!(mecanism.response(username, password, None))))
} else {
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
Some(challenge) => challenge,
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
};
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
pub fn auth_cram_md5(&mut self, username: &str, password: &str) -> SmtpResult {
let encoded_challenge = try!(self.command("AUTH CRAM-MD5")).first_word().expect("No challenge");
self.command(&format!("AUTH CRAM-MD5 {}", cram_md5(username, password, &encoded_challenge)))
/// Sends a STARTTLS command
pub fn starttls(&mut self) -> SmtpResult {
self.command("STARTTLS")
}
/// Sends the message content and close
/// Sends the message content
pub fn message(&mut self, message_content: &str) -> SmtpResult {
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
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
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));
@@ -193,61 +222,53 @@ impl<S: Connecter + Write + Read = TcpStream> Client<S> {
/// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult {
let mut parser = ResponseParser::new();
let mut line = String::new();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
// If the string is too short to be a response code
if line.len() < 3 {
return Err(FromError::from_error("Could not parse reply code, line too short"));
debug!("Read: {}", escape_crlf(line.as_ref()));
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
line.clear();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
}
let (severity, category, detail) = match (line[0..1].parse::<Severity>(), line[1..2].parse::<Category>(), line[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => (severity, category, detail),
_ => return Err(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();
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() {
true => Ok(response),
false => Err(FromError::from_error(response)),
false => Err(From::from(response)),
}
}
}
#[cfg(test)]
mod test {
use super::{escape_dot, escape_crlf};
use super::{escape_dot, remove_crlf, escape_crlf};
#[test]
fn test_escape_dot() {
assert_eq!(escape_dot(".test").as_slice(), "..test");
assert_eq!(escape_dot("\r.\n.\r\n").as_slice(), "\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.\r\ntest").as_slice(), "test\r\n..\r\ntest");
assert_eq!(escape_dot(".test"), "..test");
assert_eq!(escape_dot("\r.\n.\r\n"), "\r..\n..\r\n");
assert_eq!(escape_dot("test\r\n.test\r\n"), "test\r\n..test\r\n");
assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest");
}
#[test]
fn test_remove_crlf() {
assert_eq!(remove_crlf("\r\n"), "");
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
assert_eq!(remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_nameSIZE 42");
}
#[test]
fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n").as_slice(), "<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\nSIZE 42\r\n").as_slice(),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
);
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
}
}

94
src/client/net.rs Normal file
View 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
View 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));
}
}

View File

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

View File

@@ -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
use std::str::FromStr;
use std::result::Result;
use std::fmt::{Display, Formatter};
use std::fmt;
use std::collections::HashSet;
use response::Response;
use self::Extension::*;
use error::Error;
use authentication::Mecanism;
/// Supported ESMTP keywords
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
pub enum Extension {
/// 8BITMIME keyword
///
@@ -30,79 +24,197 @@ pub enum Extension {
///
/// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls,
/// AUTH PLAIN mecanism
///
/// RFC 4616: https://tools.ietf.org/html/rfc4616
PlainAuthentication,
/// AUTH CRAM-MD5 mecanism
///
/// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5Authentication,
/// AUTH mecanism
Authentication(Mecanism),
}
impl Extension {
fn from_str(s: &str) -> Result<Vec<Extension>, &'static str> {
let splitted : Vec<&str> = s.split(' ').collect();
match (splitted[0], splitted.len()) {
("8BITMIME", 1) => Ok(vec![EightBitMime]),
("SMTPUTF8", 1) => Ok(vec![SmtpUtfEight]),
("STARTTLS", 1) => Ok(vec![StartTls]),
("AUTH", _) => {
let mut mecanisms: Vec<Extension> = vec![];
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => mecanisms.push(PlainAuthentication),
"CRAM-MD5" => mecanisms.push(CramMd5Authentication),
_ => (),
}
}
Ok(mecanisms)
},
_ => Err("Unknown extension"),
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
Extension::StartTls => write!(f, "{}", "STARTTLS"),
Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism),
}
}
}
/// Parses supported ESMTP features
pub fn parse_esmtp_response(response: &Response) -> Vec<Extension> {
let mut esmtp_features: Vec<Extension> = Vec::new();
/// 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() {
if let Ok(keywords) = Extension::from_str(&line) {
esmtp_features.push_all(&keywords);
let splitted: Vec<&str> = line.split_whitespace().collect();
let _ = match splitted[0] {
"8BITMIME" => {
features.insert(Extension::EightBitMime);
}
"SMTPUTF8" => {
features.insert(Extension::SmtpUtfEight);
}
"STARTTLS" => {
features.insert(Extension::StartTls);
}
"AUTH" => {
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mecanism::Plain));
}
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mecanism::CramMd5));
}
_ => (),
}
}
}
_ => (),
};
}
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)]
mod test {
use super::Extension;
use response::{Severity, Category, Response};
use std::collections::HashSet;
use super::{ServerInfo, Extension};
use authentication::Mecanism;
use response::{Code, Response, Severity, Category};
#[test]
fn test_from_str() {
assert_eq!(Extension::from_str("8BITMIME"), Ok(vec![Extension::EightBitMime]));
assert_eq!(Extension::from_str("AUTH PLAIN"), Ok(vec![Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH PLAIN LOGIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
assert_eq!(Extension::from_str("AUTH CRAM-MD5 PLAIN"), Ok(vec![Extension::CramMd5Authentication, Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
fn test_extension_fmt() {
assert_eq!(format!("{}", Extension::EightBitMime),
"8BITMIME".to_string());
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)),
"AUTH PLAIN".to_string());
}
#[test]
fn test_parse_esmtp_response() {
assert_eq!(Extension::parse_esmtp_response(&Response::new(
"2".parse::<Severity>().unwrap(),
"2".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
)), vec![Extension::EightBitMime]);
assert_eq!(Extension::parse_esmtp_response(&Response::new(
"4".parse::<Severity>().unwrap(),
"3".parse::<Category>().unwrap(),
3,
vec!["me".to_string(), "8BITMIME".to_string(), "AUTH PLAIN CRAM-MD5".to_string()]
)), vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
fn test_serverinfo_fmt() {
let mut eightbitmime = HashSet::new();
assert!(eightbitmime.insert(Extension::EightBitMime));
assert_eq!(format!("{}",
ServerInfo {
name: "name".to_string(),
features: eightbitmime.clone(),
}),
"name with {EightBitMime}".to_string());
let empty = HashSet::new();
assert_eq!(format!("{}",
ServerInfo {
name: "name".to_string(),
features: empty,
}),
"name with no supported features".to_string());
let mut plain = HashSet::new();
assert!(plain.insert(Extension::Authentication(Mecanism::Plain)));
assert_eq!(format!("{}",
ServerInfo {
name: "name".to_string(),
features: plain.clone(),
}),
"name with {Authentication(Plain)}".to_string());
}
#[test]
fn test_serverinfo() {
let response = Response::new(Code::new(Severity::PositiveCompletion,
Category::Unspecified4,
1),
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]);
let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo {
name: "me".to_string(),
features: features,
};
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
assert!(server_info.supports_feature(&Extension::EightBitMime));
assert!(!server_info.supports_feature(&Extension::StartTls));
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
let response2 = Response::new(Code::new(Severity::PositiveCompletion,
Category::Unspecified4,
1),
vec!["me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]);
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
let server_info2 = ServerInfo {
name: "me".to_string(),
features: features2,
};
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
assert!(server_info2.supports_feature(&Extension::EightBitMime));
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain));
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5));
assert!(!server_info2.supports_feature(&Extension::StartTls));
}
}

View File

@@ -1,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
//!
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
//! a work in progress. It is designed to efficiently send emails from a rust application to a
//! relay email server.
//! a work in progress. It is designed to efficiently send emails from an application to a
//! relay email server, as it relies as much as possible on the relay server for sanity and RFC
//! compliance checks.
//!
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954))
//! * 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:
//!
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
//! ## Architecture
//!
//! This client is divided into three parts:
//! This client is divided into three main parts:
//!
//! * client: a low level SMTP client providing all SMTP commands
//! * sender: a high level SMTP client providing an easy method to send emails
//! * mailer: generates the email to be sent with the sender
//! * email: generates the email to be sent with the sender
//!
//! ## Usage
//!
@@ -39,8 +31,7 @@
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::mailer::EmailBuilder;
//! use std::net::TcpStream;
//! use smtp::email::EmailBuilder;
//!
//! // Create an email
//! let email = EmailBuilder::new()
@@ -53,7 +44,7 @@
//! .build();
//!
//! // 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
//! let result = sender.send(email);
//!
@@ -64,8 +55,9 @@
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::mailer::EmailBuilder;
//! use std::net::TcpStream;
//! use smtp::email::EmailBuilder;
//! use smtp::authentication::Mecanism;
//! use smtp::SUBMISSION_PORT;
//!
//! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name"));
@@ -81,11 +73,16 @@
//! let email = builder.build();
//!
//! // 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`
//! .hello_name("my.hostname.tld")
//! // Add credentials for authentication
//! .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(true).build();
//!
@@ -106,8 +103,7 @@
//!
//! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::sendable_email::SimpleSendableEmail;
//! use std::net::TcpStream;
//! use smtp::email::SimpleSendableEmail;
//!
//! // Create a minimal email
//! let email = SimpleSendableEmail::new(
@@ -116,7 +112,7 @@
//! "Hello world !"
//! );
//!
//! let mut sender: Sender<TcpStream> = SenderBuilder::localhost().build();
//! let mut sender = SenderBuilder::localhost().unwrap().build();
//! let result = sender.send(email);
//! assert!(result.is_ok());
//! ```
@@ -128,10 +124,10 @@
//! ```rust,no_run
//! use smtp::client::Client;
//! 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 _ = email_client.connect();
//! let mut email_client: Client<NetworkStream> = Client::new();
//! let _ = email_client.connect(&("localhost", SMTP_PORT));
//! let _ = email_client.ehlo("my_hostname");
//! let _ = email_client.mail("user@example.com", None);
//! let _ = email_client.rcpt("user@example.org");
@@ -140,22 +136,25 @@
//! let _ = email_client.quit();
//! ```
#![feature(plugin, core, collections, str_words)]
#![deny(missing_docs)]
#[macro_use] extern crate log;
extern crate "rustc-serialize" as serialize;
#[macro_use]
extern crate log;
extern crate rustc_serialize as serialize;
extern crate crypto;
extern crate time;
extern crate uuid;
extern crate email as email_format;
extern crate bufstream;
extern crate openssl;
mod extension;
pub mod client;
pub mod sender;
pub mod response;
pub mod error;
pub mod sendable_email;
pub mod mailer;
pub mod authentication;
pub mod email;
// Registrated port numbers:
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml

View File

@@ -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()
);
}
}

View File

@@ -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()
);
}
}

View File

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

View File

@@ -1,20 +1,12 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! SMTP response, containing a mandatory return code, and an optional text message
//! SMTP response, containing a mandatory return code and an optional text message
use std::str::FromStr;
use std::fmt::{Display, Formatter, Result};
use std::result::Result as RResult;
use std::result;
use self::Severity::*;
use self::Category::*;
use error::{SmtpResult, Error};
/// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
@@ -30,28 +22,28 @@ pub enum Severity {
}
impl FromStr for Severity {
type Err = &'static str;
fn from_str(s: &str) -> RResult<Severity, &'static str> {
type Err = Error;
fn from_str(s: &str) -> result::Result<Severity, Error> {
match s {
"2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err("First digit must be between 2 and 5"),
_ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
}
}
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
match *self {
PositiveCompletion => 2,
PositiveIntermediate => 3,
TransientNegativeCompletion => 4,
PermanentNegativeCompletion => 5,
}
)
write!(f,
"{}",
match *self {
PositiveCompletion => 2,
PositiveIntermediate => 3,
TransientNegativeCompletion => 4,
PermanentNegativeCompletion => 5,
})
}
}
@@ -73,8 +65,8 @@ pub enum Category {
}
impl FromStr for Category {
type Err = &'static str;
fn from_str(s: &str) -> RResult<Category, &'static str> {
type Err = Error;
fn from_str(s: &str) -> result::Result<Category, Error> {
match s {
"0" => Ok(Syntax),
"1" => Ok(Information),
@@ -82,23 +74,130 @@ impl FromStr for Category {
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"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 {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
match *self {
Syntax => 0,
Information => 1,
Connections => 2,
Unspecified3 => 3,
Unspecified4 => 4,
MailSystem => 5,
write!(f,
"{}",
match *self {
Syntax => 0,
Information => 1,
Connections => 2,
Unspecified3 => 3,
Unspecified4 => 4,
MailSystem => 5,
})
}
}
/// Represents a 3 digit SMTP response code
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Code {
/// First digit of the response code
severity: Severity,
/// Second digit of the response code
category: Category,
/// Third digit
detail: u8,
}
impl FromStr for Code {
type Err = Error;
#[inline]
fn from_str(s: &str) -> result::Result<Code, Error> {
if s.len() == 3 {
match (s[0..1].parse::<Severity>(),
s[1..2].parse::<Category>(),
s[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {
severity: severity,
category: category,
detail: detail,
}),
_ => 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
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Response {
/// First digit of the response code
severity: Severity,
/// Second digit of the response code
category: Category,
/// Third digit
detail: u8,
/// Response code
code: Code,
/// Server response string (optional)
/// Handle multiline responses
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]
)
}
message: Vec<String>,
}
impl 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 {
severity: severity,
category: category,
detail: detail,
message: message
code: code,
message: message,
}
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.severity {
match self.code.severity {
PositiveCompletion => true,
PositiveIntermediate => true,
_ => false,
@@ -162,25 +238,25 @@ impl Response {
/// Returns the severity (i.e. 1st digit)
pub fn severity(&self) -> Severity {
self.severity
self.code.severity
}
/// Returns the category (i.e. 2nd digit)
pub fn category(&self) -> Category {
self.category
self.code.category
}
/// Returns the detail (i.e. 3rd digit)
pub fn detail(&self) -> u8 {
self.detail
self.code.detail
}
/// Returns the reply code
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 {
self.code() == format!("{}", code)
}
@@ -189,156 +265,324 @@ impl Response {
pub fn first_word(&self) -> Option<String> {
match self.message.is_empty() {
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)]
mod test {
use super::{Severity, Category, Response};
use super::{Severity, Category, Response, ResponseParser, Code};
#[test]
fn test_severity_from_str() {
assert_eq!("2".parse::<Severity>(), Ok(Severity::PositiveCompletion));
assert_eq!("4".parse::<Severity>(), Ok(Severity::TransientNegativeCompletion));
assert_eq!("2".parse::<Severity>().unwrap(),
Severity::PositiveCompletion);
assert_eq!("4".parse::<Severity>().unwrap(),
Severity::TransientNegativeCompletion);
assert!("1".parse::<Severity>().is_err());
}
#[test]
fn test_severity_fmt() {
assert_eq!(format!("{}", Severity::PositiveCompletion).as_slice(), "2");
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
}
#[test]
fn test_category_from_str() {
assert_eq!("2".parse::<Category>(), Ok(Category::Connections));
assert_eq!("4".parse::<Category>(), Ok(Category::Unspecified4));
assert_eq!("2".parse::<Category>().unwrap(), Category::Connections);
assert_eq!("4".parse::<Category>().unwrap(), Category::Unspecified4);
assert!("6".parse::<Category>().is_err());
}
#[test]
fn test_category_fmt() {
assert_eq!(format!("{}", Category::Unspecified4).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]
fn test_response_new() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
), Response {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
});
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()]),
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()],
});
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![]),
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::Unspecified4,
detail: 1,
},
message: vec![],
});
}
#[test]
fn test_response_parser() {
let mut parser = ResponseParser::new();
assert!(parser.read_line("250-me").unwrap());
assert!(parser.read_line("250-8BITMIME").unwrap());
assert!(parser.read_line("250-SIZE 42").unwrap());
assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap());
let response = parser.response().unwrap();
assert_eq!(response,
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: 0,
},
message: vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"AUTH PLAIN CRAM-MD5".to_string()],
});
}
#[test]
fn test_response_is_positive() {
assert!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
assert!(! Response::new(
"4".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive());
assert!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.is_positive());
assert!(!Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.is_positive());
}
#[test]
fn test_response_message() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.message(),
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
let empty_message: Vec<String> = vec![];
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![])
.message(),
empty_message);
}
#[test]
fn test_response_severity() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PositiveCompletion);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.severity(),
Severity::PositiveCompletion);
assert_eq!(Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.severity(),
Severity::PermanentNegativeCompletion);
}
#[test]
fn test_response_category() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).category(), Category::Unspecified4);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.category(),
Category::Unspecified4);
}
#[test]
fn test_response_detail() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).detail(), 1);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.detail(),
1);
}
#[test]
fn test_response_code() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).code(), "241");
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.code(),
"241");
}
#[test]
fn test_response_has_code() {
assert!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(241));
assert!(! Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(251));
assert!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(241));
assert!(!Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(251));
}
#[test]
fn test_response_first_word() {
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string()));
assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(),
"4".parse::<Category>().unwrap(),
1,
vec![]
).first_word(), None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.first_word(),
Some("me".to_string()));
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.first_word(),
Some("me".to_string()));
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![])
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![" ".to_string()])
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec![" ".to_string()])
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["".to_string()])
.first_word(),
None);
}
}

View File

@@ -1,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;
}
}

View File

@@ -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
use std::string::String;
use std::net::TcpStream;
use std::net::{SocketAddr, ToSocketAddrs};
use std::io::{Read, Write};
use uuid::Uuid;
use openssl::ssl::{SslMethod, SslContext};
use SMTP_PORT;
use extension::Extension;
use error::{SmtpResult, SmtpError};
use sendable_email::SendableEmail;
use sender::server_info::ServerInfo;
use extension::{Extension, ServerInfo};
use error::{SmtpResult, Error};
use email::SendableEmail;
use client::Client;
use client::connecter::Connecter;
mod server_info;
use authentication::Mecanism;
/// Contains client configuration
pub struct SenderBuilder {
@@ -40,26 +26,48 @@ pub struct SenderBuilder {
credentials: Option<(String, String)>,
/// Socket we are connecting to
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
impl SenderBuilder {
/// Creates a new local SMTP client
pub fn new<A: ToSocketAddrs>(addr: A) -> SenderBuilder {
SenderBuilder {
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
credentials: None,
connection_reuse_count_limit: 100,
enable_connection_reuse: false,
hello_name: "localhost".to_string(),
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SenderBuilder, Error> {
let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() {
Some(addr) => Ok(SenderBuilder {
server_addr: addr,
ssl_context: None,
credentials: None,
connection_reuse_count_limit: 100,
enable_connection_reuse: false,
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
pub fn localhost() -> SenderBuilder {
pub fn localhost() -> Result<SenderBuilder, Error> {
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
pub fn hello_name(mut self, name: &str) -> SenderBuilder {
self.hello_name = name.to_string();
@@ -84,10 +92,16 @@ impl SenderBuilder {
self
}
/// Set the authentication mecanisms
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SenderBuilder {
self.authentication_mecanisms = mecanisms;
self
}
/// Build the SMTP client
///
/// 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)
}
}
@@ -102,7 +116,7 @@ struct State {
}
/// Structure that implements the high level SMTP client
pub struct Sender<S = TcpStream> {
pub struct Sender {
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
@@ -111,7 +125,7 @@ pub struct Sender<S = TcpStream> {
/// Information about the client
client_info: SenderBuilder,
/// Low level client
client: Client<S>,
client: Client,
}
macro_rules! try_smtp (
@@ -121,7 +135,7 @@ macro_rules! try_smtp (
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.client.close();
$client.reset();
}
return Err(err)
},
@@ -129,13 +143,15 @@ macro_rules! try_smtp (
})
);
impl<S = TcpStream> Sender<S> {
impl Sender {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `Sender`
pub fn new(builder: SenderBuilder) -> Sender<S> {
let client: Client<S> = Client::new(builder.server_addr);
Sender{
pub fn new(builder: SenderBuilder) -> Sender {
let client = Client::new();
Sender {
client: client,
server_info: None,
client_info: builder,
@@ -145,9 +161,7 @@ impl<S = TcpStream> Sender<S> {
},
}
}
}
impl<S: Connecter + Write + Read = TcpStream> Sender<S> {
/// Reset the client state
fn reset(&mut self) {
// Close the SMTP transaction if needed
@@ -164,8 +178,21 @@ impl<S: Connecter + Write + Read = TcpStream> Sender<S> {
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
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
if self.state.connection_reuse_count > 0 {
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 self.state.connection_reuse_count == 0 {
try!(self.client.connect());
try!(self.client.connect(&self.client_info.server_addr));
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
// Extended Hello or Hello if needed
match self.client.ehlo(&self.client_info.hello_name) {
Ok(response) => {self.server_info = Some(
ServerInfo{
name: response.first_word().expect("Server announced no hostname"),
esmtp_features: Extension::parse_esmtp_response(&response),
});
},
Err(error) => match error {
SmtpError::PermanentError(ref response) if response.has_code(550) => {
match self.client.helo(&self.client_info.hello_name) {
Ok(response) => {self.server_info = Some(
ServerInfo{
name: response.first_word().expect("Server announced no hostname"),
esmtp_features: vec!(),
});
},
Err(error) => try_smtp!(Err(error), self)
}
try!(self.get_ehlo());
},
_ => {
try_smtp!(Err(error), self)
},
},
if self.client_info.ssl_context.is_some() {
try_smtp!(self.client.starttls(), self);
try!(self.client
.upgrade_tls_stream(self.client_info.ssl_context.as_ref().unwrap()));
try!(self.get_ehlo());
}
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
}
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
let (username, password) = self.client_info.credentials.clone().unwrap();
// TODO: Use PLAIN AUTH in encrypted connections, CRAM-MD5 otherwise
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
let mut found = false;
let (username, password) = self.client_info.credentials.clone().unwrap();
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);
}
}
if self.server_info.as_ref().unwrap().supports_feature(Extension::CramMd5Authentication) {
let result = self.client.auth_cram_md5(&username, &password);
try_smtp!(result, self);
} else if self.server_info.as_ref().unwrap().supports_feature(Extension::PlainAuthentication) {
let result = self.client.auth_plain(&username, &password);
try_smtp!(result, self);
} else {
debug!("No supported authentication mecanisms available");
if !found {
debug!("No supported authentication mecanisms available");
}
}
}
let current_message = Uuid::new_v4();
email.set_message_id(format!("<{}@{}>", current_message,
self.client_info.hello_name.clone()));
let from_address = email.from_address();
let to_addresses = email.to_addresses();
let message = email.message();
let current_message = try!(email.message_id().ok_or("Missing Message-ID"));
let from_address = try!(email.from_address().ok_or("Missing From address"));
let to_addresses = try!(email.to_addresses().ok_or("Missing To address"));
let message = try!(email.message().ok_or("Missing message"));
// Mail
let mail_options = match self.server_info.as_ref().unwrap().supports_feature(Extension::EightBitMime) {
let mail_options = match self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::EightBitMime) {
true => Some("BODY=8BITMIME"),
false => None,
};
@@ -264,17 +274,22 @@ impl<S: Connecter + Write + Read = TcpStream> Sender<S> {
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
// Log the message
info!("{}: conn_use={}, size={}, status=sent ({})", current_message,
self.state.connection_reuse_count, message.len(), match result.as_ref().ok().unwrap().message().as_slice() {
[ref line, ..] => line.as_slice(),
[] => "no response",
}
);
info!("{}: conn_use={}, size={}, status=sent ({})",
current_message,
self.state.connection_reuse_count,
message.len(),
result.as_ref()
.ok()
.unwrap()
.message()
.iter()
.next()
.unwrap_or(&"no response".to_string()));
}
// Test if we can reuse the existing connection
if (!self.client_info.enable_connection_reuse) ||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
self.reset();
}

View File

@@ -1,85 +0,0 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Information about a server
use std::fmt;
use std::fmt::{Display, Formatter};
use extension::Extension;
/// Contains information about an SMTP server
#[derive(Clone,Debug)]
pub struct ServerInfo {
/// Server name
///
/// The name given in the server banner
pub name: String,
/// ESMTP features supported by the server
///
/// It contains the features supported by the server and known by the `Extension` module.
pub esmtp_features: Vec<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{} with {}",
self.name,
match self.esmtp_features.is_empty() {
true => "no supported features".to_string(),
false => format! ("{:?}", self.esmtp_features),
}
)
}
}
impl ServerInfo {
/// Checks if the server supports an ESMTP feature
pub fn supports_feature(&self, keyword: Extension) -> bool {
self.esmtp_features.contains(&keyword)
}
}
#[cfg(test)]
mod test {
use super::ServerInfo;
use extension::Extension;
#[test]
fn test_fmt() {
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::EightBitMime]
}), "name with [EightBitMime]".to_string());
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::EightBitMime]
}), "name with [EightBitMime]".to_string());
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: vec![]
}), "name with no supported features".to_string());
}
#[test]
fn test_supports_feature() {
assert!(ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::EightBitMime]
}.supports_feature(Extension::EightBitMime));
assert!(ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::PlainAuthentication, Extension::EightBitMime]
}.supports_feature(Extension::EightBitMime));
assert_eq!(ServerInfo{
name: "name".to_string(),
esmtp_features: vec![Extension::EightBitMime]
}.supports_feature(Extension::PlainAuthentication), false);
}
}