feat(transport-smtp): Add support for LOGIN auth mechanism

This commit is contained in:
Alexis Mousset
2017-03-26 20:51:13 +02:00
parent 9953820174
commit 20f6c5db3f
8 changed files with 192 additions and 118 deletions

View File

@@ -552,8 +552,8 @@ impl EmailBuilder {
/// Sets the email body to plain text content
pub fn set_text<S: Into<String>>(&mut self, body: S) {
self.message.set_body(body);
self.message
.add_header(("Content-Type", format!("{}", mime!(Text/Plain; Charset=Utf8)).as_ref()));
self.message.add_header(("Content-Type",
format!("{}", mime!(Text/Plain; Charset=Utf8)).as_ref()));
}
/// Sets the email body to HTML content
@@ -565,8 +565,8 @@ impl EmailBuilder {
/// Sets the email body to HTML content
pub fn set_html<S: Into<String>>(&mut self, body: S) {
self.message.set_body(body);
self.message
.add_header(("Content-Type", format!("{}", mime!(Text/Html; Charset=Utf8)).as_ref()));
self.message.add_header(("Content-Type",
format!("{}", mime!(Text/Html; Charset=Utf8)).as_ref()));
}
/// Sets the email content
@@ -646,9 +646,9 @@ impl EmailBuilder {
let mut e = Envelope::new();
// add all receivers in to_header and cc_header
for receiver in self.to_header
.iter()
.chain(self.cc_header.iter())
.chain(self.bcc_header.iter()) {
.iter()
.chain(self.cc_header.iter())
.chain(self.bcc_header.iter()) {
match *receiver {
Address::Mailbox(ref m) => e.add_to(m.address.clone()),
Address::Group(_, ref ms) => {
@@ -691,8 +691,8 @@ impl EmailBuilder {
self.message.add_header(Header::new_with_value("To".into(), self.to_header).unwrap());
}
if !self.from_header.is_empty() {
self.message
.add_header(Header::new_with_value("From".into(), self.from_header).unwrap());
self.message.add_header(Header::new_with_value("From".into(), self.from_header)
.unwrap());
} else {
return Err(Error::MissingFrom);
}
@@ -700,9 +700,9 @@ impl EmailBuilder {
self.message.add_header(Header::new_with_value("Cc".into(), self.cc_header).unwrap());
}
if !self.reply_to_header.is_empty() {
self.message
.add_header(Header::new_with_value("Reply-To".into(), self.reply_to_header)
.unwrap());
self.message.add_header(Header::new_with_value("Reply-To".into(),
self.reply_to_header)
.unwrap());
}
if !self.date_issued {
@@ -719,10 +719,10 @@ impl EmailBuilder {
}
Ok(Email {
message: self.message.build(),
envelope: envelope,
message_id: message_id,
})
message: self.message.build(),
envelope: envelope,
message_id: message_id,
})
}
}
@@ -816,9 +816,9 @@ pub trait ExtractableEmail {
#[cfg(test)]
mod test {
use email_format::{Header, MimeMessage};
use super::{Email, EmailBuilder, Envelope, IntoEmail, SendableEmail, SimpleEmail};
use email_format::{Header, MimeMessage};
use time::now;
use uuid::Uuid;
@@ -865,12 +865,11 @@ mod test {
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
format!("<{}@rust-smtp>",
current_message))
.unwrap());
.unwrap());
email.message
.headers
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
.unwrap());
email.message.headers.insert(Header::new_with_value("To".to_string(),
"to@example.com".to_string())
.unwrap());
email.message.body = "body".to_string();

View File

@@ -31,12 +31,18 @@ impl EmailTransport<SendmailResult> for SendmailTransport {
fn send<T: SendableEmail>(&mut self, email: T) -> SendmailResult {
// Spawn the sendmail command
let mut process = try!(Command::new(&self.command)
.args(&["-i", "-f", &email.envelope().from, &email.envelope().to.join(" ")])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn());
.args(&["-i",
"-f",
&email.envelope().from,
&email.envelope().to.join(" ")])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn());
match process.stdin.as_mut().unwrap().write_all(email.message().as_bytes()) {
match process.stdin
.as_mut()
.unwrap()
.write_all(email.message().as_bytes()) {
Ok(_) => (),
Err(error) => return Err(From::from(error)),
}

View File

@@ -3,7 +3,6 @@
use crypto::hmac::Hmac;
use crypto::mac::Mac;
use crypto::md5::Md5;
use rustc_serialize::base64::{self, FromBase64, ToBase64};
use rustc_serialize::hex::ToHex;
use std::fmt;
use std::fmt::{Display, Formatter};
@@ -16,6 +15,10 @@ pub enum Mechanism {
/// PLAIN authentication mechanism
/// RFC 4616: https://tools.ietf.org/html/rfc4616
Plain,
/// LOGIN authentication mechanism
/// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login,
/// CRAM-MD5 authentication mechanism
/// RFC 2195: https://tools.ietf.org/html/rfc2195
CramMd5,
@@ -27,6 +30,7 @@ impl Display for Mechanism {
"{}",
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
Mechanism::CramMd5 => "CRAM-MD5",
})
}
@@ -37,6 +41,7 @@ impl Mechanism {
pub fn supports_initial_response(&self) -> bool {
match *self {
Mechanism::Plain => true,
Mechanism::Login |
Mechanism::CramMd5 => false,
}
}
@@ -52,30 +57,35 @@ impl Mechanism {
Mechanism::Plain => {
match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => {
Ok(format!("{}{}{}{}", NUL, username, NUL, password)
.as_bytes()
.to_base64(base64::STANDARD))
}
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)),
}
}
Mechanism::CramMd5 => {
let encoded_challenge = match challenge {
Mechanism::Login => {
let decoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
let decoded_challenge = match encoded_challenge.from_base64() {
Ok(challenge) => challenge,
Err(error) => return Err(Error::ChallengeParsing(error)),
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
return Ok(username.to_string());
}
if vec!["Password", "Password:"].contains(&decoded_challenge) {
return Ok(password.to_string());
}
Err(Error::Client("Unrecognized challenge"))
}
Mechanism::CramMd5 => {
let decoded_challenge = match challenge {
Some(challenge) => challenge,
None => return Err(Error::Client("This mechanism does expect a challenge")),
};
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
hmac.input(&decoded_challenge);
hmac.input(decoded_challenge.as_bytes());
Ok(format!("{} {}", username, hmac.result().code().to_hex())
.as_bytes()
.to_base64(base64::STANDARD))
Ok(format!("{} {}", username, hmac.result().code().to_hex()))
}
}
}
@@ -90,20 +100,30 @@ mod test {
let mechanism = Mechanism::Plain;
assert_eq!(mechanism.response("username", "password", None).unwrap(),
"AHVzZXJuYW1lAHBhc3N3b3Jk");
"\u{0}username\u{0}password");
assert!(mechanism.response("username", "password", Some("test")).is_err());
}
#[test]
fn test_login() {
let mechanism = Mechanism::Login;
assert_eq!(mechanism.response("alice", "wonderland", Some("Username")).unwrap(),
"alice");
assert_eq!(mechanism.response("alice", "wonderland", Some("Password")).unwrap(),
"wonderland");
assert!(mechanism.response("username", "password", None).is_err());
}
#[test]
fn test_cram_md5() {
let mechanism = Mechanism::CramMd5;
assert_eq!(mechanism.response("alice",
"wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
"wonderland",
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
.unwrap(),
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
assert!(mechanism.response("alice", "wonderland", Some("tést")).is_err());
"alice a540ebe4ef2304070bbc3c456c1f64c0");
assert!(mechanism.response("alice", "wonderland", None).is_err());
}
}

View File

@@ -2,6 +2,8 @@
use bufstream::BufStream;
use openssl::ssl::SslContext;
use rustc_serialize::base64::{self, FromBase64, ToBase64};
use std::fmt::Debug;
use std::io;
use std::io::{BufRead, Read, Write};
@@ -202,20 +204,47 @@ impl<S: Connector + Timeout + Write + Read + Debug> Client<S> {
if mechanism.supports_initial_response() {
self.command(&format!("AUTH {} {}",
mechanism,
try!(mechanism.response(username, password, None))))
try!(mechanism.response(username, password, None))
.as_bytes()
.to_base64(base64::STANDARD)))
} else {
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
let encoded_challenge = match try!(self.command(&format!("AUTH {}", mechanism)))
.first_word() {
Some(challenge) => challenge,
None => return Err(Error::ResponseParsing("Could not read CRAM challenge")),
None => return Err(Error::ResponseParsing("Could not read auth challenge")),
};
debug!("CRAM challenge: {}", encoded_challenge);
debug!("auth encoded challenge: {}", encoded_challenge);
let cram_response = try!(mechanism.response(username,
password,
Some(&encoded_challenge)));
let decoded_challenge = match encoded_challenge.from_base64() {
Ok(challenge) => {
match String::from_utf8(challenge) {
Ok(value) => value,
Err(error) => return Err(Error::Utf8Parsing(error)),
}
}
Err(error) => return Err(Error::ChallengeParsing(error)),
};
self.command(&cram_response.clone())
debug!("auth decoded challenge: {}", decoded_challenge);
let mut challenge_expected = 3;
while challenge_expected > 0 {
let response = try!(self.command(&try!(mechanism.response(username,
password,
Some(&decoded_challenge)))
.as_bytes()
.to_base64(base64::STANDARD)));
if !response.has_code(334) {
return Ok(response);
}
challenge_expected -= 1;
}
Err(Error::ResponseParsing("Unexpected number of challenges"))
}
}
@@ -236,7 +265,10 @@ impl<S: Connector + Timeout + Write + Read + Debug> Client<S> {
}
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
try!(self.stream.as_mut().unwrap().flush());
try!(self.stream
.as_mut()
.unwrap()
.flush());
debug!("Wrote: {}", escape_crlf(string));
@@ -249,13 +281,19 @@ impl<S: Connector + Timeout + Write + Read + Debug> Client<S> {
let mut parser = ResponseParser::default();
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));
debug!("Read: {}", escape_crlf(line.as_ref()));
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
line.clear();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
try!(self.stream
.as_mut()
.unwrap()
.read_line(&mut line));
}
let response = try!(parser.response());

