feat(transport): Initial support for XOAUTH2

This commit is contained in:
Alexis Mousset
2018-05-15 01:04:16 +02:00
parent bc09aa2185
commit ed7c16452c
5 changed files with 48 additions and 49 deletions

View File

@@ -1,6 +1,5 @@
//! Provides limited SASL authentication mechanisms
use smtp::NUL;
use smtp::error::Error;
use std::fmt::{self, Display, Formatter};
@@ -56,6 +55,9 @@ pub enum Mechanism {
/// Obsolete but needed for some providers (like office365)
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
Login,
/// Non-standard XOAUTH2 mechanism
/// https://developers.google.com/gmail/imap/xoauth2-protocol
Xoauth2,
}
impl Display for Mechanism {
@@ -66,6 +68,7 @@ impl Display for Mechanism {
match *self {
Mechanism::Plain => "PLAIN",
Mechanism::Login => "LOGIN",
Mechanism::Xoauth2 => "XOAUTH2",
}
)
}
@@ -73,10 +76,9 @@ impl Display for Mechanism {
impl Mechanism {
/// Does the mechanism supports initial response
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
pub fn supports_initial_response(&self) -> bool {
match *self {
Mechanism::Plain => true,
Mechanism::Plain | Mechanism::Xoauth2 => true,
Mechanism::Login => false,
}
}
@@ -92,8 +94,8 @@ impl Mechanism {
Mechanism::Plain => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"{}{}{}{}",
NUL, credentials.authentication_identity, NUL, credentials.secret
"\u{0}{}\u{0}{}",
credentials.authentication_identity, credentials.secret
)),
},
Mechanism::Login => {
@@ -111,7 +113,14 @@ impl Mechanism {
}
Err(Error::Client("Unrecognized challenge"))
}
},
Mechanism::Xoauth2 => match challenge {
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
None => Ok(format!(
"user={}\x01auth=Bearer {}\x01\x01",
credentials.authentication_identity, credentials.secret
)),
},
}
}
}
@@ -149,4 +158,17 @@ mod test {
);
assert!(mechanism.response(&credentials, None).is_err());
}
#[test]
fn test_xoauth2() {
let mechanism = Mechanism::Xoauth2;
let credentials = Credentials::new("username".to_string(), "vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string());
assert_eq!(
mechanism.response(&credentials, None).unwrap(),
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
);
assert!(mechanism.response(&credentials, Some("test")).is_err());
}
}

View File

@@ -2,7 +2,6 @@
use bufstream::BufStream;
use nom::ErrorKind as NomErrorKind;
use smtp::{CRLF, MESSAGE_ENDING};
use smtp::authentication::{Credentials, Mechanism};
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
use smtp::commands::*;
@@ -72,7 +71,7 @@ impl ClientCodec {
/// Returns the string replacing all the CRLF with "\<CRLF\>"
/// Used for debug displays
fn escape_crlf(string: &str) -> String {
string.replace(CRLF, "<CRLF>")
string.replace("\r\n", "<CRLF>")
}
/// Structure that implements the SMTP client
@@ -219,7 +218,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
self.write(out_buf.as_slice())?;
}
self.write(MESSAGE_ENDING.as_bytes())?;
self.write("\r\n.\r\n".as_bytes())?;
self.read_response()
}

View File

@@ -2,7 +2,6 @@
use EmailAddress;
use base64;
use smtp::CRLF;
use smtp::authentication::{Credentials, Mechanism};
use smtp::error::Error;
use smtp::extension::{MailParameter, RcptParameter};
@@ -19,8 +18,7 @@ pub struct EhloCommand {
impl Display for EhloCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EHLO {}", self.client_id)?;
f.write_str(CRLF)
write!(f, "EHLO {}\r\n", self.client_id)
}
}
@@ -38,8 +36,7 @@ pub struct StarttlsCommand;
impl Display for StarttlsCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("STARTTLS")?;
f.write_str(CRLF)
f.write_str("STARTTLS\r\n")
}
}
@@ -64,7 +61,7 @@ impl Display for MailCommand {
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str(CRLF)
f.write_str("\r\n")
}
}
@@ -89,7 +86,7 @@ impl Display for RcptCommand {
for parameter in &self.parameters {
write!(f, " {}", parameter)?;
}
f.write_str(CRLF)
f.write_str("\r\n")
}
}
@@ -110,8 +107,7 @@ pub struct DataCommand;
impl Display for DataCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("DATA")?;
f.write_str(CRLF)
f.write_str("DATA\r\n")
}
}
@@ -122,8 +118,7 @@ pub struct QuitCommand;
impl Display for QuitCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("QUIT")?;
f.write_str(CRLF)
f.write_str("QUIT\r\n")
}
}
@@ -134,8 +129,7 @@ pub struct NoopCommand;
impl Display for NoopCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("NOOP")?;
f.write_str(CRLF)
f.write_str("NOOP\r\n")
}
}
@@ -152,7 +146,7 @@ impl Display for HelpCommand {
if self.argument.is_some() {
write!(f, " {}", self.argument.as_ref().unwrap())?;
}
f.write_str(CRLF)
f.write_str("\r\n")
}
}
@@ -172,8 +166,7 @@ pub struct VrfyCommand {
impl Display for VrfyCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "VRFY {}", self.argument)?;
f.write_str(CRLF)
write!(f, "VRFY {}\r\n", self.argument)
}
}
@@ -193,8 +186,7 @@ pub struct ExpnCommand {
impl Display for ExpnCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "EXPN {}", self.argument)?;
f.write_str(CRLF)
write!(f, "EXPN {}\r\n", self.argument)
}
}
@@ -212,8 +204,7 @@ pub struct RsetCommand;
impl Display for RsetCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("RSET")?;
f.write_str(CRLF)
f.write_str("RSET\r\n")
}
}
@@ -246,7 +237,7 @@ impl Display for AuthCommand {
None => write!(f, "AUTH {}", self.mechanism)?,
}
}
f.write_str(CRLF)
f.write_str("\r\n")
}
}

View File

@@ -145,6 +145,9 @@ impl ServerInfo {
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
"XOAUTH2" => {
features.insert(Extension::Authentication(Mechanism::Xoauth2));
}
_ => (),
}
},
@@ -362,7 +365,7 @@ mod test {
),
vec![
"me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
@@ -371,6 +374,7 @@ mod test {
let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime));
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
let server_info2 = ServerInfo {
name: "me".to_string(),

View File

@@ -8,7 +8,7 @@
//! It implements the following extensions:
//!
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN mechanisms
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
//!
@@ -44,23 +44,6 @@ pub const SMTP_PORT: u16 = 25;
/// Default submission port
pub const SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub const SP: &str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub const CRLF: &str = "\r\n";
/// Colon
pub const COLON: &str = ":";
/// The ending of message content
pub const MESSAGE_ENDING: &str = "\r\n.\r\n";
/// NUL unicode character
pub const NUL: &str = "\0";
/// How to apply TLS to a client connection
#[derive(Clone)]
#[allow(missing_debug_implementations)]