Compare commits

...

31 Commits

Author SHA1 Message Date
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
21 changed files with 1135 additions and 993 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/ /target/
/Cargo.lock /Cargo.lock

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,14 +1,13 @@
[package] [package]
name = "smtp" name = "smtp"
version = "0.0.13" version = "0.2.0"
description = "Simple SMTP client" description = "Simple SMTP client"
readme = "README.md" readme = "README.md"
documentation = "http://amousset.github.io/rust-smtp/smtp/" documentation = "http://amousset.me/rust-smtp/smtp/"
repository = "https://github.com/amousset/rust-smtp" repository = "https://github.com/amousset/rust-smtp"
homepage = "https://github.com/amousset/rust-smtp" license = "MIT"
license = "MIT/Apache-2.0" authors = ["Alexis Mousset <alexis.mousset@gmx.fr>"]
authors = ["Alexis Mousset <contact@amousset.me>"]
keywords = ["email", "smtp", "mailer"] keywords = ["email", "smtp", "mailer"]
[dependencies] [dependencies]
@@ -18,12 +17,10 @@ log = "0.3"
rustc-serialize = "0.3" rustc-serialize = "0.3"
rust-crypto = "0.2" rust-crypto = "0.2"
bufstream = "0.1" bufstream = "0.1"
email = "0.0"
[dependencies.email]
git = "https://github.com/amousset/rust-email.git"
[dev-dependencies] [dev-dependencies]
env_logger = "*" env_logger = "0.3"
[features] [features]
unstable = [] 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,4 +1,4 @@
rust-smtp [![Build Status](https://travis-ci.org/amousset/rust-smtp.svg?branch=master)](https://travis-ci.org/amousset/rust-smtp) [![Coverage Status](https://coveralls.io/repos/amousset/rust-smtp/badge.svg?branch=master)](https://coveralls.io/r/amousset/rust-smtp?branch=master) [![](https://meritbadge.herokuapp.com/smtp)](https://crates.io/crates/smtp) 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. This library implements a simple SMTP client.
@@ -11,22 +11,12 @@ To use this library, add the following to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
smtp = "*" smtp = "0.2"
``` ```
Tests
-----
You can build and run the tests with `cargo test`.
Documentation
-------------
You can build the documentation with `cargo doc`. It is also available on [GitHub pages](http://amousset.github.io/rust-smtp/smtp/).
License License
------- -------
This program is distributed under the terms of both the MIT license and the Apache License (Version 2.0). This program is distributed under the terms of the MIT license.
See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details. See LICENSE for details.

View File

@@ -1,37 +1,49 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate env_logger; extern crate env_logger;
extern crate smtp; extern crate smtp;
use smtp::sender::{Sender, SenderBuilder}; use std::sync::{Arc, Mutex};
use smtp::mailer::EmailBuilder; use std::thread;
use smtp::sender::SenderBuilder;
use smtp::email::EmailBuilder;
fn main() { fn main() {
env_logger::init().unwrap(); env_logger::init().unwrap();
let mut email_builder = EmailBuilder::new(); let sender = Arc::new(Mutex::new(SenderBuilder::localhost().unwrap().hello_name("localhost")
.enable_connection_reuse(true).build()));
email_builder = email_builder.to("user@localhost"); let mut threads = Vec::new();
let email = email_builder.from("user@localhost") for _ in 1..5 {
.body("Hello World!")
.subject("Hello") let th_sender = sender.clone();
.build(); threads.push(thread::spawn(move || {
let mut sender: Sender = SenderBuilder::localhost().hello_name("localhost") let email = EmailBuilder::new()
.enable_connection_reuse(true).build(); .to("user@localhost")
.from("user@localhost")
for _ in (1..5) { .body("Hello World!")
let _ = sender.send(email.clone()); .subject("Hello")
.build();
let _ = th_sender.lock().unwrap().send(email);
}));
} }
for thread in threads {
let _ = thread.join();
}
let email = EmailBuilder::new()
.to("user@localhost")
.from("user@localhost")
.body("Hello World!")
.subject("Hello Bis")
.build();
let mut sender = sender.lock().unwrap();
let result = sender.send(email); let result = sender.send(email);
sender.close(); sender.close();

97
src/authentication.rs Normal file
View File

@@ -0,0 +1,97 @@
//! 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"), "AHVzZXJuYW1lAHBhc3N3b3Jk");
}
#[test]
fn test_cram_md5() {
assert_eq!(cram_md5("alice", "wonderland",
"PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
}
}

View File

@@ -1,12 +1,3 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! SMTP client //! SMTP client
use std::string::String; use std::string::String;
@@ -15,14 +6,13 @@ use std::io::{BufRead, Read, Write};
use bufstream::BufStream; use bufstream::BufStream;
use response::{Response, Severity, Category}; use response::ResponseParser;
use error::SmtpResult; use authentication::Mecanism;
use error::{Error, SmtpResult};
use client::net::{Connector, SmtpStream}; use client::net::{Connector, SmtpStream};
use client::authentication::{plain, cram_md5};
use {CRLF, MESSAGE_ENDING}; use {CRLF, MESSAGE_ENDING};
pub mod net; pub mod net;
mod authentication;
/// Returns the string after adding a dot at the beginning of each line starting with a dot /// Returns the string after adding a dot at the beginning of each line starting with a dot
/// ///
@@ -43,6 +33,12 @@ fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CR><LF>") string.replace(CRLF, "<CR><LF>")
} }
/// Returns the string removing all the CRLF
#[inline]
fn remove_crlf(string: &str) -> String {
string.replace(CRLF, "")
}
/// Structure that implements the SMTP client /// Structure that implements the SMTP client
pub struct Client<S: Write + Read = SmtpStream> { pub struct Client<S: Write + Read = SmtpStream> {
/// TCP stream between client and server /// TCP stream between client and server
@@ -50,7 +46,6 @@ pub struct Client<S: Write + Read = SmtpStream> {
stream: Option<BufStream<S>>, stream: Option<BufStream<S>>,
/// Socket we are connecting to /// Socket we are connecting to
server_addr: SocketAddr, server_addr: SocketAddr,
} }
macro_rules! return_err ( macro_rules! return_err (
@@ -63,11 +58,16 @@ impl<S: Write + Read = SmtpStream> Client<S> {
/// Creates a new SMTP client /// Creates a new SMTP client
/// ///
/// It does not connects to the server, but only creates the `Client` /// It does not connects to the server, but only creates the `Client`
pub fn new<A: ToSocketAddrs>(addr: A) -> Client<S> { pub fn new<A: ToSocketAddrs>(addr: A) -> Result<Client<S>, Error> {
Client{ let mut addresses = try!(addr.to_socket_addrs());
stream: None,
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(), match addresses.next() {
} Some(addr) => Ok(Client {
stream: None,
server_addr: addr,
}),
None => Err(From::from("Could nor resolve hostname")),
}
} }
} }
@@ -162,15 +162,21 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
self.command("RSET") self.command("RSET")
} }
/// Sends an AUTH command with PLAIN mecanism /// Sends an AUTH command with the given mecanism
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult { pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
self.command(&format!("AUTH PLAIN {}", plain(username, password)))
}
/// Sends an AUTH command with CRAM-MD5 mecanism if mecanism.supports_initial_response() {
pub fn auth_cram_md5(&mut self, username: &str, password: &str) -> SmtpResult { self.command(&format!("AUTH {} {}", mecanism, try!(mecanism.response(username, password, None))))
let encoded_challenge = try!(self.command("AUTH CRAM-MD5")).first_word().expect("No challenge"); } else {
self.command(&format!("AUTH CRAM-MD5 {}", cram_md5(username, password, &encoded_challenge))) let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
Some(challenge) => challenge,
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
};
let cram_response = try!(mecanism.response(username, password, Some(&encoded_challenge)));
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
}
} }
/// Sends the message content /// Sends the message content
@@ -194,34 +200,18 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
/// Gets the SMTP response /// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult { fn get_reply(&mut self) -> SmtpResult {
let mut parser = ResponseParser::new();
let mut line = String::new(); let mut line = String::new();
try!(self.stream.as_mut().unwrap().read_line(&mut line)); try!(self.stream.as_mut().unwrap().read_line(&mut line));
// If the string is too short to be a response code while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
if line.len() < 3 { line.clear();
return Err(From::from("Could not parse reply code, line too short")); 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>()) { let response = try!(parser.response());
(Ok(severity), Ok(category), Ok(detail)) => (severity, category, detail),
_ => return Err(From::from("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);
match response.is_positive() { match response.is_positive() {
true => Ok(response), true => Ok(response),
@@ -232,7 +222,7 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{escape_dot, escape_crlf}; use super::{escape_dot, remove_crlf, escape_crlf};
#[test] #[test]
fn test_escape_dot() { fn test_escape_dot() {
@@ -242,6 +232,16 @@ mod test {
assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest"); assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest");
} }
#[test]
fn test_remove_crlf() {
assert_eq!(remove_crlf("\r\n"), "");
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
assert_eq!(
remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_nameSIZE 42"
);
}
#[test] #[test]
fn test_escape_crlf() { fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CR><LF>"); assert_eq!(escape_crlf("\r\n"), "<CR><LF>");

View File

@@ -1,12 +1,3 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! A trait to represent a stream //! A trait to represent a stream
use std::io; use std::io;
@@ -28,3 +19,5 @@ impl Connector for SmtpStream {
/// Represents an atual SMTP network stream /// Represents an atual SMTP network stream
//Used later for ssl //Used later for ssl
pub type SmtpStream = TcpStream; pub type SmtpStream = TcpStream;

355
src/email.rs Normal file
View File

@@ -0,0 +1,355 @@
//! 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,87 +1,88 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Error and result type for SMTP clients //! Error and result type for SMTP clients
use std::error::Error; use std::error::Error as StdError;
use std::io; use std::io;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::fmt; use std::fmt;
use response::{Severity, Response}; use response::{Severity, Response};
use self::SmtpError::*; use serialize::base64::FromBase64Error;
use self::Error::*;
/// An enum of all error kinds. /// An enum of all error kinds.
#[derive(Debug)] #[derive(Debug)]
pub enum SmtpError { pub enum Error {
/// Transient error, 4xx reply code /// Transient SMTP error, 4xx reply code
/// ///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1) /// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
TransientError(Response), TransientError(Response),
/// Permanent error, 5xx reply code /// Permanent SMTP error, 5xx reply code
/// ///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1) /// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
PermanentError(Response), PermanentError(Response),
/// TODO /// Error parsing a response
ClientError(String), ResponseParsingError(&'static str),
/// Error parsing a base64 string in response
ChallengeParsingError(FromBase64Error),
/// Internal client error
ClientError(&'static str),
/// DNS resolution error
ResolutionError,
/// IO error /// IO error
IoError(io::Error), IoError(io::Error),
} }
impl Display for SmtpError { impl Display for Error {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description()) fmt.write_str(self.description())
} }
} }
impl Error for SmtpError { impl StdError for Error {
fn description(&self) -> &str { fn description(&self) -> &str {
match *self { match *self {
TransientError(_) => "a transient error occured during the SMTP transaction", TransientError(_) => "a transient error occured during the SMTP transaction",
PermanentError(_) => "a permanent error occured during the SMTP transaction", PermanentError(_) => "a permanent error occured during the SMTP transaction",
ResponseParsingError(_) => "an error occured while parsing an SMTP response",
ChallengeParsingError(_) => "an error occured while parsing a CRAM-MD5 challenge",
ResolutionError => "Could no resolve hostname",
ClientError(_) => "an unknown error occured", ClientError(_) => "an unknown error occured",
IoError(_) => "an I/O error occured", IoError(_) => "an I/O error occured",
} }
} }
fn cause(&self) -> Option<&Error> { fn cause(&self) -> Option<&StdError> {
match *self { match *self {
IoError(ref err) => Some(&*err as &Error), IoError(ref err) => Some(&*err as &StdError),
_ => None, _ => None,
} }
} }
} }
impl From<io::Error> for SmtpError { impl From<io::Error> for Error {
fn from(err: io::Error) -> SmtpError { fn from(err: io::Error) -> Error {
IoError(err) IoError(err)
} }
} }
impl From<Response> for SmtpError { impl From<Response> for Error {
fn from(response: Response) -> SmtpError { fn from(response: Response) -> Error {
match response.severity() { match response.severity() {
Severity::TransientNegativeCompletion => TransientError(response), Severity::TransientNegativeCompletion => TransientError(response),
Severity::PermanentNegativeCompletion => PermanentError(response), Severity::PermanentNegativeCompletion => PermanentError(response),
_ => ClientError("Unknown error code".to_string()) _ => ClientError("Unknown error code")
} }
} }
} }
impl From<&'static str> for SmtpError { impl From<&'static str> for Error {
fn from(string: &'static str) -> SmtpError { fn from(string: &'static str) -> Error {
ClientError(string.to_string()) ClientError(string)
} }
} }
/// SMTP result type /// SMTP result type
pub type SmtpResult = Result<Response, SmtpError>; pub type SmtpResult = Result<Response, Error>;
#[cfg(test)] #[cfg(test)]
mod test { mod test {

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 //! ESMTP features
use std::str::FromStr;
use std::result::Result; use std::result::Result;
use std::fmt::{Display, Formatter};
use std::fmt;
use std::collections::HashSet;
use response::Response; use response::Response;
use self::Extension::*; use error::Error;
use authentication::Mecanism;
/// Supported ESMTP keywords /// Supported ESMTP keywords
#[derive(PartialEq,Eq,Copy,Clone,Debug)] #[derive(PartialEq,Eq,Hash,Clone,Debug)]
pub enum Extension { pub enum Extension {
/// 8BITMIME keyword /// 8BITMIME keyword
/// ///
@@ -30,81 +24,174 @@ pub enum Extension {
/// ///
/// RFC 2487: https://tools.ietf.org/html/rfc2487 /// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls, StartTls,
/// AUTH PLAIN mecanism /// AUTH mecanism
/// Authentication(Mecanism),
/// RFC 4616: https://tools.ietf.org/html/rfc4616
PlainAuthentication,
/// AUTH CRAM-MD5 mecanism
///
/// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5Authentication,
} }
impl Extension { impl Display for Extension {
fn from_str(s: &str) -> Result<Vec<Extension>, &'static str> { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let splitted : Vec<&str> = s.split(' ').collect(); match *self {
match (splitted[0], splitted.len()) { Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
("8BITMIME", 1) => Ok(vec![EightBitMime]), Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
("SMTPUTF8", 1) => Ok(vec![SmtpUtfEight]), Extension::StartTls => write!(f, "{}", "STARTTLS"),
("STARTTLS", 1) => Ok(vec![StartTls]), Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism),
("AUTH", _) => {
let mut mecanisms: Vec<Extension> = vec![];
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => mecanisms.push(PlainAuthentication),
"CRAM-MD5" => mecanisms.push(CramMd5Authentication),
_ => (),
}
}
Ok(mecanisms)
},
_ => Err("Unknown extension"),
} }
} }
}
/// Parses supported ESMTP features /// Contains information about an SMTP server
pub fn parse_esmtp_response(response: &Response) -> Vec<Extension> { #[derive(Clone,Debug,Eq,PartialEq)]
let mut esmtp_features: Vec<Extension> = Vec::new(); 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() { for line in response.message() {
if let Ok(keywords) = Extension::from_str(&line) {
for keyword in keywords { let splitted : Vec<&str> = line.split_whitespace().collect();
esmtp_features.push(keyword); 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)] #[cfg(test)]
mod test { mod test {
use super::Extension; use std::collections::HashSet;
use response::{Severity, Category, Response};
use super::{ServerInfo, Extension};
use authentication::Mecanism;
use response::{Code, Response, Severity, Category};
#[test] #[test]
fn test_from_str() { fn test_extension_fmt() {
assert_eq!(Extension::from_str("8BITMIME"), Ok(vec![Extension::EightBitMime])); assert_eq!(format!("{}", Extension::EightBitMime), "8BITMIME".to_string());
assert_eq!(Extension::from_str("AUTH PLAIN"), Ok(vec![Extension::PlainAuthentication])); assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)), "AUTH PLAIN".to_string());
assert_eq!(Extension::from_str("AUTH PLAIN LOGIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
assert_eq!(Extension::from_str("AUTH CRAM-MD5 PLAIN"), Ok(vec![Extension::CramMd5Authentication, Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
} }
#[test] #[test]
fn test_parse_esmtp_response() { fn test_serverinfo_fmt() {
assert_eq!(Extension::parse_esmtp_response(&Response::new( let mut eightbitmime = HashSet::new();
"2".parse::<Severity>().unwrap(), assert!(eightbitmime.insert(Extension::EightBitMime));
"2".parse::<Category>().unwrap(),
1, assert_eq!(format!("{}", 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()] 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(), let mut features = HashSet::new();
"3".parse::<Category>().unwrap(), assert!(features.insert(Extension::EightBitMime));
3,
vec!["me".to_string(), "8BITMIME".to_string(), "AUTH PLAIN CRAM-MD5".to_string()] let server_info = ServerInfo {
)), vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]); 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,22 +1,14 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! # Rust SMTP client //! # Rust SMTP client
//! //!
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still //! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
//! a work in progress. It is designed to efficiently send emails from a rust application to a //! a work in progress. It is designed to efficiently send emails from an application to a
//! relay email server. //! relay email server, as it relies as much as possible on the relay server for sanity and RFC compliance
//! checks.
//! //!
//! It implements the following extensions: //! It implements the following extensions:
//! //!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152)) //! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) //! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms
//! //!
//! It will eventually implement the following extensions: //! It will eventually implement the following extensions:
//! //!
@@ -25,11 +17,11 @@
//! //!
//! ## Architecture //! ## Architecture
//! //!
//! This client is divided into three parts: //! This client is divided into three main parts:
//! //!
//! * client: a low level SMTP client providing all SMTP commands //! * client: a low level SMTP client providing all SMTP commands
//! * sender: a high level SMTP client providing an easy method to send emails //! * sender: a high level SMTP client providing an easy method to send emails
//! * mailer: generates the email to be sent with the sender //! * email: generates the email to be sent with the sender
//! //!
//! ## Usage //! ## Usage
//! //!
@@ -39,7 +31,7 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder}; //! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::mailer::EmailBuilder; //! use smtp::email::EmailBuilder;
//! //!
//! // Create an email //! // Create an email
//! let email = EmailBuilder::new() //! let email = EmailBuilder::new()
@@ -52,7 +44,7 @@
//! .build(); //! .build();
//! //!
//! // Open a local connection on port 25 //! // Open a local connection on port 25
//! let mut sender = SenderBuilder::localhost().build(); //! let mut sender = SenderBuilder::localhost().unwrap().build();
//! // Send the email //! // Send the email
//! let result = sender.send(email); //! let result = sender.send(email);
//! //!
@@ -63,7 +55,8 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder}; //! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::mailer::EmailBuilder; //! use smtp::email::EmailBuilder;
//! use smtp::authentication::Mecanism;
//! //!
//! let mut builder = EmailBuilder::new(); //! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name")); //! builder = builder.to(("user@example.org", "Alias name"));
@@ -79,11 +72,13 @@
//! let email = builder.build(); //! let email = builder.build();
//! //!
//! // Connect to a remote server on a custom port //! // Connect to a remote server on a custom port
//! let mut sender = SenderBuilder::new(("server.tld", 10025)) //! let mut sender = SenderBuilder::new(("server.tld", 10025)).unwrap()
//! // Set the name sent during EHLO/HELO, default is `localhost` //! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name("my.hostname.tld") //! .hello_name("my.hostname.tld")
//! // Add credentials for authentication //! // Add credentials for authentication
//! .credentials("username", "password") //! .credentials("username", "password")
//! // Configure accepted authetication mecanisms
//! .authentication_mecanisms(vec![Mecanism::CramMd5])
//! // Enable connection reuse //! // Enable connection reuse
//! .enable_connection_reuse(true).build(); //! .enable_connection_reuse(true).build();
//! //!
@@ -104,7 +99,7 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! use smtp::sender::{Sender, SenderBuilder}; //! use smtp::sender::{Sender, SenderBuilder};
//! use smtp::sendable_email::SimpleSendableEmail; //! use smtp::email::SimpleSendableEmail;
//! //!
//! // Create a minimal email //! // Create a minimal email
//! let email = SimpleSendableEmail::new( //! let email = SimpleSendableEmail::new(
@@ -113,7 +108,7 @@
//! "Hello world !" //! "Hello world !"
//! ); //! );
//! //!
//! let mut sender = SenderBuilder::localhost().build(); //! let mut sender = SenderBuilder::localhost().unwrap().build();
//! let result = sender.send(email); //! let result = sender.send(email);
//! assert!(result.is_ok()); //! assert!(result.is_ok());
//! ``` //! ```
@@ -128,7 +123,7 @@
//! use smtp::SMTP_PORT; //! use smtp::SMTP_PORT;
//! use std::net::TcpStream; //! use std::net::TcpStream;
//! //!
//! let mut email_client: Client<SmtpStream> = Client::new(("localhost", SMTP_PORT)); //! let mut email_client: Client<SmtpStream> = Client::new(("localhost", SMTP_PORT)).unwrap();
//! let _ = email_client.connect(); //! let _ = email_client.connect();
//! let _ = email_client.ehlo("my_hostname"); //! let _ = email_client.ehlo("my_hostname");
//! let _ = email_client.mail("user@example.com", None); //! let _ = email_client.mail("user@example.com", None);
@@ -145,7 +140,7 @@ extern crate rustc_serialize as serialize;
extern crate crypto; extern crate crypto;
extern crate time; extern crate time;
extern crate uuid; extern crate uuid;
extern crate email; extern crate email as email_format;
extern crate bufstream; extern crate bufstream;
mod extension; mod extension;
@@ -153,8 +148,8 @@ pub mod client;
pub mod sender; pub mod sender;
pub mod response; pub mod response;
pub mod error; pub mod error;
pub mod sendable_email; pub mod authentication;
pub mod mailer; pub mod email;
// Registrated port numbers: // Registrated port numbers:
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml // https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml

View File

@@ -1,212 +0,0 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Simple email (very incomplete)
use email::{MimeMessage, Header, Address};
use time::{now, Tm};
use uuid::Uuid;
use sendable_email::SendableEmail;
/// Converts an adress or an address with an alias to an `Address`
pub trait ToHeader {
/// Converts to an `Address` struct
fn to_header(&self) -> Header;
}
impl ToHeader for Header {
fn to_header(&self) -> Header {
(*self).clone()
}
}
impl<'a> ToHeader for (&'a str, &'a str) {
fn to_header(&self) -> Header {
let (name, value) = *self;
Header::new(name.to_string(), value.to_string())
}
}
/// Converts an adress or an address with an alias to an `Address`
pub trait ToAddress {
/// Converts to an `Address` struct
fn to_address(&self) -> Address;
}
impl ToAddress for Address {
fn to_address(&self) -> Address {
(*self).clone()
}
}
impl<'a> ToAddress for &'a str {
fn to_address(&self) -> Address {
Address::new_mailbox(self.to_string())
}
}
impl<'a> ToAddress for (&'a str, &'a str) {
fn to_address(&self) -> Address {
let (address, alias) = *self;
Address::new_mailbox_with_name(address.to_string(), alias.to_string())
}
}
/// TODO
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct EmailBuilder {
/// Email content
content: Email,
/// Date issued
date_issued: bool,
}
/// Simple email representation
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Email {
/// Message
message: MimeMessage,
/// The enveloppe recipients addresses
to: Vec<String>,
/// The enveloppe sender address
from: Option<String>,
/// Message-ID
message_id: Uuid,
}
impl Email {
/// Displays the formatted email content
pub fn as_string(&self) -> String {
self.message.as_string()
}
}
impl EmailBuilder {
/// Creates a new empty email
pub fn new() -> EmailBuilder {
let current_message = Uuid::new_v4();
let mut email = Email {
message: MimeMessage::new_blank_message(),
to: vec![],
from: None,
message_id: current_message,
};
email.message.headers.insert(
Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>", current_message)
).unwrap()
);
EmailBuilder {
content: email,
date_issued: false,
}
}
/// Sets the email body
pub fn body(mut self, body: &str) -> EmailBuilder {
self.content.message.body = body.to_string();
self
}
/// Add a generic header
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
self.insert_header(header);
self
}
fn insert_header<A: ToHeader>(&mut self, header: A) {
self.content.message.headers.insert(header.to_header());
}
/// Adds a `From` header and store the sender address
pub fn from<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.from = Some(address.to_address().get_address().unwrap());
self.insert_header(("From", address.to_address().to_string().as_ref()));
self
}
/// Adds a `To` header and store the recipient address
pub fn to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.to.push(address.to_address().get_address().unwrap());
self.insert_header(("To", address.to_address().to_string().as_ref()));
self
}
/// Adds a `Cc` header and store the recipient address
pub fn cc<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.to.push(address.to_address().get_address().unwrap());
self.insert_header(("Cc", address.to_address().to_string().as_ref()));
self
}
/// Adds a `Reply-To` header
pub fn reply_to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.insert_header(("Reply-To", address.to_address().to_string().as_ref()));
self
}
/// Adds a `Sender` header
pub fn sender<A: ToAddress>(mut self, address: A) -> EmailBuilder {
self.content.from = Some(address.to_address().get_address().unwrap());
self.insert_header(("Sender", address.to_address().to_string().as_ref()));
self
}
/// Adds a `Subject` header
pub fn subject(mut self, subject: &str) -> EmailBuilder {
self.insert_header(("Subject", subject));
self
}
/// Adds a `Date` header with the given date
pub fn date(mut self, date: &Tm) -> EmailBuilder {
self.insert_header(("Date", Tm::rfc822(date).to_string().as_ref()));
self.date_issued = true;
self
}
/// Build the Email
pub fn build(mut self) -> Email {
if !self.date_issued {
self.insert_header(("Date", Tm::rfc822(&now()).to_string().as_ref()));
}
self.content.message.update_headers();
self.content
}
}
impl SendableEmail for Email {
/// Return the to addresses, and fails if it is not set
fn to_addresses(&self) -> Vec<String> {
if self.to.is_empty() {
panic!("The To field is empty")
}
self.to.clone()
}
/// Return the from address, and fails if it is not set
fn from_address(&self) -> String {
match self.from {
Some(ref from_address) => from_address.clone(),
None => panic!("The From field is empty"),
}
}
fn message(&self) -> String {
format! ("{}", self.as_string())
}
fn message_id(&self) -> String {
format!("{}", self.message_id)
}
}

View File

@@ -1,20 +1,12 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT //! SMTP response, containing a mandatory return code and an optional text message
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! SMTP response, containing a mandatory return code, and an optional text message
use std::str::FromStr; use std::str::FromStr;
use std::fmt::{Display, Formatter, Result}; use std::fmt::{Display, Formatter, Result};
use std::result::Result as RResult; use std::result;
use self::Severity::*; use self::Severity::*;
use self::Category::*; use self::Category::*;
use error::{SmtpResult, Error};
/// First digit indicates severity /// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)] #[derive(PartialEq,Eq,Copy,Clone,Debug)]
@@ -30,14 +22,14 @@ pub enum Severity {
} }
impl FromStr for Severity { impl FromStr for Severity {
type Err = &'static str; type Err = Error;
fn from_str(s: &str) -> RResult<Severity, &'static str> { fn from_str(s: &str) -> result::Result<Severity, Error> {
match s { match s {
"2" => Ok(PositiveCompletion), "2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate), "3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion), "4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion), "5" => Ok(PermanentNegativeCompletion),
_ => Err("First digit must be between 2 and 5"), _ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
} }
} }
} }
@@ -73,8 +65,8 @@ pub enum Category {
} }
impl FromStr for Category { impl FromStr for Category {
type Err = &'static str; type Err = Error;
fn from_str(s: &str) -> RResult<Category, &'static str> { fn from_str(s: &str) -> result::Result<Category, Error> {
match s { match s {
"0" => Ok(Syntax), "0" => Ok(Syntax),
"1" => Ok(Information), "1" => Ok(Information),
@@ -82,7 +74,7 @@ impl FromStr for Category {
"3" => Ok(Unspecified3), "3" => Ok(Unspecified3),
"4" => Ok(Unspecified4), "4" => Ok(Unspecified4),
"5" => Ok(MailSystem), "5" => Ok(MailSystem),
_ => Err("Second digit must be between 0 and 5"), _ => Err(Error::ResponseParsingError("Second digit must be between 0 and 5")),
} }
} }
} }
@@ -102,17 +94,112 @@ impl Display for Category {
} }
} }
/// Contains an SMTP reply, with separed code and message /// Represents a 3 digit SMTP response code
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq,Eq,Clone,Debug)] #[derive(PartialEq,Eq,Clone,Debug)]
pub struct Response { pub struct Code {
/// First digit of the response code /// First digit of the response code
severity: Severity, severity: Severity,
/// Second digit of the response code /// Second digit of the response code
category: Category, category: Category,
/// Third digit /// Third digit
detail: u8, 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"))
}
}
}
/// Contains an SMTP reply, with separed code and message
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Response {
/// Response code
code: Code,
/// Server response string (optional) /// Server response string (optional)
/// Handle multiline responses /// Handle multiline responses
message: Vec<String> message: Vec<String>
@@ -120,18 +207,16 @@ pub struct Response {
impl Response { impl Response {
/// Creates a new `Response` /// Creates a new `Response`
pub fn new(severity: Severity, category: Category, detail: u8, message: Vec<String>) -> Response { pub fn new(code: Code, message: Vec<String>) -> Response {
Response { Response {
severity: severity, code: code,
category: category, message: message,
detail: detail,
message: message
} }
} }
/// Tells if the response is positive /// Tells if the response is positive
pub fn is_positive(&self) -> bool { pub fn is_positive(&self) -> bool {
match self.severity { match self.code.severity {
PositiveCompletion => true, PositiveCompletion => true,
PositiveIntermediate => true, PositiveIntermediate => true,
_ => false, _ => false,
@@ -145,25 +230,25 @@ impl Response {
/// Returns the severity (i.e. 1st digit) /// Returns the severity (i.e. 1st digit)
pub fn severity(&self) -> Severity { pub fn severity(&self) -> Severity {
self.severity self.code.severity
} }
/// Returns the category (i.e. 2nd digit) /// Returns the category (i.e. 2nd digit)
pub fn category(&self) -> Category { pub fn category(&self) -> Category {
self.category self.code.category
} }
/// Returns the detail (i.e. 3rd digit) /// Returns the detail (i.e. 3rd digit)
pub fn detail(&self) -> u8 { pub fn detail(&self) -> u8 {
self.detail self.code.detail
} }
/// Returns the reply code /// Returns the reply code
fn code(&self) -> String { fn code(&self) -> String {
format!("{}{}{}", self.severity, self.category, self.detail) self.code.code()
} }
/// Checls code equality /// Tests code equality
pub fn has_code(&self, code: u16) -> bool { pub fn has_code(&self, code: u16) -> bool {
self.code() == format!("{}", code) self.code() == format!("{}", code)
} }
@@ -172,20 +257,22 @@ impl Response {
pub fn first_word(&self) -> Option<String> { pub fn first_word(&self) -> Option<String> {
match self.message.is_empty() { match self.message.is_empty() {
true => None, true => None,
false => Some(self.message[0].split(" ").next().unwrap().to_string()), false => match self.message[0].split_whitespace().next() {
Some(word) => Some(word.to_string()),
None => None,
}
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{Severity, Category, Response}; use super::{Severity, Category, Response, ResponseParser, Code};
#[test] #[test]
fn test_severity_from_str() { fn test_severity_from_str() {
assert_eq!("2".parse::<Severity>(), Ok(Severity::PositiveCompletion)); assert_eq!("2".parse::<Severity>().unwrap(), Severity::PositiveCompletion);
assert_eq!("4".parse::<Severity>(), Ok(Severity::TransientNegativeCompletion)); assert_eq!("4".parse::<Severity>().unwrap(), Severity::TransientNegativeCompletion);
assert!("1".parse::<Severity>().is_err()); assert!("1".parse::<Severity>().is_err());
} }
@@ -196,8 +283,8 @@ mod test {
#[test] #[test]
fn test_category_from_str() { fn test_category_from_str() {
assert_eq!("2".parse::<Category>(), Ok(Category::Connections)); assert_eq!("2".parse::<Category>().unwrap(), Category::Connections);
assert_eq!("4".parse::<Category>(), Ok(Category::Unspecified4)); assert_eq!("4".parse::<Category>().unwrap(), Category::Unspecified4);
assert!("6".parse::<Category>().is_err()); assert!("6".parse::<Category>().is_err());
} }
@@ -206,44 +293,116 @@ mod test {
assert_eq!(format!("{}", Category::Unspecified4), "4"); assert_eq!(format!("{}", Category::Unspecified4), "4");
} }
#[test]
fn test_code_new() {
assert_eq!(
Code::new(Severity::TransientNegativeCompletion, Category::Connections, 0),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 0,
}
);
}
#[test]
fn test_code_from_str() {
assert_eq!(
"421".parse::<Code>().unwrap(),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
}
);
}
#[test]
fn test_code_code() {
let code = Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: 1,
};
assert_eq!(code.code(), "421");
}
#[test] #[test]
fn test_response_new() { fn test_response_new() {
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
), Response { ), Response {
severity: Severity::PositiveCompletion, code: Code {
category: Category::Unspecified4, severity: Severity::PositiveCompletion,
detail: 1, category: Category::Unspecified4,
detail: 1,
},
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()], message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
}); });
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![] vec![]
), Response { ), Response {
severity: Severity::PositiveCompletion, code: Code {
category: Category::Unspecified4, severity: Severity::PositiveCompletion,
detail: 1, category: Category::Unspecified4,
detail: 1,
},
message: vec![], message: vec![],
}); });
} }
#[test]
fn test_response_parser() {
let mut parser = ResponseParser::new();
assert!(parser.read_line("250-me").unwrap());
assert!(parser.read_line("250-8BITMIME").unwrap());
assert!(parser.read_line("250-SIZE 42").unwrap());
assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap());
let response = parser.response().unwrap();
assert_eq!(
response,
Response {
code: Code {
severity: Severity::PositiveCompletion,
category: Category::MailSystem,
detail: 0,
},
message: vec!["me".to_string(), "8BITMIME".to_string(),
"SIZE 42".to_string(), "AUTH PLAIN CRAM-MD5".to_string()],
}
);
}
#[test] #[test]
fn test_response_is_positive() { fn test_response_is_positive() {
assert!(Response::new( assert!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive()); ).is_positive());
assert!(! Response::new( assert!(! Response::new(
"4".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "5".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).is_positive()); ).is_positive());
} }
@@ -251,16 +410,20 @@ mod test {
#[test] #[test]
fn test_response_message() { fn test_response_message() {
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]); ).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
let empty_message: Vec<String> = vec![]; let empty_message: Vec<String> = vec![];
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![] vec![]
).message(), empty_message); ).message(), empty_message);
} }
@@ -268,19 +431,31 @@ mod test {
#[test] #[test]
fn test_response_severity() { fn test_response_severity() {
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PositiveCompletion); ).severity(), Severity::PositiveCompletion);
assert_eq!(Response::new(
Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
detail: 1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).severity(), Severity::PermanentNegativeCompletion);
} }
#[test] #[test]
fn test_response_category() { fn test_response_category() {
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).category(), Category::Unspecified4); ).category(), Category::Unspecified4);
} }
@@ -288,9 +463,11 @@ mod test {
#[test] #[test]
fn test_response_detail() { fn test_response_detail() {
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).detail(), 1); ).detail(), 1);
} }
@@ -298,9 +475,11 @@ mod test {
#[test] #[test]
fn test_response_code() { fn test_response_code() {
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).code(), "241"); ).code(), "241");
} }
@@ -308,15 +487,19 @@ mod test {
#[test] #[test]
fn test_response_has_code() { fn test_response_has_code() {
assert!(Response::new( assert!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(241)); ).has_code(241));
assert!(! Response::new( assert!(! Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).has_code(251)); ).has_code(251));
} }
@@ -324,22 +507,52 @@ mod test {
#[test] #[test]
fn test_response_first_word() { fn test_response_first_word() {
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string())); ).first_word(), Some("me".to_string()));
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
).first_word(), Some("me".to_string())); ).first_word(), Some("me".to_string()));
assert_eq!(Response::new( assert_eq!(Response::new(
"2".parse::<Severity>().unwrap(), Code {
"4".parse::<Category>().unwrap(), severity: "2".parse::<Severity>().unwrap(),
1, category: "4".parse::<Category>().unwrap(),
detail:1,
},
vec![] vec![]
).first_word(), None); ).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,63 +0,0 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! SMTP sendable email
use uuid::Uuid;
/// Email sendable by an SMTP client
pub trait SendableEmail {
/// From address
fn from_address(&self) -> String;
/// To addresses
fn to_addresses(&self) -> Vec<String>;
/// Message content
fn message(&self) -> String;
/// Message ID
fn message_id(&self) -> String;
}
/// Minimal email structure
pub struct SimpleSendableEmail {
/// From address
from: String,
/// To addresses
to: Vec<String>,
/// Message
message: String,
}
impl SimpleSendableEmail {
/// Returns a new email
pub fn new(from_address: &str, to_address: &str, message: &str) -> SimpleSendableEmail {
SimpleSendableEmail {
from: from_address.to_string(),
to: vec![to_address.to_string()],
message: message.to_string(),
}
}
}
impl SendableEmail for SimpleSendableEmail {
fn from_address(&self) -> String {
self.from.clone()
}
fn to_addresses(&self) -> Vec<String> {
self.to.clone()
}
fn message(&self) -> String {
self.message.clone()
}
fn message_id(&self) -> String {
format!("<{}@rust-smtp>", Uuid::new_v4())
}
}

View File

@@ -1,26 +1,15 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Sends an email using the client //! Sends an email using the client
use std::string::String; use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs}; use std::net::{SocketAddr, ToSocketAddrs};
use SMTP_PORT; use SMTP_PORT;
use extension::Extension; use extension::{Extension, ServerInfo};
use error::{SmtpResult, SmtpError}; use error::{SmtpResult, Error};
use sendable_email::SendableEmail; use email::SendableEmail;
use sender::server_info::ServerInfo;
use client::Client; use client::Client;
use client::net::SmtpStream; use client::net::SmtpStream;
use authentication::Mecanism;
mod server_info;
/// Contains client configuration /// Contains client configuration
pub struct SenderBuilder { pub struct SenderBuilder {
@@ -36,23 +25,32 @@ pub struct SenderBuilder {
credentials: Option<(String, String)>, credentials: Option<(String, String)>,
/// Socket we are connecting to /// Socket we are connecting to
server_addr: SocketAddr, server_addr: SocketAddr,
/// List of authentication mecanism, sorted by priority
authentication_mecanisms: Vec<Mecanism>,
} }
/// Builder for the SMTP Sender /// Builder for the SMTP Sender
impl SenderBuilder { impl SenderBuilder {
/// Creates a new local SMTP client /// Creates a new local SMTP client
pub fn new<A: ToSocketAddrs>(addr: A) -> SenderBuilder { pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SenderBuilder, Error> {
SenderBuilder { let mut addresses = try!(addr.to_socket_addrs());
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
credentials: None,
connection_reuse_count_limit: 100, match addresses.next() {
enable_connection_reuse: false, Some(addr) => Ok(SenderBuilder {
hello_name: "localhost".to_string(), server_addr: addr,
} 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 /// Creates a new local SMTP client to port 25
pub fn localhost() -> SenderBuilder { pub fn localhost() -> Result<SenderBuilder, Error> {
SenderBuilder::new(("localhost", SMTP_PORT)) SenderBuilder::new(("localhost", SMTP_PORT))
} }
@@ -79,6 +77,12 @@ impl SenderBuilder {
self.credentials = Some((username.to_string(), password.to_string())); self.credentials = Some((username.to_string(), password.to_string()));
self self
} }
/// Set the authentication mecanisms
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SenderBuilder {
self.authentication_mecanisms = mecanisms;
self
}
/// Build the SMTP client /// Build the SMTP client
/// ///
@@ -130,7 +134,7 @@ impl Sender {
/// ///
/// It does not connects to the server, but only creates the `Sender` /// It does not connects to the server, but only creates the `Sender`
pub fn new(builder: SenderBuilder) -> Sender { pub fn new(builder: SenderBuilder) -> Sender {
let client: Client<SmtpStream> = Client::new(builder.server_addr); let client: Client<SmtpStream> = Client::new(builder.server_addr).unwrap();
Sender{ Sender{
client: client, client: client,
server_info: None, server_info: None,
@@ -175,59 +179,52 @@ impl Sender {
info!("connection established to {}", self.client_info.server_addr); info!("connection established to {}", self.client_info.server_addr);
// Extended Hello or Hello if needed // Extended Hello or Hello if needed
match self.client.ehlo(&self.client_info.hello_name) { let hello_response = match self.client.ehlo(&self.client_info.hello_name) {
Ok(response) => {self.server_info = Some( Ok(response) => response,
ServerInfo{
name: response.first_word().expect("Server announced no hostname"),
esmtp_features: Extension::parse_esmtp_response(&response),
});
},
Err(error) => match error { Err(error) => match error {
SmtpError::PermanentError(ref response) if response.has_code(550) => { Error::PermanentError(ref response) if response.has_code(550) => {
match self.client.helo(&self.client_info.hello_name) { match self.client.helo(&self.client_info.hello_name) {
Ok(response) => {self.server_info = Some( Ok(response) => response,
ServerInfo{
name: response.first_word().expect("Server announced no hostname"),
esmtp_features: vec!(),
});
},
Err(error) => try_smtp!(Err(error), self) Err(error) => try_smtp!(Err(error), self)
} }
}, },
_ => { _ => {
try_smtp!(Err(error), self) try_smtp!(Err(error), self)
}, },
}, },
} };
self.server_info = Some(try_smtp!(ServerInfo::from_response(&hello_response), self));
// Print server information // Print server information
debug!("server {}", self.server_info.as_ref().unwrap()); debug!("server {}", self.server_info.as_ref().unwrap());
} }
// TODO: Use PLAIN AUTH in encrypted connections, CRAM-MD5 otherwise
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 { if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
let (username, password) = self.client_info.credentials.clone().unwrap(); let (username, password) = self.client_info.credentials.clone().unwrap();
if self.server_info.as_ref().unwrap().supports_feature(Extension::CramMd5Authentication) { let mut found = false;
let result = self.client.auth_cram_md5(&username, &password);
try_smtp!(result, self); for mecanism in self.client_info.authentication_mecanisms.clone() {
} else if self.server_info.as_ref().unwrap().supports_feature(Extension::PlainAuthentication) { if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
let result = self.client.auth_plain(&username, &password); found = true;
try_smtp!(result, self); let result = self.client.auth(mecanism, &username, &password);
} else { try_smtp!(result, self);
debug!("No supported authentication mecanisms available"); }
} }
if !found {
debug!("No supported authentication mecanisms available");
}
} }
let current_message = email.message_id(); let current_message = try!(email.message_id().ok_or("Missing Message-ID"));
let from_address = email.from_address(); let from_address = try!(email.from_address().ok_or("Missing From address"));
let to_addresses = email.to_addresses(); let to_addresses = try!(email.to_addresses().ok_or("Missing To address"));
let message = email.message(); let message = try!(email.message().ok_or("Missing message"));
// Mail // Mail
let mail_options = match self.server_info.as_ref().unwrap().supports_feature(Extension::EightBitMime) { let mail_options = match self.server_info.as_ref().unwrap().supports_feature(&Extension::EightBitMime) {
true => Some("BODY=8BITMIME"), true => Some("BODY=8BITMIME"),
false => None, false => None,
}; };

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