From ed7c16452ce4e0d28a7611bf81e333e307fb0be0 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Tue, 15 May 2018 01:04:16 +0200 Subject: [PATCH] feat(transport): Initial support for XOAUTH2 --- lettre/src/smtp/authentication.rs | 34 +++++++++++++++++++++++++------ lettre/src/smtp/client/mod.rs | 5 ++--- lettre/src/smtp/commands.rs | 33 +++++++++++------------------- lettre/src/smtp/extension.rs | 6 +++++- lettre/src/smtp/mod.rs | 19 +---------------- 5 files changed, 48 insertions(+), 49 deletions(-) diff --git a/lettre/src/smtp/authentication.rs b/lettre/src/smtp/authentication.rs index 589f909..be15826 100644 --- a/lettre/src/smtp/authentication.rs +++ b/lettre/src/smtp/authentication.rs @@ -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()); + } } diff --git a/lettre/src/smtp/client/mod.rs b/lettre/src/smtp/client/mod.rs index 7ab3296..9328e5e 100644 --- a/lettre/src/smtp/client/mod.rs +++ b/lettre/src/smtp/client/mod.rs @@ -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 "\" /// Used for debug displays fn escape_crlf(string: &str) -> String { - string.replace(CRLF, "") + string.replace("\r\n", "") } /// Structure that implements the SMTP client @@ -219,7 +218,7 @@ impl InnerClient { self.write(out_buf.as_slice())?; } - self.write(MESSAGE_ENDING.as_bytes())?; + self.write("\r\n.\r\n".as_bytes())?; self.read_response() } diff --git a/lettre/src/smtp/commands.rs b/lettre/src/smtp/commands.rs index 9f3762a..133c263 100644 --- a/lettre/src/smtp/commands.rs +++ b/lettre/src/smtp/commands.rs @@ -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") } } diff --git a/lettre/src/smtp/extension.rs b/lettre/src/smtp/extension.rs index b4fba39..9bd55dc 100644 --- a/lettre/src/smtp/extension.rs +++ b/lettre/src/smtp/extension.rs @@ -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(), diff --git a/lettre/src/smtp/mod.rs b/lettre/src/smtp/mod.rs index 4d6d84f..4f0d780 100644 --- a/lettre/src/smtp/mod.rs +++ b/lettre/src/smtp/mod.rs @@ -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)]