View File

@@ -1,11 +1,12 @@
//! Error and result type for SMTP clients
use rustc_serialize::base64::FromBase64Error;
use self::Error::*;
use rustc_serialize::base64::FromBase64Error;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io;
use std::string::FromUtf8Error;
use transport::smtp::response::{Response, Severity};
/// An enum of all error kinds.
@@ -23,6 +24,8 @@ pub enum Error {
ResponseParsing(&'static str),
/// Error parsing a base64 string in response
ChallengeParsing(FromBase64Error),
/// Error parsing UTF8in response
Utf8Parsing(FromUtf8Error),
/// Internal client error
Client(&'static str),
/// DNS resolution error
@@ -43,7 +46,8 @@ impl StdError for Error {
Transient(_) => "a transient error occured during the SMTP transaction",
Permanent(_) => "a permanent error occured during the SMTP transaction",
ResponseParsing(_) => "an error occured while parsing an SMTP response",
ChallengeParsing(_) => "an error occured while parsing a CRAM-MD5 challenge",
ChallengeParsing(_) => "an error occured while parsing an SMTP AUTH challenge",
Utf8Parsing(_) => "an error occured while parsing an SMTP response as UTF8",
Resolution => "could not resolve hostname",
Client(_) => "an unknown error occured",
Io(_) => "an I/O error occured",

View File

@@ -104,9 +104,9 @@ impl ServerInfo {
}
Ok(ServerInfo {
name: name,
features: features,
})
name: name,
features: features,
})
}
/// Checks if the server supports an ESMTP feature
@@ -122,9 +122,9 @@ impl ServerInfo {
#[cfg(test)]
mod test {
use std::collections::HashSet;
use super::{Extension, ServerInfo};
use std::collections::HashSet;
use transport::smtp::authentication::Mechanism;
use transport::smtp::response::{Category, Code, Response, Severity};

View File

@@ -100,17 +100,17 @@ impl SmtpTransportBuilder {
match addresses.next() {
Some(addr) => {
Ok(SmtpTransportBuilder {
server_addr: addr,
ssl_context: SslContext::builder(SslMethod::tls()).unwrap().build(),
security_level: SecurityLevel::Opportunistic,
smtp_utf8: false,
credentials: None,
connection_reuse_count_limit: 100,
connection_reuse: false,
hello_name: "localhost".to_string(),
authentication_mechanism: None,
timeout: Some(Duration::new(60, 0)),
})
server_addr: addr,
ssl_context: SslContext::builder(SslMethod::tls()).unwrap().build(),
security_level: SecurityLevel::Opportunistic,
smtp_utf8: false,
credentials: None,
connection_reuse_count_limit: 100,
connection_reuse: false,
hello_name: "localhost".to_string(),
authentication_mechanism: None,
timeout: Some(Duration::new(60, 0)),
})
}
None => Err(From::from("Could nor resolve hostname")),
}
@@ -313,10 +313,11 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
try!(self.get_ehlo());
match (&self.client_info.security_level,
self.server_info.as_ref().unwrap().supports_feature(&Extension::StartTls)) {
(&SecurityLevel::AlwaysEncrypt, false) => {
return Err(From::from("Could not encrypt connection, aborting"))
}
self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::StartTls)) {
(&SecurityLevel::AlwaysEncrypt, false) => return Err(From::from("Could not encrypt connection, aborting")),
(&SecurityLevel::Opportunistic, false) => (),
(&SecurityLevel::NeverEncrypt, _) => (),
(&SecurityLevel::EncryptedWrapper, _) => (),
@@ -333,7 +334,10 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
}
if self.client_info.credentials.is_some() {
let (username, password) = self.client_info.credentials.clone().unwrap();
let (username, password) = self.client_info
.credentials
.clone()
.unwrap();
let mut found = false;
@@ -344,7 +348,8 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
if self.client.is_encrypted() {
// If encrypted, allow all mechanisms, with a preference for the
// simplest
vec![Mechanism::Plain, Mechanism::CramMd5]
// Login is obsolete so try it last
vec![Mechanism::Plain, Mechanism::CramMd5, Mechanism::Login]
} else {
// If not encrypted, do not allow clear-text passwords
vec![Mechanism::CramMd5]
@@ -353,7 +358,10 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
};
for mechanism in accepted_mechanisms {
if self.server_info.as_ref().unwrap().supports_auth_mechanism(mechanism) {
if self.server_info
.as_ref()
.unwrap()
.supports_auth_mechanism(mechanism) {
found = true;
try_smtp!(self.client.auth(mechanism, &username, &password), self);
break;
@@ -368,13 +376,13 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
// Mail
let mail_options = match (self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::EightBitMime),
self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::SmtpUtfEight)) {
.as_ref()
.unwrap()
.supports_feature(&Extension::EightBitMime),
self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::SmtpUtfEight)) {
(true, true) => Some("BODY=8BITMIME SMTPUTF8"),
(true, false) => Some("BODY=8BITMIME"),
(false, _) => None,
@@ -409,12 +417,12 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
self.state.connection_reuse_count,
message.len(),
result.as_ref()
.ok()
.unwrap()
.message()
.iter()
.next()
.unwrap_or(&"no response".to_string()));
.ok()
.unwrap()
.message()
.iter()
.next()
.unwrap_or(&"no response".to_string()));
}
// Test if we can reuse the existing connection

