diff --git a/src/client/mod.rs b/src/client/mod.rs index 9cf251b..b9013c5 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -15,7 +15,7 @@ use std::io::{BufRead, Read, Write}; use bufstream::BufStream; -use response::{Response, Severity, Category}; +use response::ResponseParser; use error::SmtpResult; use client::net::{Connector, SmtpStream}; use client::authentication::{plain, cram_md5}; @@ -43,6 +43,12 @@ fn escape_crlf(string: &str) -> String { string.replace(CRLF, "") } +/// Returns the string removing all the CRLF +#[inline] +fn remove_crlf(string: &str) -> String { + string.replace(CRLF, "") +} + /// Structure that implements the SMTP client pub struct Client { /// TCP stream between client and server @@ -194,34 +200,18 @@ impl Client { /// Gets the SMTP response fn get_reply(&mut self) -> SmtpResult { + + let mut parser = ResponseParser::new(); + let mut line = String::new(); try!(self.stream.as_mut().unwrap().read_line(&mut line)); - // If the string is too short to be a response code - if line.len() < 3 { - return Err(From::from("Could not parse reply code, line too short")); + while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) { + line.clear(); + try!(self.stream.as_mut().unwrap().read_line(&mut line)); } - let (severity, category, detail) = match (line[0..1].parse::(), line[1..2].parse::(), line[2..3].parse::()) { - (Ok(severity), Ok(category), Ok(detail)) => (severity, category, detail), - _ => return Err(From::from("Could not parse reply code")), - }; - - let mut message = Vec::new(); - - // 3 chars for code + space + CRLF - while line.len() > 6 { - let end = line.len() - 2; - message.push(line[4..end].to_string()); - if line.as_bytes()[3] == '-' as u8 { - line.clear(); - try!(self.stream.as_mut().unwrap().read_line(&mut line)); - } else { - line.clear(); - } - } - - let response = Response::new(severity, category, detail, message); + let response = try!(parser.response()); match response.is_positive() { true => Ok(response), @@ -232,7 +222,7 @@ impl Client { #[cfg(test)] mod test { - use super::{escape_dot, escape_crlf}; + use super::{escape_dot, remove_crlf, escape_crlf}; #[test] fn test_escape_dot() { @@ -242,6 +232,16 @@ mod test { assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest"); } + #[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/src/extension.rs b/src/extension.rs index 4710194..8037de0 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -81,7 +81,7 @@ impl Extension { #[cfg(test)] mod test { use super::Extension; - use response::{Severity, Category, Response}; + use response::{Severity, Category, Response, Code}; #[test] fn test_from_str() { @@ -95,15 +95,19 @@ mod test { #[test] fn test_parse_esmtp_response() { assert_eq!(Extension::parse_esmtp_response(&Response::new( - "2".parse::().unwrap(), - "2".parse::().unwrap(), - 1, + Code::new( + "2".parse::().unwrap(), + "2".parse::().unwrap(), + 1, + ), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] )), vec![Extension::EightBitMime]); assert_eq!(Extension::parse_esmtp_response(&Response::new( - "4".parse::().unwrap(), - "3".parse::().unwrap(), - 3, + Code::new( + "4".parse::().unwrap(), + "3".parse::().unwrap(), + 3, + ), vec!["me".to_string(), "8BITMIME".to_string(), "AUTH PLAIN CRAM-MD5".to_string()] )), vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]); } diff --git a/src/response.rs b/src/response.rs index 592d41a..7519f32 100644 --- a/src/response.rs +++ b/src/response.rs @@ -15,6 +15,7 @@ use std::result::Result as RResult; use self::Severity::*; use self::Category::*; +use error::{SmtpResult, SmtpError}; /// First digit indicates severity #[derive(PartialEq,Eq,Copy,Clone,Debug)] @@ -102,17 +103,113 @@ impl Display for Category { } } -/// Contains an SMTP reply, with separed code and message -/// -/// The text message is optional, only the code is mandatory +/// Represents a 3 digit SMTP response code #[derive(PartialEq,Eq,Clone,Debug)] -pub struct Response { +pub struct Code { /// First digit of the response code severity: Severity, /// Second digit of the response code category: Category, /// Third digit detail: u8, +} + +impl FromStr for Code { + type Err = SmtpError; + + #[inline] + fn from_str(s: &str) -> RResult { + 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}), + _ => return Err(From::from("Could not parse reply code")), + } + } else { + Err(From::from("Could not parse reply code")) + } + } +} + +impl Code { + /// Creates a new `Code` structure + pub fn new(severity: Severity, category: Category, detail: u8) -> Code { + Code { + severity: severity, + category: category, + detail: detail, + } + } + + /// Returns the reply code + pub fn code(&self) -> String { + format!("{}{}{}", self.severity, self.category, self.detail) + } +} + +/// Parses an SMTP response +#[derive(PartialEq,Eq,Clone,Debug)] +pub struct ResponseParser { + /// Response code + code: Option, + /// Server response string (optional) + /// Handle multiline responses + message: Vec +} + +impl ResponseParser { + /// Creates a new parser + pub fn new() -> ResponseParser { + ResponseParser { + code: None, + message: vec![], + } + } + + /// Parses a line and return a `bool` indicating if there are more lines to come + pub fn read_line(&mut self, line: &str) -> RResult { + + if line.len() < 3 { + return Err(From::from("Could not parse reply code, line too short")); + } + + if self.code.is_none() { + self.code = Some(try!(line[0..3].parse::())); + } else { + if self.code.as_ref().unwrap().code() != line[0..3] { + println!("pouet"); + return Err(From::from("Could not parse reply code")); + } + } + + if line.len() > 4 { + self.message.push(line[4..].to_string()); + if line.as_bytes()[3] == '-' as u8 { + Ok(true) + } else { + Ok(false) + } + } else { + Ok(false) + } + } + + /// Builds a response from a `ResponseParser` + pub fn response(self) -> SmtpResult { + if self.code.is_some() { + Ok(Response::new(self.code.unwrap(), self.message)) + } else { + Err(From::from("Could not parse reply code")) + } + } +} + +/// Contains an SMTP reply, with separed code and message +/// +/// The text message is optional, only the code is mandatory +#[derive(PartialEq,Eq,Clone,Debug)] +pub struct Response { + /// Response code + code: Code, /// Server response string (optional) /// Handle multiline responses message: Vec @@ -120,18 +217,16 @@ pub struct Response { impl Response { /// Creates a new `Response` - pub fn new(severity: Severity, category: Category, detail: u8, message: Vec) -> Response { + pub fn new(code: Code, message: Vec) -> Response { Response { - severity: severity, - category: category, - detail: detail, - message: message + code: code, + message: message, } } /// Tells if the response is positive pub fn is_positive(&self) -> bool { - match self.severity { + match self.code.severity { PositiveCompletion => true, PositiveIntermediate => true, _ => false, @@ -143,24 +238,30 @@ impl Response { self.message.clone() } + /// Gets the first line beginning with the given string + /// TODO testing + pub fn get_line_beginning_with(&self, start: &str) -> Option { + self.message.iter().find(|&x| (*x).starts_with(start)).map(|s| s.to_string()) + } + /// Returns the severity (i.e. 1st digit) pub fn severity(&self) -> Severity { - self.severity + self.code.severity } /// Returns the category (i.e. 2nd digit) pub fn category(&self) -> Category { - self.category + self.code.category } /// Returns the detail (i.e. 3rd digit) pub fn detail(&self) -> u8 { - self.detail + self.code.detail } /// Returns the reply code fn code(&self) -> String { - format!("{}{}{}", self.severity, self.category, self.detail) + self.code.code() } /// Tests code equality @@ -182,7 +283,7 @@ impl Response { #[cfg(test)] mod test { - use super::{Severity, Category, Response}; + use super::{Severity, Category, Response, ResponseParser, Code}; #[test] fn test_severity_from_str() { @@ -208,44 +309,117 @@ mod test { assert_eq!(format!("{}", Category::Unspecified4), "4"); } + #[test] + fn test_code_new() { + assert_eq!( + Code::new(Severity::TransientNegativeCompletion, Category::Connections, 0), + Code { + severity: Severity::TransientNegativeCompletion, + category: Category::Connections, + detail: 0, + } + ); + } + + #[test] + fn test_code_from_str() { + assert_eq!( + "421".parse::().unwrap(), + Code { + severity: Severity::TransientNegativeCompletion, + category: Category::Connections, + detail: 1, + } + ); + } + + #[test] + fn test_code_code() { + let code = Code { + severity: Severity::TransientNegativeCompletion, + category: Category::Connections, + detail: 1, + }; + + assert_eq!(code.code(), "421"); + } + #[test] fn test_response_new() { assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail: 1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ), Response { - severity: Severity::PositiveCompletion, - category: Category::Unspecified4, - detail: 1, + code: Code { + severity: Severity::PositiveCompletion, + category: Category::Unspecified4, + detail: 1, + }, message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()], }); assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec![] ), Response { - severity: Severity::PositiveCompletion, - category: Category::Unspecified4, - detail: 1, + code: Code { + severity: Severity::PositiveCompletion, + category: Category::Unspecified4, + detail: 1, + }, message: vec![], }); } + #[test] + fn test_response_parser() { + let mut parser = ResponseParser::new(); + + 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: 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!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).is_positive()); assert!(! Response::new( - "4".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "5".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).is_positive()); } @@ -253,16 +427,20 @@ mod test { #[test] fn test_response_message() { assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]); let empty_message: Vec = vec![]; assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec![] ).message(), empty_message); } @@ -270,15 +448,19 @@ mod test { #[test] fn test_response_severity() { assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).severity(), Severity::PositiveCompletion); assert_eq!(Response::new( - "5".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "5".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail: 1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).severity(), Severity::PermanentNegativeCompletion); } @@ -286,9 +468,11 @@ mod test { #[test] fn test_response_category() { assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).category(), Category::Unspecified4); } @@ -296,9 +480,11 @@ mod test { #[test] fn test_response_detail() { assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).detail(), 1); } @@ -306,9 +492,11 @@ mod test { #[test] fn test_response_code() { assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).code(), "241"); } @@ -316,15 +504,19 @@ mod test { #[test] fn test_response_has_code() { assert!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).has_code(241)); assert!(! Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).has_code(251)); } @@ -332,39 +524,51 @@ mod test { #[test] fn test_response_first_word() { assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).first_word(), Some("me".to_string())); assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()] ).first_word(), Some("me".to_string())); assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec![] ).first_word(), None); assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec![" ".to_string()] ).first_word(), None); assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec![" ".to_string()] ).first_word(), None); assert_eq!(Response::new( - "2".parse::().unwrap(), - "4".parse::().unwrap(), - 1, + Code { + severity: "2".parse::().unwrap(), + category: "4".parse::().unwrap(), + detail:1, + }, vec!["".to_string()] ).first_word(), None); }