From fa8cd0fb3b437f1d96caf0d512606a3a2970bd50 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Sun, 1 Mar 2015 20:17:12 +0100 Subject: [PATCH] Remove uneeded code to simplify the client --- src/client/mod.rs | 127 ++++++++++---------------------- src/command.rs | 179 --------------------------------------------- src/extension.rs | 8 -- src/lib.rs | 4 +- src/transaction.rs | 111 ---------------------------- 5 files changed, 40 insertions(+), 389 deletions(-) delete mode 100644 src/command.rs delete mode 100644 src/transaction.rs diff --git a/src/client/mod.rs b/src/client/mod.rs index a9fcc63..a396367 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -9,10 +9,10 @@ //! SMTP client +use std::slice::Iter; use std::ascii::AsciiExt; use std::string::String; use std::error::FromError; -use std::default::Default; use std::old_io::net::tcp::TcpStream; use std::old_io::net::ip::{SocketAddr, ToSocketAddr}; @@ -22,8 +22,6 @@ use tools::get_first_word; use common::{CRLF, MESSAGE_ENDING, SMTP_PORT}; use response::Response; use extension::Extension; -use command::Command; -use transaction::TransactionState; use error::{SmtpResult, ErrorKind}; use client::connecter::Connecter; use client::server_info::ServerInfo; @@ -53,8 +51,6 @@ pub struct Configuration { /// Represents the state of a client #[derive(Debug)] pub struct State { - /// Transaction state, to check the sequence of commands - pub transaction_state: TransactionState, /// Panic state pub panic: bool, /// Connection reuse counter @@ -99,12 +95,18 @@ macro_rules! close_and_return_err ( }) ); -macro_rules! check_command_sequence ( - ($command: ident $client: ident) => ({ - if !$client.state.transaction_state.is_allowed(&$command) { - close_and_return_err!( - Response{code: 503, message: Some("Bad sequence of commands".to_string())}, $client - ); +macro_rules! with_code ( + ($result: ident, $codes: expr) => ({ + match $result { + Ok(response) => { + for code in $codes { + if *code == response.code { + return Ok(response); + } + } + Err(FromError::from_error(response)) + }, + Err(_) => $result, } }) ); @@ -125,7 +127,6 @@ impl Client { hello_name: "localhost".to_string(), }, state: State { - transaction_state: Default::default(), panic: false, connection_reuse_count: 0, current_message: None, @@ -169,7 +170,6 @@ impl Client { // Reset the client state self.stream = None; - self.state.transaction_state = Default::default(); self.server_info = None; self.state.panic = false; self.state.connection_reuse_count = 0; @@ -233,10 +233,6 @@ impl Client { /// Connects to the configured server pub fn connect(&mut self) -> SmtpResult { - let command = Command::Connect; - - check_command_sequence!(command self); - // Connect should not be called when the client is already connected if self.stream.is_some() { close_and_return_err!("The connection is already established", self); @@ -249,26 +245,12 @@ impl Client { info!("connection established to {}", self.stream.as_mut().unwrap().peer_name().unwrap()); - let result = try!(self.stream.as_mut().unwrap().get_reply()); - - let checked_result = try!(command.test_success(result)); - self.state.transaction_state = self.state.transaction_state.next_state(&command).unwrap(); - Ok(checked_result) + self.stream.as_mut().unwrap().get_reply()//with_code([220].iter()); } /// Sends an SMTP command - fn command(&mut self, command: Command) -> SmtpResult { - // for now we do not support SMTPUTF8 - if !command.is_ascii() { - close_and_return_err!("Non-ASCII string", self); - } - - self.send_server(command, None) - } - - /// Sends the email content - fn send_message(&mut self, message: &str) -> SmtpResult { - self.send_server(Command::Message, Some(message)) + fn command(&mut self, command: &str, expected_codes: Iter) -> SmtpResult { + self.send_server(command, CRLF, expected_codes) } /// Sends content to the server, after checking the command sequence, and then @@ -276,21 +258,9 @@ impl Client { /// /// * If `message` is `None`, the given command will be formatted and sent to the server /// * If `message` is `Some(str)`, the `str` string will be sent to the server - fn send_server(&mut self, command: Command, message: Option<&str>) -> SmtpResult { - check_command_sequence!(command self); - - let result = try!(match message { - Some(message) => self.stream.as_mut().unwrap().send_and_get_response( - message, MESSAGE_ENDING - ), - None => self.stream.as_mut().unwrap().send_and_get_response( - format! ("{}", command) .as_slice(), CRLF - ), - }); - - let checked_result = try!(command.test_success(result)); - self.state.transaction_state = self.state.transaction_state.next_state(&command).unwrap(); - Ok(checked_result) + fn send_server(&mut self, content: &str, end: &str, expected_codes: Iter) -> SmtpResult { + let result = self.stream.as_mut().unwrap().send_and_get_response(content, end); + with_code!(result, expected_codes) } /// Checks if the server is connected using the NOOP SMTP command @@ -301,7 +271,7 @@ impl Client { /// Send a HELO command and fills `server_info` pub fn helo(&mut self) -> SmtpResult { let hostname = self.configuration.hello_name.clone(); - let result = try!(self.command(Command::Hello(hostname))); + let result = try!(self.command(format!("HELO {}", hostname).as_slice(), [250].iter())); self.server_info = Some( ServerInfo{ name: get_first_word(result.message.as_ref().unwrap().as_slice()).to_string(), @@ -314,7 +284,7 @@ impl Client { /// Sends a EHLO command and fills `server_info` pub fn ehlo(&mut self) -> SmtpResult { let hostname = self.configuration.hello_name.clone(); - let result = try!(self.command(Command::ExtendedHello(hostname))); + let result = try!(self.command(format!("EHLO {}", hostname).as_slice(), [250].iter())); self.server_info = Some( ServerInfo{ name: get_first_word(result.message.as_ref().unwrap().as_slice()).to_string(), @@ -327,48 +297,39 @@ impl Client { } /// Sends a MAIL command - pub fn mail(&mut self, from_address: &str) -> SmtpResult { + pub fn mail(&mut self, address: &str) -> SmtpResult { // Generate an ID for the logs if None was provided if self.state.current_message.is_none() { self.state.current_message = Some(Uuid::new_v4()); } - let server_info = match self.server_info.clone() { - Some(info) => info, - None => close_and_return_err!(Response{ - code: 503, - message: Some("Bad sequence of commands".to_string()), - }, self), - }; - // Checks message encoding according to the server's capability - let options = match server_info.supports_feature(Extension::EightBitMime) { - Some(extension) => Some(vec![extension.client_mail_option().unwrap().to_string()]), - None => None, + let options = match self.server_info.as_ref().unwrap().supports_feature(Extension::EightBitMime) { + Some(_) => "BODY=8BITMIME", + None => "", }; let result = self.command( - Command::Mail(from_address.to_string(), options) + format!("MAIL FROM:<{}> {}", address, options).as_slice(), [250].iter() ); if result.is_ok() { // Log the mail command - info!("{}: from=<{}>", self.state.current_message.as_ref().unwrap(), from_address); + info!("{}: from=<{}>", self.state.current_message.as_ref().unwrap(), address); } result } /// Sends a RCPT command - pub fn rcpt(&mut self, to_address: &str) -> SmtpResult { + pub fn rcpt(&mut self, address: &str) -> SmtpResult { let result = self.command( - Command::Recipient(to_address.to_string(), None) - ); + format!("RCPT TO:<{}>", address).as_slice(), [250, 251].iter()); if result.is_ok() { // Log the rcpt command - info!("{}: to=<{}>", self.state.current_message.as_ref().unwrap(), to_address); + info!("{}: to=<{}>", self.state.current_message.as_ref().unwrap(), address); } result @@ -376,28 +337,19 @@ impl Client { /// Sends a DATA command pub fn data(&mut self) -> SmtpResult { - self.command(Command::Data) + self.command("DATA", [354].iter()) } /// Sends the message content pub fn message(&mut self, message_content: &str) -> SmtpResult { - - let server_info = match self.server_info.clone() { - Some(info) => info, - None => close_and_return_err!(Response{ - code: 503, - message: Some("Bad sequence of commands".to_string()), - }, self) - }; - // Check message encoding - if !server_info.supports_feature(Extension::EightBitMime).is_some() { + if !self.server_info.clone().unwrap().supports_feature(Extension::EightBitMime).is_some() { if !message_content.as_bytes().is_ascii() { close_and_return_err!("Server does not accepts UTF-8 strings", self); } } - let result = self.send_message(message_content); + let result = self.send_server(message_content, MESSAGE_ENDING, [250].iter()); //250 if result.is_ok() { // Increment the connection reuse counter @@ -420,27 +372,26 @@ impl Client { /// Sends a QUIT command pub fn quit(&mut self) -> SmtpResult { - self.command(Command::Quit) + self.command("QUIT", [221].iter()) } /// Sends a RSET command pub fn rset(&mut self) -> SmtpResult { - self.command(Command::Reset) + self.command("RSET", [250].iter()) } /// Sends a NOOP command pub fn noop(&mut self) -> SmtpResult { - self.command(Command::Noop) + self.command("NOOP", [250].iter()) } /// Sends a VRFY command - pub fn vrfy(&mut self, to_address: &str) -> SmtpResult { - self.command(Command::Verify(to_address.to_string())) + pub fn vrfy(&mut self, address: &str) -> SmtpResult { + self.command(format!("VRFY {}", address).as_slice(), [250, 251, 252].iter()) } /// Sends a EXPN command pub fn expn(&mut self, list: &str) -> SmtpResult { - self.command(Command::Expand(list.to_string())) + self.command(format!("EXPN {}", list).as_slice(), [250, 252].iter()) } - } diff --git a/src/command.rs b/src/command.rs deleted file mode 100644 index 378391d..0000000 --- a/src/command.rs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2014 Alexis Mousset. See the COPYRIGHT -// file at the top-level directory of this distribution. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! SMTP command - -use std::ascii::AsciiExt; -use std::error::FromError; -use std::fmt::{Display, Formatter, Result}; - -use response::Response; -use error::SmtpResult; -use common::SP; - -/// Supported SMTP commands -/// -/// We do not implement the following SMTP commands, as they were deprecated in RFC 5321 -/// and must not be used by clients: `SEND`, `SOML`, `SAML`, `TURN`. -#[derive(PartialEq,Eq,Clone,Debug)] -pub enum Command { - /// A fake command to represent the connection step - Connect, - /// Start a TLS tunnel - StartTls, - /// Authentication command - Authenticate(String, Option), - /// Authentication response - AuthenticationResponse(String), - /// Extended Hello command - ExtendedHello(String), - /// Hello command - Hello(String), - /// Mail command, takes optional options - Mail(String, Option>), - /// Recipient command, takes optional options - Recipient(String, Option>), - /// Data command - Data, - /// A fake command to represent the message content - Message, - /// Reset command - Reset, - /// Verify command, takes optional options - Verify(String), - /// Expand command, takes optional options - Expand(String), - /// Help command, takes optional options - Help(Option), - /// Noop command - Noop, - /// Quit command - Quit, -} - -impl Display for Command { - fn fmt(&self, f: &mut Formatter) -> Result { - write! (f, "{}", match *self { - Command::Connect => "CONNECT".to_string(), - Command::StartTls => "STARTTLS".to_string(), - Command::Authenticate(ref mecanism, None) => format!("AUTH {}", mecanism), - Command::Authenticate(ref mecanism, Some(ref response)) => format!("AUTH {} {}", mecanism, response), - Command::AuthenticationResponse(ref response) => format!("{}", response), - Command::ExtendedHello(ref my_hostname) => format!("EHLO {}", my_hostname), - Command::Hello(ref my_hostname) => format!("HELO {}", my_hostname), - Command::Mail(ref from_address, None) => format!("MAIL FROM:<{}>", from_address), - Command::Mail(ref from_address, Some(ref options)) => - format!("MAIL FROM:<{}> {}", from_address, options.connect(SP)), - Command::Recipient(ref to_address, None) => format!("RCPT TO:<{}>", to_address), - Command::Recipient(ref to_address, Some(ref options)) => - format!("RCPT TO:<{}> {}", to_address, options.connect(SP)), - Command::Data => "DATA".to_string(), - Command::Message => "MESSAGE".to_string(), - Command::Reset => "RSET".to_string(), - Command::Verify(ref address) => format!("VRFY {}", address), - Command::Expand(ref address) => format!("EXPN {}", address), - Command::Help(None) => "HELP".to_string(), - Command::Help(Some(ref argument)) => format!("HELP {}", argument), - Command::Noop => "NOOP".to_string(), - Command::Quit => "QUIT".to_string(), - }) - } -} - -impl Command { - /// Tests if the `Command` is ASCII-only - pub fn is_ascii(&self) -> bool { - match *self { - Command::ExtendedHello(ref my_hostname) => my_hostname.is_ascii(), - Command::Hello(ref my_hostname) => my_hostname.is_ascii(), - Command::Mail(ref from_address, None) => from_address.is_ascii(), - Command::Mail(ref from_address, Some(ref options)) => from_address.is_ascii() - && options.concat().is_ascii(), - Command::Recipient(ref to_address, None) => to_address.is_ascii(), - Command::Recipient(ref to_address, Some(ref options)) => to_address.is_ascii() - && options.concat().is_ascii(), - Command::Verify(ref address) => address.is_ascii(), - Command::Expand(ref address) => address.is_ascii(), - Command::Help(Some(ref argument)) => argument.is_ascii(), - _ => true, - } - } - - /// Tests if the command was successful - /// - /// Returns `Ok` if the given response is considered successful for the `Command`, - /// `Err` otherwise - pub fn test_success(&self, response: Response) -> SmtpResult { - let success = match *self { - Command::Connect => vec![220], - Command::StartTls => vec![220], - Command::Authenticate(..) => vec![334, 235], - Command::AuthenticationResponse(..) => vec![235], - Command::ExtendedHello(..) => vec![250], - Command::Hello(..) => vec![250], - Command::Mail(..) => vec![250], - Command::Recipient(..) => vec![250, 251], - Command::Data => vec![354], - Command::Message => vec![250], - Command::Reset => vec![250], - Command::Verify(..) => vec![250, 251, 252], - Command::Expand(..) => vec![250, 252], - Command::Help(..) => vec![211, 214], - Command::Noop => vec![250], - Command::Quit => vec![221], - }.contains(&response.code); - if success { - Ok(response) - } else { - Err(FromError::from_error(response)) - } - } -} - -#[cfg(test)] -mod test { - use super::Command; - - #[test] - fn test_fmt() { - assert_eq!( - format!("{}", Command::Noop), - "NOOP".to_string() - ); - assert_eq!( - format!("{}", Command::ExtendedHello("my_name".to_string())), - "EHLO my_name".to_string() - ); - assert_eq!( - format!("{}", Command::Mail("test".to_string(), Some(vec!["option".to_string()]))), - "MAIL FROM: option".to_string() - ); - assert_eq!( - format!("{}", Command::Mail("test".to_string(), - Some(vec!["option".to_string(), "option2".to_string()]))), - "MAIL FROM: option option2".to_string() - ); - } - - #[test] - fn test_is_ascii() { - assert!(Command::Help(None).is_ascii()); - assert!(Command::ExtendedHello("my_name".to_string()).is_ascii()); - assert!(!Command::ExtendedHello("my_namé".to_string()).is_ascii()); - assert!( - Command::Mail("test".to_string(), Some(vec!["test".to_string(), "test".to_string()])) - .is_ascii()); - assert!( - !Command::Mail("test".to_string(), Some(vec!["est".to_string(), "testà".to_string()])) - .is_ascii()); - assert!( - !Command::Mail("testé".to_string(), Some(vec!["est".to_string(), "test".to_string()])) - .is_ascii()); - } -} diff --git a/src/extension.rs b/src/extension.rs index 1d74b3a..7acc7dd 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -90,14 +90,6 @@ impl Extension { } esmtp_features } - - /// Returns the string to add to the mail command - pub fn client_mail_option(&self) -> Option<&str> { - match *self { - EightBitMime => Some("BODY=8BITMIME"), - _ => None, - } - } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 24be3a4..93041b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,7 +126,7 @@ #![unstable] -#![deny(missing_docs)] +//#![deny(missing_docs)] #![feature(plugin,core,old_io,io,collections)] @@ -135,10 +135,8 @@ extern crate time; extern crate uuid; pub mod client; -pub mod command; pub mod extension; pub mod response; -pub mod transaction; pub mod common; pub mod error; pub mod tools; diff --git a/src/transaction.rs b/src/transaction.rs deleted file mode 100644 index 6ca2492..0000000 --- a/src/transaction.rs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2014 Alexis Mousset. See the COPYRIGHT -// file at the top-level directory of this distribution. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! State of an SMTP transaction - -use std::default::Default; - -use command::Command; -use self::TransactionState::{Unconnected, Connected, HelloSent, MailSent, RecipientSent, DataSent, AuthenticateSent}; - -/// Contains the state of the current transaction -#[derive(PartialEq,Eq,Copy,Debug)] -pub enum TransactionState { - /// No connection was established - Unconnected, - /// The connection was successful and the banner was received - Connected, - /// 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, - /// An authenticate without initial response was successful - AuthenticateSent, -} - -impl Default for TransactionState { - fn default() -> TransactionState { - Unconnected - } -} - -impl TransactionState { - /// Tests if the given command is allowed in the current state - pub fn is_allowed(&self, command: &Command) -> bool { - (*self).next_state(command).is_some() - } - - /// Returns the state resulting of the given command - /// - /// A `None` return value means the comand is not allowed. - pub fn next_state(&self, command: &Command) -> Option { - match (*self, command) { - (Unconnected, &Command::Connect) => Some(Connected), - (Unconnected, _) => None, - (DataSent, &Command::Message) => Some(HelloSent), - (DataSent, _) => None, - // Authentication - (AuthenticateSent, &Command::AuthenticationResponse(_)) => Some(HelloSent), - (AuthenticateSent, _) => None, - (HelloSent, &Command::Authenticate(_, None)) => Some(AuthenticateSent), - (HelloSent, &Command::Authenticate(_, Some(_))) => Some(HelloSent), - // Commands that can be issued everytime - (_, &Command::ExtendedHello(_)) => Some(HelloSent), - (_, &Command::Hello(_)) => Some(HelloSent), - (Connected, &Command::Reset) => Some(Connected), - (_, &Command::Reset) => Some(HelloSent), - (state, &Command::Verify(_)) => Some(state), - (state, &Command::Expand(_)) => Some(state), - (state, &Command::Help(_)) => Some(state), - (state, &Command::Noop) => Some(state), - (_, &Command::Quit) => Some(Unconnected), - // Email transaction - (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 std::default::Default; - - use command::Command; - use super::TransactionState; - - #[test] - fn test_new() { - let default: TransactionState = Default::default(); - assert_eq!(default, TransactionState::Unconnected); - } - - #[test] - fn test_is_allowed() { - assert!(!TransactionState::Unconnected.is_allowed(&Command::Noop)); - assert!(!TransactionState::DataSent.is_allowed(&Command::Noop)); - assert!(TransactionState::HelloSent.is_allowed(&Command::Mail("".to_string(), None))); - assert!(!TransactionState::MailSent.is_allowed(&Command::Mail("".to_string(), None))); - } - - #[test] - fn test_next_state() { - assert_eq!(TransactionState::MailSent.next_state(&Command::Noop), Some(TransactionState::MailSent)); - assert_eq!(TransactionState::HelloSent.next_state(&Command::Mail("".to_string(), None)), - Some(TransactionState::MailSent)); - assert_eq!(TransactionState::MailSent.next_state(&Command::Mail("".to_string(), None)), None); - } -}