Remove uneeded code to simplify the client

This commit is contained in:
Alexis Mousset
2015-03-01 20:17:12 +01:00
parent 2a7f8531fd
commit fa8cd0fb3b
5 changed files with 40 additions and 389 deletions

View File

@@ -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<S = TcpStream> Client<S> {
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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
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<u16>) -> 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
///
/// * 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<u16>) -> 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
}
/// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// 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<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// 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())
}
}

View File

@@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<String>),
/// Authentication response
AuthenticationResponse(String),
/// Extended Hello command
ExtendedHello(String),
/// Hello command
Hello(String),
/// Mail command, takes optional options
Mail(String, Option<Vec<String>>),
/// Recipient command, takes optional options
Recipient(String, Option<Vec<String>>),
/// 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<String>),
/// 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:<test> option".to_string()
);
assert_eq!(
format!("{}", Command::Mail("test".to_string(),
Some(vec!["option".to_string(), "option2".to_string()]))),
"MAIL FROM:<test> 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());
}
}

View File

@@ -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)]

View File

@@ -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;

View File

@@ -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 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<TransactionState> {
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);
}
}