View File

@@ -116,10 +116,10 @@ impl FromStr for Code {
s[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => {
Ok(Code {
severity: severity,
category: category,
detail: detail,
})
severity: severity,
category: category,
detail: detail,
})
}
_ => Err(Error::ResponseParsing("Could not parse response code")),
}
@@ -217,8 +217,7 @@ impl Response {
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.code.severity {
PositiveCompletion => true,
PositiveIntermediate => true,
PositiveCompletion | PositiveIntermediate => true,
_ => false,
}
}
@@ -401,7 +400,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.is_positive());
.is_positive());
assert!(!Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
@@ -410,7 +409,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.is_positive());
.is_positive());
}
#[test]
@@ -423,7 +422,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.message(),
.message(),
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
let empty_message: Vec<String> = vec![];
assert_eq!(Response::new(Code {
@@ -432,7 +431,7 @@ mod test {
detail: 1,
},
vec![])
.message(),
.message(),
empty_message);
}
@@ -446,7 +445,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.severity(),
.severity(),
Severity::PositiveCompletion);
assert_eq!(Response::new(Code {
severity: "5".parse::<Severity>().unwrap(),
@@ -456,7 +455,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.severity(),
.severity(),
Severity::PermanentNegativeCompletion);
}
@@ -470,7 +469,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.category(),
.category(),
Category::Unspecified4);
}
@@ -484,7 +483,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.detail(),
.detail(),
1);
}
@@ -498,7 +497,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.code(),
.code(),
"241");
}
@@ -512,7 +511,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(241));
.has_code(241));
assert!(!Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
category: "4".parse::<Category>().unwrap(),
@@ -521,7 +520,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.has_code(251));
.has_code(251));
}
#[test]
@@ -534,7 +533,7 @@ mod test {
vec!["me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.first_word(),
.first_word(),
Some("me".to_string()));
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
@@ -544,7 +543,7 @@ mod test {
vec!["me mo".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string()])
.first_word(),
.first_word(),
Some("me".to_string()));
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
@@ -552,7 +551,7 @@ mod test {
detail: 1,
},
vec![])
.first_word(),
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
@@ -560,7 +559,7 @@ mod test {
detail: 1,
},
vec![" ".to_string()])
.first_word(),
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
@@ -568,7 +567,7 @@ mod test {
detail: 1,
},
vec![" ".to_string()])
.first_word(),
.first_word(),
None);
assert_eq!(Response::new(Code {
severity: "2".parse::<Severity>().unwrap(),
@@ -576,7 +575,7 @@ mod test {
detail: 1,
},
vec!["".to_string()])
.first_word(),
.first_word(),
None);
}
}