Refactoring

This commit is contained in:
Alexis Mousset
2015-07-14 21:52:55 +02:00
parent 0b7e004ac8
commit ef8c426cd4
7 changed files with 238 additions and 247 deletions

View File

@@ -9,6 +9,9 @@
//! Provides authentication mecanisms
use std::fmt::{Display, Formatter};
use std::fmt::Result as FmtResult;
use serialize::base64::{self, ToBase64, FromBase64};
use serialize::hex::ToHex;
use crypto::hmac::Hmac;
@@ -17,84 +20,83 @@ use crypto::mac::Mac;
use NUL;
use error::Error;
use extension::Extension;
/// Represents an authentication mecanism
pub trait Mecanism {
/// Returns the matching `Extension`
fn extension() -> Extension;
/// Returns the initial response support
fn supports_initial_response() -> bool;
/// Returns the response
fn response(username: &str, password: &str, challenge: Option<&str>) -> Result<String, Error>;
/// TODO
#[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,
}
/// PLAIN authentication mecanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
#[derive(Copy,Clone)]
pub struct Plain;
impl Mecanism for Plain {
fn extension() -> Extension {
Extension::PlainAuthentication
impl Display for Mecanism {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{}",
match *self {
Mecanism::Plain => "PLAIN",
Mecanism::CramMd5 => "CRAM-MD5",
}
)
}
}
fn supports_initial_response() -> bool {
true
}
fn response(username: &str, password: &str, challenge: Option<&str>) -> Result<String, Error> {
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)),
impl Mecanism {
/// TODO
pub fn supports_initial_response(&self) -> bool {
match *self {
Mecanism::Plain => true,
Mecanism::CramMd5 => false,
}
}
}
/// CRAM-MD5 authentication mecanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195
#[derive(Copy,Clone)]
pub struct CramMd5;
/// TODO
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")),
};
impl Mecanism for CramMd5 {
fn extension() -> Extension {
Extension::CramMd5Authentication
}
let decoded_challenge = match encoded_challenge.from_base64() {
Ok(challenge) => challenge,
Err(error) => return Err(Error::ChallengeParsingError(error)),
};
fn supports_initial_response() -> bool {
false
}
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
hmac.input(&decoded_challenge);
fn response(username: &str, password: &str, challenge: Option<&str>) -> Result<String, Error> {
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))
Ok(format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD))
},
}
}
}
#[cfg(test)]
mod test {
use super::{Mecanism, Plain, CramMd5};
use super::Mecanism;
#[test]
fn test_plain() {
assert_eq!(Plain::response("username", "password", None).unwrap(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
let mecanism = Mecanism::Plain;
assert_eq!(mecanism.response("username", "password", None).unwrap(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
}
#[test]
fn test_cram_md5() {
assert_eq!(CramMd5::response("alice", "wonderland",
let mecanism = Mecanism::CramMd5;
assert_eq!(mecanism.response("alice", "wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")).unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
}

View File

@@ -0,0 +1,54 @@
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
// file at the top-level directory of this distribution.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! 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;
use error::Error;
/// 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) -> Result<String, Error> {
let 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(&challenge);
Ok(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==").unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
}
}

0
src/client/connector.rs Normal file
View File

View File

@@ -16,7 +16,7 @@ use std::io::{BufRead, Read, Write};
use bufstream::BufStream;
use response::ResponseParser;
use authentication::{Mecanism, CramMd5, Plain};
use authentication::Mecanism;
use error::{Error, SmtpResult};
use client::net::{Connector, SmtpStream};
use {CRLF, MESSAGE_ENDING};
@@ -167,21 +167,21 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
self.command("RSET")
}
/// Sends an AUTH command with PLAIN mecanism
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
self.command(&format!("AUTH PLAIN {}", try!(Plain::response(username, password, None))))
}
/// Sends an AUTH command with the given mecanism
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
/// Sends an AUTH command with CRAM-MD5 mecanism
pub fn auth_cram_md5(&mut self, username: &str, password: &str) -> SmtpResult {
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")),
};
if mecanism.supports_initial_response() {
self.command(&format!("AUTH {} {}", mecanism, try!(mecanism.response(username, password, None))))
} else {
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
Some(challenge) => challenge,
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
};
let cram_response = try!(CramMd5::response(username, password, Some(&encoded_challenge)));
let cram_response = try!(mecanism.response(username, password, Some(&encoded_challenge)));
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
}
}
/// Sends the message content

View File

@@ -9,14 +9,17 @@
//! ESMTP features
use std::str::FromStr;
use std::result::Result;
use std::fmt::{Display, Formatter};
use std::fmt::Result as FmtResult;
use std::collections::HashSet;
use response::Response;
use self::Extension::*;
use error::Error;
use authentication::Mecanism;
/// Supported ESMTP keywords
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
pub enum Extension {
/// 8BITMIME keyword
///
@@ -30,85 +33,115 @@ pub enum Extension {
///
/// RFC 2487: https://tools.ietf.org/html/rfc2487
StartTls,
/// AUTH PLAIN mecanism
///
/// RFC 4616: https://tools.ietf.org/html/rfc4616
PlainAuthentication,
/// AUTH CRAM-MD5 mecanism
///
/// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5Authentication,
/// AUTH mecanism
Authentication(Mecanism),
}
impl Extension {
fn from_str(s: &str) -> Result<Vec<Extension>, &'static str> {
let splitted : Vec<&str> = s.split_whitespace().collect();
match (splitted[0], splitted.len()) {
("8BITMIME", 1) => Ok(vec![EightBitMime]),
("SMTPUTF8", 1) => Ok(vec![SmtpUtfEight]),
("STARTTLS", 1) => Ok(vec![StartTls]),
("AUTH", _) => {
let mut mecanisms: Vec<Extension> = vec![];
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => mecanisms.push(PlainAuthentication),
"CRAM-MD5" => mecanisms.push(CramMd5Authentication),
_ => (),
}
}
Ok(mecanisms)
},
_ => Err("Unknown extension"),
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{}",
match *self {
Extension::EightBitMime => "8BITMIME",
Extension::SmtpUtfEight => "SMTPUTF8",
Extension::StartTls => "STARTTLS",
Extension::Authentication(_) => "AUTH",
}
)
}
}
/// Parses supported ESMTP features
pub fn parse_esmtp_response(response: &Response) -> Vec<Extension> {
let mut esmtp_features: Vec<Extension> = Vec::new();
/// Contains information about an SMTP server
#[derive(Clone,Debug)]
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: HashSet<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{} with {}",
self.name,
match self.esmtp_features.is_empty() {
true => "no supported features".to_string(),
false => format! ("{:?}", self.esmtp_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 esmtp_features: HashSet<Extension> = HashSet::new();
for line in response.message() {
if let Ok(keywords) = Extension::from_str(&line) {
for keyword in keywords {
esmtp_features.push(keyword);
}
let splitted : Vec<&str> = line.split_whitespace().collect();
let _ = match (splitted[0], splitted.len()) {
("8BITMIME", 1) => {esmtp_features.insert(Extension::EightBitMime);},
("SMTPUTF8", 1) => {esmtp_features.insert(Extension::SmtpUtfEight);},
("STARTTLS", 1) => {esmtp_features.insert(Extension::StartTls);},
("AUTH", _) => {
for &mecanism in &splitted[1..] {
match mecanism {
"PLAIN" => {esmtp_features.insert(Extension::Authentication(Mecanism::Plain));},
"CRAM-MD5" => {esmtp_features.insert(Extension::Authentication(Mecanism::CramMd5));},
_ => (),
}
}
},
(_, _) => (),
};
}
esmtp_features
Ok(ServerInfo{
name: name,
esmtp_features: esmtp_features,
})
}
/// Checks if the server supports an ESMTP feature
pub fn supports_feature(&self, keyword: &Extension) -> bool {
self.esmtp_features.contains(keyword)
}
/// Checks if the server supports an ESMTP feature
pub fn supports_auth_mecanism(&self, mecanism: Mecanism) -> bool {
self.esmtp_features.contains(&Extension::Authentication(mecanism))
}
}
#[cfg(test)]
mod test {
use super::Extension;
use response::{Severity, Category, Response, Code};
use std::collections::HashSet;
use super::{ServerInfo, Extension};
#[test]
fn test_from_str() {
assert_eq!(Extension::from_str("8BITMIME"), Ok(vec![Extension::EightBitMime]));
assert_eq!(Extension::from_str("AUTH PLAIN"), Ok(vec![Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH PLAIN LOGIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
assert_eq!(Extension::from_str("AUTH CRAM-MD5 PLAIN"), Ok(vec![Extension::CramMd5Authentication, Extension::PlainAuthentication]));
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
}
#[test]
fn test_parse_esmtp_response() {
assert_eq!(Extension::parse_esmtp_response(&Response::new(
Code::new(
"2".parse::<Severity>().unwrap(),
"2".parse::<Category>().unwrap(),
1,
),
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
)), vec![Extension::EightBitMime]);
assert_eq!(Extension::parse_esmtp_response(&Response::new(
Code::new(
"4".parse::<Severity>().unwrap(),
"3".parse::<Category>().unwrap(),
3,
),
vec!["me".to_string(), "8BITMIME".to_string(), "AUTH PLAIN CRAM-MD5".to_string()]
)), vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
fn test_serverinfo_fmt() {
let mut eightbitmime = HashSet::new();
assert!(eightbitmime.insert(Extension::EightBitMime));
let empty = HashSet::new();
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: eightbitmime.clone()
}), "name with {EightBitMime}".to_string());
assert_eq!(format!("{}", ServerInfo{
name: "name".to_string(),
esmtp_features: empty,
}), "name with no supported features".to_string());
}
}

View File

@@ -13,14 +13,12 @@ use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use SMTP_PORT;
use extension::Extension;
use extension::{Extension, ServerInfo};
use error::{SmtpResult, Error};
use sendable_email::SendableEmail;
use sender::server_info::ServerInfo;
use client::Client;
use client::net::SmtpStream;
mod server_info;
use authentication::Mecanism;
/// Contains client configuration
pub struct SenderBuilder {
@@ -171,37 +169,26 @@ impl Sender {
if self.state.connection_reuse_count == 0 {
try!(self.client.connect());
let hello_error = Error::ResponseParsingError("No hostname announced by the server");
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
// Extended Hello or Hello if needed
match self.client.ehlo(&self.client_info.hello_name) {
Ok(response) => {self.server_info = Some(
ServerInfo{
name: try_smtp!(response.first_word().ok_or(hello_error), self),
esmtp_features: Extension::parse_esmtp_response(&response),
});
},
let hello_response = match self.client.ehlo(&self.client_info.hello_name) {
Ok(response) => response,
Err(error) => match error {
Error::PermanentError(ref response) if response.has_code(550) => {
match self.client.helo(&self.client_info.hello_name) {
Ok(response) => {self.server_info = Some(
ServerInfo{
name: try_smtp!(response.first_word().ok_or(hello_error), self),
esmtp_features: vec!(),
});
},
Ok(response) => response,
Err(error) => try_smtp!(Err(error), self)
}
},
_ => {
try_smtp!(Err(error), self)
},
},
}
};
self.server_info = Some(try_smtp!(ServerInfo::from_response(&hello_response), self));
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
@@ -212,11 +199,11 @@ impl Sender {
let (username, password) = self.client_info.credentials.clone().unwrap();
if self.server_info.as_ref().unwrap().supports_feature(Extension::CramMd5Authentication) {
let result = self.client.auth_cram_md5(&username, &password);
if self.server_info.as_ref().unwrap().supports_auth_mecanism(Mecanism::CramMd5) {
let result = self.client.auth(Mecanism::CramMd5, &username, &password);
try_smtp!(result, self);
} else if self.server_info.as_ref().unwrap().supports_feature(Extension::PlainAuthentication) {
let result = self.client.auth_plain(&username, &password);
} else if self.server_info.as_ref().unwrap().supports_auth_mecanism(Mecanism::Plain) {
let result = self.client.auth(Mecanism::Plain, &username, &password);
try_smtp!(result, self);
} else {
debug!("No supported authentication mecanisms available");
@@ -229,7 +216,7 @@ impl Sender {
let message = email.message();
// Mail
let mail_options = match self.server_info.as_ref().unwrap().supports_feature(Extension::EightBitMime) {
let mail_options = match self.server_info.as_ref().unwrap().supports_feature(&Extension::EightBitMime) {
true => Some("BODY=8BITMIME"),
false => None,
};

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