From 01fde07a48456b8d369fe59ca4d3123cd0a1f2fd Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Sun, 8 Oct 2017 15:46:50 +0200 Subject: [PATCH] feat(transport): Use nom for parsing smtp responses --- lettre/Cargo.toml | 3 +- lettre/src/lib.rs | 3 + lettre/src/smtp/client/mod.rs | 50 ++--- lettre/src/smtp/error.rs | 11 ++ lettre/src/smtp/extension.rs | 4 + lettre/src/smtp/response.rs | 336 +++++++++++++++++----------------- 6 files changed, 202 insertions(+), 205 deletions(-) diff --git a/lettre/Cargo.toml b/lettre/Cargo.toml index b708956..c9c880a 100644 --- a/lettre/Cargo.toml +++ b/lettre/Cargo.toml @@ -16,6 +16,7 @@ travis-ci = { repository = "lettre/lettre" } [dependencies] log = "^0.3" +nom = { version = "^3.2", optional = true } bufstream = { version = "^0.1", optional = true } native-tls = { version = "^0.1", optional = true } base64 = { version = "^0.8", optional = true } @@ -34,7 +35,7 @@ unstable = [] serde-impls = ["serde", "serde_derive"] file-transport = ["serde-impls", "serde_json"] crammd5-auth = ["rust-crypto", "hex"] -smtp-transport = ["bufstream", "native-tls", "base64"] +smtp-transport = ["bufstream", "native-tls", "base64", "nom"] sendmail-transport = [] [[example]] diff --git a/lettre/src/lib.rs b/lettre/src/lib.rs index 6fbb5c1..3ee1e67 100644 --- a/lettre/src/lib.rs +++ b/lettre/src/lib.rs @@ -23,6 +23,9 @@ extern crate serde_json; #[cfg(feature = "serde-impls")] #[macro_use] extern crate serde_derive; +#[cfg(feature = "smtp-transport")] +#[macro_use] +extern crate nom; #[cfg(feature = "smtp-transport")] pub mod smtp; diff --git a/lettre/src/smtp/client/mod.rs b/lettre/src/smtp/client/mod.rs index 56896f5..2844e4b 100644 --- a/lettre/src/smtp/client/mod.rs +++ b/lettre/src/smtp/client/mod.rs @@ -1,18 +1,20 @@ //! SMTP client 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::*; use smtp::error::{Error, SmtpResult}; -use smtp::response::ResponseParser; +use smtp::response::Response; use std::fmt::{Debug, Display}; use std::io::{self, BufRead, BufReader, Read, Write}; use std::net::ToSocketAddrs; use std::string::String; use std::time::Duration; + pub mod net; pub mod mock; @@ -73,12 +75,6 @@ fn escape_crlf(string: &str) -> String { string.replace(CRLF, "") } -/// Returns the string removing all the CRLF -/// Used for debug displays -fn remove_crlf(string: &str) -> String { - string.replace(CRLF, "") -} - /// Structure that implements the SMTP client #[derive(Debug, Default)] pub struct Client { @@ -253,31 +249,33 @@ impl Client { /// Gets the SMTP response fn get_reply(&mut self) -> SmtpResult { - let mut parser = ResponseParser::default(); + let mut raw_response = String::new(); + let mut response = raw_response.parse::(); - let mut line = String::new(); - self.stream.as_mut().unwrap().read_line(&mut line)?; - - debug!("Read: {}", escape_crlf(line.as_ref())); - - while parser.read_line(remove_crlf(line.as_ref()).as_ref())? { - line.clear(); - self.stream.as_mut().unwrap().read_line(&mut line)?; + while response.is_err() { + if response.as_ref().err().unwrap() != &NomErrorKind::Complete { + break; + } + // TODO read more than one line + self.stream.as_mut().unwrap().read_line(&mut raw_response)?; + response = raw_response.parse::(); } - let response = parser.response()?; + debug!("Read: {}", escape_crlf(raw_response.as_ref())); - if response.is_positive() { - Ok(response) + let final_response = response?; + + if final_response.is_positive() { + Ok(final_response) } else { - Err(From::from(response)) + Err(From::from(final_response)) } } } #[cfg(test)] mod test { - use super::{ClientCodec, escape_crlf, remove_crlf}; + use super::{ClientCodec, escape_crlf}; #[test] fn test_codec() { @@ -299,16 +297,6 @@ mod test { ); } - #[test] - fn test_remove_crlf() { - assert_eq!(remove_crlf("\r\n"), ""); - assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name"); - assert_eq!( - remove_crlf("EHLO my_name\r\nSIZE 42\r\n"), - "EHLO my_nameSIZE 42" - ); - } - #[test] fn test_escape_crlf() { assert_eq!(escape_crlf("\r\n"), ""); diff --git a/lettre/src/smtp/error.rs b/lettre/src/smtp/error.rs index 8309253..b05b904 100644 --- a/lettre/src/smtp/error.rs +++ b/lettre/src/smtp/error.rs @@ -3,6 +3,7 @@ use self::Error::*; use base64::DecodeError; use native_tls; +use nom; use smtp::response::{Response, Severity}; use std::error::Error as StdError; use std::fmt; @@ -35,6 +36,8 @@ pub enum Error { Io(io::Error), /// TLS error Tls(native_tls::Error), + /// Parsing error + Parsing(nom::simple_errors::Err), } impl Display for Error { @@ -68,6 +71,7 @@ impl StdError for Error { Client(err) => err, Io(ref err) => err.description(), Tls(ref err) => err.description(), + Parsing(ref err) => err.description(), } } @@ -77,6 +81,7 @@ impl StdError for Error { Utf8Parsing(ref err) => Some(&*err as &StdError), Io(ref err) => Some(&*err as &StdError), Tls(ref err) => Some(&*err as &StdError), + Parsing(ref err) => Some(&*err as &StdError), _ => None, } } @@ -94,6 +99,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: nom::simple_errors::Err) -> Error { + Parsing(err) + } +} + impl From for Error { fn from(response: Response) -> Error { match response.code.severity { diff --git a/lettre/src/smtp/extension.rs b/lettre/src/smtp/extension.rs index 0d007ba..48e2a27 100644 --- a/lettre/src/smtp/extension.rs +++ b/lettre/src/smtp/extension.rs @@ -106,6 +106,10 @@ impl ServerInfo { let mut features: HashSet = HashSet::new(); for line in response.message.as_slice() { + if line.is_empty() { + continue; + } + let splitted: Vec<&str> = line.split_whitespace().collect(); match splitted[0] { "8BITMIME" => { diff --git a/lettre/src/smtp/response.rs b/lettre/src/smtp/response.rs index d1d321b..f42df7f 100644 --- a/lettre/src/smtp/response.rs +++ b/lettre/src/smtp/response.rs @@ -1,53 +1,43 @@ //! SMTP response, containing a mandatory return code and an optional text //! message -use self::Category::*; use self::Severity::*; -use smtp::error::{Error, SmtpResult}; +use nom::{ErrorKind as NomErrorKind, IResult as NomResult, crlf}; + +use nom::simple_errors::Err as NomError; use std::fmt::{Display, Formatter, Result}; use std::result; -use std::str::FromStr; +use std::str::{FromStr, from_utf8}; + /// First digit indicates severity #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum Severity { /// 2yx - PositiveCompletion, + PositiveCompletion = 2, /// 3yz - PositiveIntermediate, + PositiveIntermediate = 3, /// 4yz - TransientNegativeCompletion, + TransientNegativeCompletion = 4, /// 5yz - PermanentNegativeCompletion, + PermanentNegativeCompletion = 5, } impl FromStr for Severity { - type Err = Error; - fn from_str(s: &str) -> result::Result { - match s { - "2" => Ok(PositiveCompletion), - "3" => Ok(PositiveIntermediate), - "4" => Ok(TransientNegativeCompletion), - "5" => Ok(PermanentNegativeCompletion), - _ => Err(Error::ResponseParsing( - "First digit must be between 2 and 5", - )), + type Err = NomError; + + fn from_str(s: &str) -> result::Result { + match parse_severity(s.as_bytes()) { + NomResult::Done(_, res) => Ok(res), + NomResult::Error(e) => Err(e), + NomResult::Incomplete(_) => Err(NomErrorKind::Complete), } } } impl Display for Severity { fn fmt(&self, f: &mut Formatter) -> Result { - write!( - f, - "{}", - match *self { - PositiveCompletion => 2, - PositiveIntermediate => 3, - TransientNegativeCompletion => 4, - PermanentNegativeCompletion => 5, - } - ) + write!(f, "{}", *self as u8) } } @@ -55,50 +45,34 @@ impl Display for Severity { #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum Category { /// x0z - Syntax, + Syntax = 0, /// x1z - Information, + Information = 1, /// x2z - Connections, + Connections = 2, /// x3z - Unspecified3, + Unspecified3 = 3, /// x4z - Unspecified4, + Unspecified4 = 4, /// x5z - MailSystem, + MailSystem = 5, } impl FromStr for Category { - type Err = Error; - fn from_str(s: &str) -> result::Result { - match s { - "0" => Ok(Syntax), - "1" => Ok(Information), - "2" => Ok(Connections), - "3" => Ok(Unspecified3), - "4" => Ok(Unspecified4), - "5" => Ok(MailSystem), - _ => Err(Error::ResponseParsing( - "Second digit must be between 0 and 5", - )), + type Err = NomError; + + fn from_str(s: &str) -> result::Result { + match parse_category(s.as_bytes()) { + NomResult::Done(_, res) => Ok(res), + NomResult::Error(e) => Err(e), + NomResult::Incomplete(_) => Err(NomErrorKind::Complete), } } } impl Display for Category { fn fmt(&self, f: &mut Formatter) -> Result { - write!( - f, - "{}", - match *self { - Syntax => 0, - Information => 1, - Connections => 2, - Unspecified3 => 3, - Unspecified4 => 4, - MailSystem => 5, - } - ) + write!(f, "{}", *self as u8) } } @@ -107,17 +81,18 @@ impl Display for Category { pub struct Detail(pub u8); impl FromStr for Detail { - type Err = Error; - fn from_str(s: &str) -> result::Result { - match s.parse::() { - Ok(d) if d < 10 => Ok(Detail(d)), - _ => Err(Error::ResponseParsing( - "Third digit must be between 0 and 9", - )), + type Err = NomError; + + fn from_str(s: &str) -> result::Result { + match parse_detail(s.as_bytes()) { + NomResult::Done(_, res) => Ok(res), + NomResult::Error(e) => Err(e), + NomResult::Incomplete(_) => Err(NomErrorKind::Complete), } } } + impl Display for Detail { fn fmt(&self, f: &mut Formatter) -> Result { write!(f, "{}", self.0) @@ -142,29 +117,13 @@ impl Display for Code { } impl FromStr for Code { - type Err = Error; + type Err = NomError; - #[inline] - fn from_str(s: &str) -> result::Result { - if s.len() == 3 { - match ( - s[0..1].parse::(), - s[1..2].parse::(), - s[2..3].parse::(), - ) { - (Ok(severity), Ok(category), Ok(detail)) => { - Ok(Code { - severity: severity, - category: category, - detail: detail, - }) - } - _ => Err(Error::ResponseParsing("Could not parse response code")), - } - } else { - Err(Error::ResponseParsing( - "Wrong code length (should be 3 digit)", - )) + fn from_str(s: &str) -> result::Result { + match parse_code(s.as_bytes()) { + NomResult::Done(_, res) => Ok(res), + NomResult::Error(e) => Err(e), + NomResult::Incomplete(_) => Err(NomErrorKind::Complete), } } } @@ -184,60 +143,6 @@ impl Code { } } -/// Parses an SMTP response -#[derive(PartialEq, Eq, Clone, Debug, Default)] -pub struct ResponseParser { - /// Response code - code: Option, - /// Server response string (optional) - /// Handle multiline responses - message: Vec, -} - -impl ResponseParser { - /// Parses a line and return a `bool` indicating if there are more lines to come - pub fn read_line(&mut self, line: &str) -> result::Result { - - if line.len() < 3 { - return Err(Error::ResponseParsing( - "Incorrect response code (should be 3 digits)", - )); - } - - match self.code { - Some(ref code) => { - if code.to_string() != line[0..3] { - return Err(Error::ResponseParsing( - "Response code has changed during a \ - reponse", - )); - } - } - None => self.code = Some(line[0..3].parse::()?), - } - - if line.len() > 4 { - self.message.push(line[4..].to_string()); - Ok(line.as_bytes()[3] == b'-') - } else { - Ok(false) - } - } - - /// Builds a response from a `ResponseParser` - pub fn response(self) -> SmtpResult { - match self.code { - Some(code) => Ok(Response::new(code, self.message)), - None => { - Err(Error::ResponseParsing( - "Incomplete response, could not read response \ - code", - )) - } - } - } -} - /// Contains an SMTP reply, with separated code and message /// /// The text message is optional, only the code is mandatory @@ -250,6 +155,18 @@ pub struct Response { pub message: Vec, } +impl FromStr for Response { + type Err = NomError; + + fn from_str(s: &str) -> result::Result { + match parse_response(s.as_bytes()) { + NomResult::Done(_, res) => Ok(res), + NomResult::Error(e) => Err(e), + NomResult::Incomplete(_) => Err(NomErrorKind::Complete), + } + } +} + impl Response { /// Creates a new `Response` pub fn new(code: Code, message: Vec) -> Response { @@ -285,9 +202,111 @@ impl Response { } } +// Parsers (originaly from tokio-smtp) + +named!(parse_code, + map!( + tuple!(parse_severity, parse_category, parse_detail), + |(severity, category, detail)| { + Code { + severity: severity, + category: category, + detail: detail, + } + } + ) +); + +named!(parse_detail, + complete!(alt!( + tag!("0") => { |_| Detail(0) } | + tag!("1") => { |_| Detail(1) } | + tag!("2") => { |_| Detail(2) } | + tag!("3") => { |_| Detail(3) } | + tag!("4") => { |_| Detail(4) } | + tag!("5") => { |_| Detail(5) } | + tag!("6") => { |_| Detail(6) } | + tag!("7") => { |_| Detail(7) } | + tag!("8") => { |_| Detail(8) } | + tag!("9") => { |_| Detail(9) } + )) +); + +named!(parse_severity, + complete!(alt!( + tag!("2") => { |_| Severity::PositiveCompletion } | + tag!("3") => { |_| Severity::PositiveIntermediate } | + tag!("4") => { |_| Severity::TransientNegativeCompletion } | + tag!("5") => { |_| Severity::PermanentNegativeCompletion } + )) +); + +named!(parse_category, + complete!(alt!( + tag!("0") => { |_| Category::Syntax } | + tag!("1") => { |_| Category::Information } | + tag!("2") => { |_| Category::Connections } | + tag!("3") => { |_| Category::Unspecified3 } | + tag!("4") => { |_| Category::Unspecified4 } | + tag!("5") => { |_| Category::MailSystem } + )) +); + +named!(parse_response, + map_res!( + tuple!( + // Parse any number of continuation lines. + many0!( + tuple!( + parse_code, + preceded!( + char!('-'), + take_until_and_consume!(b"\r\n".as_ref()) + ) + ) + ), + // Parse the final line. + tuple!( + parse_code, + terminated!( + opt!( + preceded!( + char!(' '), + take_until!(b"\r\n".as_ref()) + ) + ), + crlf + ) + ) + ), + |(lines, (last_code, last_line)): (Vec<_>, _)| { + // Check that all codes are equal. + if !lines.iter().all(|&(ref code, _)| *code == last_code) { + return Err(()); + } + + // Extract text from lines, and append last line. + let mut lines = lines.into_iter() + .map(|(_, text)| text) + .collect::>(); + if let Some(text) = last_line { + lines.push(text); + } + + Ok(Response { + code: last_code, + message: lines.into_iter() + .map(|line| from_utf8(line).map(|s| s.to_string())) + .collect::, _>>() + .map_err(|_| ())?, + }) + } + ) +); + #[cfg(test)] mod test { - use super::{Category, Code, Detail, Response, ResponseParser, Severity}; + use super::{Category, Code, Detail, Response, Severity}; #[test] fn test_severity_from_str() { @@ -300,7 +319,7 @@ mod test { Severity::TransientNegativeCompletion ); assert!("1".parse::().is_err()); - assert!("51".parse::().is_err()); + assert!("a51".parse::().is_err()); } #[test] @@ -356,12 +375,12 @@ mod test { detail: "1".parse::().unwrap(), } ); - assert!("2222".parse::().is_err()); + assert!("r2222".parse::().is_err()); assert!("aaa".parse::().is_err()); assert!("-32".parse::().is_err()); assert!("-333".parse::().is_err()); assert!("".parse::().is_err()); - assert!("292".parse::().is_err()); + assert!("9292".parse::().is_err()); } #[test] @@ -423,35 +442,6 @@ mod test { ); } - #[test] - fn test_response_parser() { - let mut parser = ResponseParser::default(); - - assert!(parser.read_line("250-me").unwrap()); - assert!(parser.read_line("250-8BITMIME").unwrap()); - assert!(parser.read_line("250-SIZE 42").unwrap()); - assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap()); - - let response = parser.response().unwrap(); - - assert_eq!( - response, - Response { - code: Code { - severity: Severity::PositiveCompletion, - category: Category::MailSystem, - detail: Detail(0), - }, - message: vec![ - "me".to_string(), - "8BITMIME".to_string(), - "SIZE 42".to_string(), - "AUTH PLAIN CRAM-MD5".to_string(), - ], - } - ); - } - #[test] fn test_response_is_positive() { assert!(