From ef102e73ba94a2e185ad12bc476255a61b0508d0 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Sun, 16 Feb 2014 14:14:50 +0100 Subject: [PATCH] Reorganize the SMTP client --- src/examples/client.rs | 2 +- src/smtp/client.rs | 154 +++++++++++++++++++++++++++++------------ src/smtp/commands.rs | 58 +++++++++++++--- src/smtp/common.rs | 3 +- src/smtp/lib.rs | 10 ++- 5 files changed, 170 insertions(+), 57 deletions(-) diff --git a/src/examples/client.rs b/src/examples/client.rs index cedccb9..c1de87c 100644 --- a/src/examples/client.rs +++ b/src/examples/client.rs @@ -1,6 +1,6 @@ #[crate_id = "client"]; -extern mod smtp; +extern crate smtp; use std::io::net::tcp::TcpStream; use smtp::client::SmtpClient; diff --git a/src/smtp/client.rs b/src/smtp/client.rs index 05f18cc..fc199bf 100644 --- a/src/smtp/client.rs +++ b/src/smtp/client.rs @@ -11,7 +11,7 @@ email_client.send_mail("user@example.org", [&"user@localhost"], "Message content # TODO - Support ESMTP : Parse server answer, and manage mail and rcpt options. +Support ESMTP : Parse server answer, and manage mail and rcpt options. * Client options: `mail_options` and `rcpt_options` lists @@ -25,14 +25,25 @@ Support SSL/TLS use std::fmt; use std::str::from_utf8; +use std::result::Result; use std::io::{IoResult, IoError}; use std::io::net::ip::{SocketAddr, Port}; use std::io::net::tcp::TcpStream; use std::io::net::addrinfo::get_host_addresses; use common::{SMTP_PORT, CRLF}; use commands; +use commands::{Command, SmtpCommand}; + +// // Define smtp_fail! and smtp_success! +// macro_rules! smtp_fail( +// ($command:expr $code:ident $message:expr) => ( +// fail!("{} failed: {:u} {:s}", $command, $code, $message); +// ); +// ) + /// Contains an SMTP reply, with separed code and message +#[deriving(Eq,Clone)] pub struct SmtpResponse { /// Server respinse code code code: uint, @@ -55,14 +66,14 @@ impl fmt::Show for SmtpResponse { } impl SmtpResponse { - /// Check the repsonse code and fail if there is an error - fn check_response(&self, expected_codes: &[uint]) { + /// Check the response code + fn with_code(&self, expected_codes: &[uint]) -> Result { for &code in expected_codes.iter() { if code == self.code { - return; + return Ok(SmtpResponse{code: self.code, message: self.message.clone()}); } } - fail!("Failed with {}", self.to_str()); + return Err(SmtpResponse{code: self.code, message: self.message.clone()}); } } @@ -75,14 +86,35 @@ pub struct SmtpClient { /// Port we are connecting on port: Port, /// Our hostname for HELO/EHLO commands - my_hostname: ~str + my_hostname: ~str, + /// Does the server supports ESMTP + does_esmtp: Option, + /// ESMTP features supported by the server + esmtp_features: Option<~[~str]> } -impl SmtpClient { +impl SmtpClient { + /// Create a new SMTP client + pub fn new(host: &str, port: Option, my_hostname: Option<&str>) -> SmtpClient { + SmtpClient{ + stream: None, + host: host.to_owned(), + port: port.unwrap_or(SMTP_PORT), + my_hostname: my_hostname.unwrap_or("localhost").to_owned(), + does_esmtp: None, + esmtp_features: None + } + } +// fn parse_ehello_or_hello_response(response: &str) { +// // split +// } +} + +impl SmtpClient { /// Send an SMTP command - pub fn send_command(&mut self, command: commands::Command, option: Option<~str>) -> SmtpResponse { - self.send_and_get_response(commands::SmtpCommand::new(command, option).get_formatted_command()) + pub fn send_command(&mut self, command: Command, option: Option<~str>) -> SmtpResponse { + self.send_and_get_response(SmtpCommand::new(command, option).to_str()) } /// Send an email @@ -92,7 +124,8 @@ impl SmtpClient { /// Send a complete message or a command to the server and get the response fn send_and_get_response(&mut self, string: ~str) -> SmtpResponse { - match (&mut self.stream.clone().unwrap() as &mut Writer).write_str(format!("{:s}{:s}", string, CRLF)) { + match (&mut self.stream.clone().unwrap() as &mut Writer) + .write_str(format!("{:s}{:s}", string, CRLF)) { Err(..) => fail!("Could not write to stream"), Ok(..) => debug!("Write success") } @@ -105,7 +138,7 @@ impl SmtpClient { /// Get the SMTP response fn get_reply(&mut self) -> Option { - let response = match self.stream.clone().unwrap().read_to_str() { + let response = match self.read_to_str() { Err(..) => fail!("No answer"), Ok(string) => string }; @@ -120,59 +153,93 @@ impl SmtpClient { } } - /// Create a new SMTP client - pub fn new(host: &str, port: Option, my_hostname: Option<&str>) -> SmtpClient { - SmtpClient{ - stream: None, - host: host.to_owned(), - port: port.unwrap_or(SMTP_PORT), - my_hostname: my_hostname.unwrap_or("localhost").to_owned(), - } - } -} - -impl SmtpClient { - - /// Send an email - pub fn send_mail(&mut self, from_addr: &str, to_addrs: &[&str], message: &str) { - let my_hostname = self.my_hostname.clone(); - self.connect().check_response([220]); - self.send_command(commands::Hello, Some(my_hostname)).check_response([250]); - self.send_command(commands::Mail, Some(from_addr.to_owned())).check_response([250]); - for &to_addr in to_addrs.iter() { - self.send_command(commands::Recipient, Some(to_addr.to_owned())).check_response([250]); - } - self.send_command(commands::Data, None).check_response([354]); - self.send_message(message.to_owned()).check_response([250]); - self.send_command(commands::Quit, None).check_response([221]); - } - /// Connect to the configured server pub fn connect(&mut self) -> SmtpResponse { - if !self.stream.is_none() { fail!("The connection is already established"); } - let ip = match get_host_addresses(self.host.clone()) { Ok(ip_vector) => ip_vector[0], Err(..) => fail!("Cannot resolve {}", self.host) }; - self.stream = match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) { Err(..) => fail!("Cannot connect to {}:{}", self.host, self.port), Ok(stream) => Some(stream) }; - match self.get_reply() { None => fail!("No banner on {}", self.host), Some(response) => response } } + + /// Send a QUIT command and end the program + fn smtp_fail(&mut self, command: ~str, response: SmtpResponse) { + self.send_command(commands::Quit, None); + fail!("{} failed: {:u} {:s}", command, response.code, response.message); + } + + /// Send an email + pub fn send_mail(&mut self, from_addr: &str, to_addrs: &[&str], message: &str) { + let my_hostname = self.my_hostname.clone(); + + // Connect + match self.connect().with_code([220]) { + Ok(response) => info!("{:u} {:s}", response.code, response.message), + Err(response) => self.smtp_fail(~"CONNECT", response) + } + + // Ehello or Hello + match self.send_command(commands::Ehello, Some(my_hostname.clone())).with_code([250, 500]) { + Ok(SmtpResponse{code: 250, message: message}) => { + self.does_esmtp = Some(true); + info!("{:u} {:s}", 250u, message); + }, + Ok(SmtpResponse{code: code, message: message}) => { + self.does_esmtp = Some(false); + info!("{:u} {:s}", code, message); + match self.send_command(commands::Ehello, Some(my_hostname.clone())).with_code([250]) { + Ok(response) => info!("{:u} {:s}", response.code, response.message), + Err(response) => self.smtp_fail(~"HELO", response) + } + }, + Err(response) => self.smtp_fail(~"EHLO", response) + } + + // Mail + match self.send_command(commands::Mail, Some(from_addr.to_owned())).with_code([250]) { + Ok(response) => info!("{:u} {:s}", response.code, response.message), + Err(response) => self.smtp_fail(~"MAIL", response) + } + + // Recipient + for &to_addr in to_addrs.iter() { + match self.send_command(commands::Recipient, Some(to_addr.to_owned())).with_code([250]) { + Ok(response) => info!("{:u} {:s}", response.code, response.message), + Err(response) => self.smtp_fail(~"RCPT", response) + } + } + + // Data + match self.send_command(commands::Data, None).with_code([354]) { + Ok(response) => info!("{:u} {:s}", response.code, response.message), + Err(response) => self.smtp_fail(~"DATA", response) + } + + // Message content + match self.send_message(message.to_owned()).with_code([250]) { + Ok(response) => info!("{:u} {:s}", response.code, response.message), + Err(response) => self.smtp_fail(~"MESSAGE", response) + } + + // Quit + match self.send_command(commands::Quit, None).with_code([221]) { + Ok(response) => info!("{:u} {:s}", response.code, response.message), + Err(response) => self.smtp_fail(~"DATA", response) + } + } } impl Reader for SmtpClient { - /// Read a string from the client socket fn read(&mut self, buf: &mut [u8]) -> IoResult { self.stream.clone().unwrap().read(buf) @@ -193,7 +260,6 @@ impl Reader for SmtpClient { } impl Writer for SmtpClient { - /// Send a string on the client socket fn write(&mut self, buf: &[u8]) -> IoResult<()> { self.stream.clone().unwrap().write(buf) diff --git a/src/smtp/commands.rs b/src/smtp/commands.rs index 86716ef..0751778 100644 --- a/src/smtp/commands.rs +++ b/src/smtp/commands.rs @@ -27,21 +27,35 @@ use std::io; /// List of SMTP commands #[deriving(Eq,Clone)] pub enum Command { + /// Hello command Hello, + /// Ehello command Ehello, + /// Mail command Mail, + /// Recipient command Recipient, + /// Data command Data, + /// Reset command Reset, + /// SendMail command SendMail, + /// SendOrMail command SendOrMail, + /// SendAndMail command SendAndMail, + /// Verify command Verify, + /// Expand command Expand, + /// Help command Help, + /// Noop command Noop, + /// Quit command Quit, - /// Deprecated in RFC 5321 + /// Turn command, deprecated in RFC 5321 Turn, } @@ -164,7 +178,9 @@ impl fmt::Show for Command { /// Structure for a complete SMTP command, containing an optionnal string argument. pub struct SmtpCommand { + /// The SMTP command (e.g. MAIL, QUIT, ...) command: Command, + /// An optionnal argument to the command argument: Option<~str> } @@ -177,20 +193,31 @@ impl SmtpCommand { _ => SmtpCommand {command: command, argument: argument} } } +} +impl ToStr for SmtpCommand { /// Return the formatted command, ready to be used in an SMTP session. - pub fn get_formatted_command(&self) -> ~str { + fn to_str(&self) -> ~str { match (self.command.takes_argument(), self.command.needs_argument(), self.argument.clone()) { - (true, _, Some(argument)) => format!("{} {}", self.command, argument), - (_, false, None) => format!("{}", self.command), - _ => fail!("Wrong SMTP syntax") + (true, _, Some(argument)) => format!("{} {}", self.command, argument), + (_, false, None) => format!("{}", self.command), + _ => fail!("Wrong SMTP syntax") } } } +impl fmt::Show for SmtpCommand { + /// Return the formatted command, ready to be used in an SMTP session. + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), io::IoError> { + f.buf.write( + self.to_str().as_bytes() + ) + } +} + #[cfg(test)] mod test { - use super::SmtpCommand; + use super::{Command, SmtpCommand}; #[test] fn test_command_parameters() { @@ -199,13 +226,28 @@ mod test { assert!((super::Hello).needs_argument() == true); } + #[test] + fn test_to_str() { + assert!(super::Turn.to_str() == ~"TURN"); + } + +// #[test] +// fn test_from_str() { +// assert!(from_str == ~"TURN"); +// } + + #[test] + fn test_fmt() { + assert!(format!("{}", super::Turn) == ~"TURN"); + } + #[test] fn test_get_simple_command() { - assert!(SmtpCommand::new(super::Turn, None).get_formatted_command() == ~"TURN"); + assert!(SmtpCommand::new(super::Turn, None).to_str() == ~"TURN"); } #[test] fn test_get_argument_command() { - assert!(SmtpCommand::new(super::Ehello, Some(~"example.example")).get_formatted_command() == ~"EHLO example.example"); + assert!(SmtpCommand::new(super::Ehello, Some(~"example.example")).to_str() == ~"EHLO example.example"); } } diff --git a/src/smtp/common.rs b/src/smtp/common.rs index 44bc893..d5a4363 100644 --- a/src/smtp/common.rs +++ b/src/smtp/common.rs @@ -32,7 +32,6 @@ pub fn unquote_email_address(addr: &str) -> ~str { #[cfg(test)] mod test { - #[test] fn test_quote_email_address() { assert!(super::quote_email_address("plop") == ~""); @@ -44,4 +43,4 @@ mod test { assert!(super::unquote_email_address("") == ~"plop"); assert!(super::unquote_email_address("plop") == ~"plop"); } -} \ No newline at end of file +} diff --git a/src/smtp/lib.rs b/src/smtp/lib.rs index 84146b5..e08fbb0 100644 --- a/src/smtp/lib.rs +++ b/src/smtp/lib.rs @@ -1,14 +1,20 @@ +/*! + * SMTP library + * + * For now, contains only a basic and uncomplete SMTP client and some common general functions. + */ + #[crate_id = "smtp#0.1-pre"]; #[comment = "Rust SMTP client"]; -#[license = "MIT/ASL2"]; +#[license = "ASL2"]; #[crate_type = "lib"]; //#[crate_type = "dylib"]; //#[crate_type = "rlib"]; #[deny(non_camel_case_types)]; -//#[deny(missing_doc)]; +#[deny(missing_doc)]; pub mod commands; pub mod common;