diff --git a/src/email/mod.rs b/src/email/mod.rs index 47eeb35..5e94119 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -552,8 +552,8 @@ impl EmailBuilder { /// Sets the email body to plain text content pub fn set_text>(&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>(&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(); diff --git a/src/transport/sendmail/mod.rs b/src/transport/sendmail/mod.rs index 4ef989b..309a7e6 100644 --- a/src/transport/sendmail/mod.rs +++ b/src/transport/sendmail/mod.rs @@ -31,12 +31,18 @@ impl EmailTransport for SendmailTransport { fn send(&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)), } diff --git a/src/transport/smtp/authentication.rs b/src/transport/smtp/authentication.rs index e119d44..e7ac401 100644 --- a/src/transport/smtp/authentication.rs +++ b/src/transport/smtp/authentication.rs @@ -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()); } } diff --git a/src/transport/smtp/client/mod.rs b/src/transport/smtp/client/mod.rs index 65ac325..69c98a2 100644 --- a/src/transport/smtp/client/mod.rs +++ b/src/transport/smtp/client/mod.rs @@ -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 Client { 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 Client { } 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 Client { 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()); diff --git a/src/transport/smtp/error.rs b/src/transport/smtp/error.rs index ba04702..942fb8e 100644 --- a/src/transport/smtp/error.rs +++ b/src/transport/smtp/error.rs @@ -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", diff --git a/src/transport/smtp/extension.rs b/src/transport/smtp/extension.rs index d2de9da..2954e77 100644 --- a/src/transport/smtp/extension.rs +++ b/src/transport/smtp/extension.rs @@ -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}; diff --git a/src/transport/smtp/mod.rs b/src/transport/smtp/mod.rs index 8efac17..8db7db9 100644 --- a/src/transport/smtp/mod.rs +++ b/src/transport/smtp/mod.rs @@ -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 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 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 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 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 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 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 diff --git a/src/transport/smtp/response.rs b/src/transport/smtp/response.rs index 2c5dcef..d1421b6 100644 --- a/src/transport/smtp/response.rs +++ b/src/transport/smtp/response.rs @@ -116,10 +116,10 @@ impl FromStr for Code { s[2..3].parse::()) { (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::().unwrap(), category: "4".parse::().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 = 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::().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::().unwrap(), category: "4".parse::().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::().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::().unwrap(), @@ -552,7 +551,7 @@ mod test { detail: 1, }, vec![]) - .first_word(), + .first_word(), None); assert_eq!(Response::new(Code { severity: "2".parse::().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::().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::().unwrap(), @@ -576,7 +575,7 @@ mod test { detail: 1, }, vec!["".to_string()]) - .first_word(), + .first_word(), None); } }