diff --git a/src/client.rs b/src/client.rs index e15ba00..8ae8c68 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,11 +19,11 @@ use std::io::net::ip::Port; use std::io::net::tcp::TcpStream; use common::{resolve_host, get_first_word, unquote_email_address}; -use smtp::smtp_response::SmtpResponse; -use smtp::esmtp_parameter; -use smtp::esmtp_parameter::EsmtpParameter; -use smtp::smtp_command; -use smtp::smtp_command::SmtpCommand; +use smtp::response::SmtpResponse; +use smtp::extension; +use smtp::extension::SmtpExtension; +use smtp::command; +use smtp::command::SmtpCommand; use smtp::{SMTP_PORT, CRLF}; /// Information about an SMTP server @@ -32,7 +32,7 @@ struct SmtpServerInfo { /// Server name name: T, /// ESMTP features supported by the server - esmtp_features: Option> + esmtp_features: Option> } impl Show for SmtpServerInfo{ @@ -53,12 +53,12 @@ impl SmtpServerInfo { /// Parses supported ESMTP features /// /// TODO: Improve parsing - fn parse_esmtp_response(message: T) -> Option> { + fn parse_esmtp_response(message: T) -> Option> { let mut esmtp_features = Vec::new(); for line in message.as_slice().split_str(CRLF) { match from_str::>(line) { Some(SmtpResponse{code: 250, message: message}) => { - match from_str::(message.unwrap().into_owned()) { + match from_str::(message.unwrap().into_owned()) { Some(keyword) => esmtp_features.push(keyword), None => () } @@ -73,11 +73,11 @@ impl SmtpServerInfo { } /// Checks if the server supports an ESMTP feature - fn supports_feature(&self, keyword: EsmtpParameter) -> Result { + fn supports_feature(&self, keyword: SmtpExtension) -> Result { match self.esmtp_features.clone() { Some(esmtp_features) => { for feature in esmtp_features.iter() { - if keyword.same_keyword_as(*feature) { + if keyword.same_extension_as(*feature) { return Ok(*feature); } } @@ -224,7 +224,7 @@ impl SmtpClient { // Checks message encoding according to the server's capability // TODO : Add an encoding check. - if ! self.server_info.clone().unwrap().supports_feature(esmtp_parameter::EightBitMime).is_ok() { + if ! self.server_info.clone().unwrap().supports_feature(extension::EightBitMime).is_ok() { if ! message.clone().into_owned().is_ascii() { self.smtp_fail("Server does not accepts UTF-8 strings"); } @@ -327,7 +327,7 @@ impl SmtpClient { pub fn helo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { check_state_in!(vec!(Connected)); - match self.send_command(smtp_command::Hello(my_hostname.clone())).with_code(vec!(250)) { + match self.send_command(command::Hello(my_hostname.clone())).with_code(vec!(250)) { Ok(response) => { self.server_info = Some( SmtpServerInfo{ @@ -346,7 +346,7 @@ impl SmtpClient { pub fn ehlo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { check_state_not_in!(vec!(Unconnected)); - match self.send_command(smtp_command::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) { + match self.send_command(command::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) { Ok(response) => { self.server_info = Some( SmtpServerInfo{ @@ -365,7 +365,7 @@ impl SmtpClient { pub fn mail(&mut self, from_address: StrBuf, options: Option>) -> Result, SmtpResponse> { check_state_in!(vec!(HeloSent)); - match self.send_command(smtp_command::Mail(StrBuf::from_str(unquote_email_address(from_address.to_owned())), options)).with_code(vec!(250)) { + match self.send_command(command::Mail(StrBuf::from_str(unquote_email_address(from_address.to_owned())), options)).with_code(vec!(250)) { Ok(response) => { self.state = MailSent; Ok(response) @@ -380,7 +380,7 @@ impl SmtpClient { pub fn rcpt(&mut self, to_address: StrBuf, options: Option>) -> Result, SmtpResponse> { check_state_in!(vec!(MailSent, RcptSent)); - match self.send_command(smtp_command::Recipient(StrBuf::from_str(unquote_email_address(to_address.to_owned())), options)).with_code(vec!(250)) { + match self.send_command(command::Recipient(StrBuf::from_str(unquote_email_address(to_address.to_owned())), options)).with_code(vec!(250)) { Ok(response) => { self.state = RcptSent; Ok(response) @@ -395,7 +395,7 @@ impl SmtpClient { pub fn data(&mut self) -> Result, SmtpResponse> { check_state_in!(vec!(RcptSent)); - match self.send_command(smtp_command::Data).with_code(vec!(354)) { + match self.send_command(command::Data).with_code(vec!(354)) { Ok(response) => { self.state = DataSent; Ok(response) @@ -424,7 +424,7 @@ impl SmtpClient { /// Sends a QUIT command pub fn quit(&mut self) -> Result, SmtpResponse> { check_state_not_in!(vec!(Unconnected)); - match self.send_command(smtp_command::Quit).with_code(vec!(221)) { + match self.send_command(command::Quit).with_code(vec!(221)) { Ok(response) => { Ok(response) }, @@ -437,7 +437,7 @@ impl SmtpClient { /// Sends a RSET command pub fn rset(&mut self) -> Result, SmtpResponse> { check_state_not_in!(vec!(Unconnected)); - match self.send_command(smtp_command::Reset).with_code(vec!(250)) { + match self.send_command(command::Reset).with_code(vec!(250)) { Ok(response) => { if vec!(MailSent, RcptSent, DataSent).contains(&self.state) { self.state = HeloSent; @@ -453,13 +453,13 @@ impl SmtpClient { /// Sends a NOOP commands pub fn noop(&mut self) -> Result, SmtpResponse> { check_state_not_in!(vec!(Unconnected)); - self.send_command(smtp_command::Noop).with_code(vec!(250)) + self.send_command(command::Noop).with_code(vec!(250)) } /// Sends a VRFY command pub fn vrfy(&mut self, to_address: StrBuf) -> Result, SmtpResponse> { check_state_not_in!(vec!(Unconnected)); - self.send_command(smtp_command::Verify(to_address)).with_code(vec!(250)) + self.send_command(command::Verify(to_address)).with_code(vec!(250)) } } @@ -498,17 +498,17 @@ impl Writer for SmtpClient { #[cfg(test)] mod test { use super::SmtpServerInfo; - use smtp::esmtp_parameter; + use smtp::extension; #[test] fn test_smtp_server_info_fmt() { assert_eq!(format!("{}", SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(esmtp_parameter::EightBitMime)) + esmtp_features: Some(vec!(extension::EightBitMime)) }), "name with [8BITMIME]".to_owned()); assert_eq!(format!("{}", SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(esmtp_parameter::EightBitMime, esmtp_parameter::Size(42))) + esmtp_features: Some(vec!(extension::EightBitMime, extension::Size(42))) }), "name with [8BITMIME, SIZE=42]".to_owned()); assert_eq!(format!("{}", SmtpServerInfo{ name: "name", @@ -519,13 +519,13 @@ mod test { #[test] fn test_smtp_server_info_parse_esmtp_response() { assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 SIZE 42"), - Some(vec!(esmtp_parameter::EightBitMime, esmtp_parameter::Size(42)))); + Some(vec!(extension::EightBitMime, extension::Size(42)))); assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 UNKNON 42"), - Some(vec!(esmtp_parameter::EightBitMime))); + Some(vec!(extension::EightBitMime))); assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-9BITMIME\r\n250 SIZE a"), None); assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-SIZE 42\r\n250 SIZE 43"), - Some(vec!(esmtp_parameter::Size(42), esmtp_parameter::Size(43)))); + Some(vec!(extension::Size(42), extension::Size(43)))); assert_eq!(SmtpServerInfo::parse_esmtp_response(""), None); } @@ -534,19 +534,19 @@ mod test { fn test_smtp_server_info_supports_feature() { assert_eq!(SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(esmtp_parameter::EightBitMime)) - }.supports_feature(esmtp_parameter::EightBitMime), Ok(esmtp_parameter::EightBitMime)); + esmtp_features: Some(vec!(extension::EightBitMime)) + }.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime)); assert_eq!(SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(esmtp_parameter::Size(42), esmtp_parameter::EightBitMime)) - }.supports_feature(esmtp_parameter::EightBitMime), Ok(esmtp_parameter::EightBitMime)); + esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime)) + }.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime)); assert_eq!(SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(esmtp_parameter::Size(42), esmtp_parameter::EightBitMime)) - }.supports_feature(esmtp_parameter::Size(0)), Ok(esmtp_parameter::Size(42))); + esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime)) + }.supports_feature(extension::Size(0)), Ok(extension::Size(42))); assert!(SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(esmtp_parameter::EightBitMime)) - }.supports_feature(esmtp_parameter::Size(42)).is_err()); + esmtp_features: Some(vec!(extension::EightBitMime)) + }.supports_feature(extension::Size(42)).is_err()); } } diff --git a/src/smtp.rs b/src/smtp.rs index 3d28b7c..ec361bd 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -23,7 +23,7 @@ pub static SP: &'static str = " "; pub static CRLF: &'static str = "\r\n"; /// A module -pub mod smtp_command { +pub mod command { use std::fmt::{Show, Formatter, Result}; /// Supported SMTP commands @@ -55,7 +55,6 @@ pub mod smtp_command { Noop, /// Quit command Quit, - } impl Show for SmtpCommand { @@ -90,13 +89,13 @@ pub mod smtp_command { } /// This is a module -pub mod esmtp_parameter { +pub mod extension { use std::from_str::FromStr; use std::fmt::{Show, Formatter, Result}; /// Supported ESMTP keywords #[deriving(Eq,Clone)] - pub enum EsmtpParameter { + pub enum SmtpExtension { /// 8BITMIME keyword /// /// RFC 6152 : https://tools.ietf.org/html/rfc6152 @@ -107,7 +106,7 @@ pub mod esmtp_parameter { Size(uint) } - impl Show for EsmtpParameter { + impl Show for SmtpExtension { fn fmt(&self, f: &mut Formatter) -> Result { f.buf.write( match self { @@ -118,8 +117,8 @@ pub mod esmtp_parameter { } } - impl FromStr for EsmtpParameter { - fn from_str(s: &str) -> Option { + impl FromStr for SmtpExtension { + fn from_str(s: &str) -> Option { let splitted : Vec<&str> = s.splitn(' ', 1).collect(); match splitted.len() { 1 => match *splitted.get(0) { @@ -135,9 +134,9 @@ pub mod esmtp_parameter { } } - impl EsmtpParameter { + impl SmtpExtension { /// Checks if the ESMTP keyword is the same - pub fn same_keyword_as(&self, other: EsmtpParameter) -> bool { + pub fn same_extension_as(&self, other: SmtpExtension) -> bool { if *self == other { return true; } @@ -149,7 +148,7 @@ pub mod esmtp_parameter { } } /// This is a module -pub mod smtp_response { +pub mod response { use std::from_str::FromStr; use std::fmt::{Show, Formatter, Result}; use common::remove_trailing_crlf; @@ -224,55 +223,142 @@ pub mod smtp_response { } } +/// a module +pub mod transaction_state { + use std::fmt; + use std::fmt::{Show, Formatter}; + use super::command; + use super::command::SmtpCommand; + + /// Contains the state of the current transaction + #[deriving(Eq,Clone)] + pub enum TransactionState { + /// The connection was successful and the banner was received + OutOfTransaction, + /// An HELO or EHLO was successful + HelloSent, + /// A MAIL command was successful send + MailSent, + /// At least one RCPT command was sucessful + RecipientSent, + /// A DATA command was successful + DataSent + } + + impl Show for TransactionState { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.buf.write( + match *self { + OutOfTransaction => "OutOfTransaction", + HelloSent => "HelloSent", + MailSent => "MailSent", + RecipientSent => "RecipientSent", + DataSent => "DataSent" + }.as_bytes() + ) + } + } + + impl TransactionState { + /// bla bla + pub fn is_command_possible(&self, command: SmtpCommand) -> bool { + match (*self, command) { + // Only the message content can be sent in this state + (DataSent, _) => false, + // Commands that can be issued everytime + (_, command::ExtendedHello(_)) => true, + (_, command::Hello(_)) => true, + (_, command::Reset) => true, + (_, command::Verify(_)) => true, + (_, command::Expand(_)) => true, + (_, command::Help(_)) => true, + (_, command::Noop) => true, + (_, command::Quit) => true, + // Commands that require a particular state + (HelloSent, command::Mail(_, _)) => true, + (MailSent, command::Recipient(_, _)) => true, + (RecipientSent, command::Recipient(_, _)) => true, + (RecipientSent, command::Data) => true, + // Everything else + (_, _) => false + } + } + + /// a method + pub fn next_state(&mut self, command: SmtpCommand) -> Option { + match (*self, command) { + (DataSent, _) => None, + // Commands that can be issued everytime + (_, command::ExtendedHello(_)) => Some(HelloSent), + (_, command::Hello(_)) => Some(HelloSent), + (_, command::Reset) => Some(OutOfTransaction), + (state, command::Verify(_)) => Some(state), + (state, command::Expand(_)) => Some(state), + (state, command::Help(_)) => Some(state), + (state, command::Noop) => Some(state), + (_, command::Quit) => Some(OutOfTransaction), + // Commands that require a particular state + (HelloSent, command::Mail(_, _)) => Some(MailSent), + (MailSent, command::Recipient(_, _)) => Some(RecipientSent), + (RecipientSent, command::Recipient(_, _)) => Some(RecipientSent), + (RecipientSent, command::Data) => Some(DataSent), + // Everything else + (_, _) => None + } + } + } +} + #[cfg(test)] mod test { - use super::smtp_response::SmtpResponse; - use super::esmtp_parameter; - use super::esmtp_parameter::EsmtpParameter; - use super::smtp_command; - use super::smtp_command::SmtpCommand; + use super::response::SmtpResponse; + use super::extension; + use super::extension::SmtpExtension; + use super::command; + use super::command::SmtpCommand; + use super::transaction_state; #[test] fn test_command_fmt() { - let noop: SmtpCommand = smtp_command::Noop; + let noop: SmtpCommand = command::Noop; assert_eq!(format!("{}", noop), "NOOP".to_owned()); - assert_eq!(format!("{}", smtp_command::ExtendedHello("me")), "EHLO me".to_owned()); + assert_eq!(format!("{}", command::ExtendedHello("me")), "EHLO me".to_owned()); assert_eq!(format!("{}", - smtp_command::Mail("test", Some(vec!("option")))), "MAIL FROM: option".to_owned() + command::Mail("test", Some(vec!("option")))), "MAIL FROM: option".to_owned() ); } #[test] - fn test_esmtp_parameter_same_keyword_as() { - assert_eq!(esmtp_parameter::EightBitMime.same_keyword_as(esmtp_parameter::EightBitMime), true); - assert_eq!(esmtp_parameter::Size(42).same_keyword_as(esmtp_parameter::Size(42)), true); - assert_eq!(esmtp_parameter::Size(42).same_keyword_as(esmtp_parameter::Size(43)), true); - assert_eq!(esmtp_parameter::Size(42).same_keyword_as(esmtp_parameter::EightBitMime), false); + fn test_extension_same_extension_as() { + assert_eq!(extension::EightBitMime.same_extension_as(extension::EightBitMime), true); + assert_eq!(extension::Size(42).same_extension_as(extension::Size(42)), true); + assert_eq!(extension::Size(42).same_extension_as(extension::Size(43)), true); + assert_eq!(extension::Size(42).same_extension_as(extension::EightBitMime), false); } #[test] - fn test_esmtp_parameter_fmt() { - assert_eq!(format!("{}", esmtp_parameter::EightBitMime), "8BITMIME".to_owned()); - assert_eq!(format!("{}", esmtp_parameter::Size(42)), "SIZE=42".to_owned()); + fn test_extension_fmt() { + assert_eq!(format!("{}", extension::EightBitMime), "8BITMIME".to_owned()); + assert_eq!(format!("{}", extension::Size(42)), "SIZE=42".to_owned()); } #[test] - fn test_esmtp_parameter_from_str() { - assert_eq!(from_str::("8BITMIME"), Some(esmtp_parameter::EightBitMime)); - assert_eq!(from_str::("SIZE 42"), Some(esmtp_parameter::Size(42))); - assert_eq!(from_str::("SIZ 42"), None); - assert_eq!(from_str::("SIZE 4a2"), None); + fn test_extension_from_str() { + assert_eq!(from_str::("8BITMIME"), Some(extension::EightBitMime)); + assert_eq!(from_str::("SIZE 42"), Some(extension::Size(42))); + assert_eq!(from_str::("SIZ 42"), None); + assert_eq!(from_str::("SIZE 4a2"), None); // TODO: accept trailing spaces ? - assert_eq!(from_str::("SIZE 42 "), None); + assert_eq!(from_str::("SIZE 42 "), None); } #[test] - fn test_smtp_response_fmt() { + fn test_response_fmt() { assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message")}), "200 message".to_owned()); } #[test] - fn test_smtp_response_from_str() { + fn test_response_from_str() { assert_eq!(from_str::>("200 response message"), Some(SmtpResponse{ code: 200, @@ -312,7 +398,7 @@ mod test { } #[test] - fn test_smtp_response_with_code() { + fn test_response_with_code() { assert_eq!(SmtpResponse{code: 200, message: Some("message")}.with_code(vec!(200)), Ok(SmtpResponse{code: 200, message: Some("message")})); assert_eq!(SmtpResponse{code: 400, message: Some("message")}.with_code(vec!(200)), @@ -320,4 +406,21 @@ mod test { assert_eq!(SmtpResponse{code: 200, message: Some("message")}.with_code(vec!(200, 300)), Ok(SmtpResponse{code: 200, message: Some("message")})); } + + #[test] + fn test_transaction_state_is_command_possible() { + let noop: SmtpCommand = command::Noop; + assert!(transaction_state::OutOfTransaction.is_command_possible(noop.clone())); + assert!(! transaction_state::DataSent.is_command_possible(noop)); + assert!(transaction_state::HelloSent.is_command_possible(command::Mail("", None))); + assert!(! transaction_state::MailSent.is_command_possible(command::Mail("", None))); + } + + #[test] + fn test_transaction_state_next_state() { + let noop: SmtpCommand = command::Noop; + assert_eq!(transaction_state::MailSent.next_state(noop), Some(transaction_state::MailSent)); + assert_eq!(transaction_state::HelloSent.next_state(command::Mail("", None)), Some(transaction_state::MailSent)); + assert_eq!(transaction_state::MailSent.next_state(command::Mail("", None)), None); + } }