From 91e050bfc8d23b7e7a7bbbf63065f46110b8acdb Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Tue, 13 May 2014 20:20:14 +0200 Subject: [PATCH] Refactoring to create an SMTP library --- src/client.rs | 75 ++++++------ src/commands.rs | 300 ------------------------------------------------ src/common.rs | 21 +++- src/lib.rs | 4 +- 4 files changed, 63 insertions(+), 337 deletions(-) delete mode 100644 src/commands.rs diff --git a/src/client.rs b/src/client.rs index 19b39bf..9d9df9f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -17,10 +17,14 @@ use std::strbuf::StrBuf; use std::io::{IoResult, Reader, Writer}; use std::io::net::ip::{SocketAddr, Port}; use std::io::net::tcp::TcpStream; -use std::io::net::addrinfo::get_host_addresses; -use common::{CRLF, get_first_word, unquote_email_address}; -use commands; -use commands::{SMTP_PORT, SmtpCommand, EsmtpParameter, SmtpResponse}; + +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::{SMTP_PORT, CRLF}; /// Information about an SMTP server #[deriving(Clone)] @@ -131,6 +135,7 @@ macro_rules! smtp_fail_if_err( /// Structure that implements the SMTP client pub struct SmtpClient { /// TCP stream between client and server + /// Value is None before connection stream: Option, /// Host we are connecting to host: T, @@ -141,7 +146,7 @@ pub struct SmtpClient { /// Information about the server /// Value is None before HELO/EHLO server_info: Option>, - /// Transaction state, permits to check order againt RFCs + /// Transaction state, to check the sequence of commands state: SmtpClientState } @@ -165,9 +170,9 @@ impl SmtpClient { if !self.stream.is_none() { fail!("The connection is already established"); } - let ip = match get_host_addresses(self.host.clone().into_owned()) { - Ok(ip_vector) => *ip_vector.get(0), // TODO : select a random ip - Err(..) => fail!("Cannot resolve {:s}", self.host) + let ip = match resolve_host(self.host.clone().into_owned()) { + Ok(ip) => ip, + Err(..) => fail!("Cannot resolve {:s}", self.host) }; self.stream = match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) { Ok(stream) => Some(stream), @@ -219,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(commands::EightBitMime).is_ok() { + if ! self.server_info.clone().unwrap().supports_feature(esmtp_parameter::EightBitMime).is_ok() { if ! message.clone().into_owned().is_ascii() { self.smtp_fail("Server does not accepts UTF-8 strings"); } @@ -310,14 +315,19 @@ impl SmtpClient { /// Closes the TCP stream pub fn close(&mut self) { + // Close the TCP connection drop(self.stream.clone().unwrap()); + // Reset client state + self.stream = None; + self.state = Unconnected; + self.server_info = None; } /// Send a HELO command pub fn helo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { check_state_in!(vec!(Connected)); - match self.send_command(commands::Hello(my_hostname.clone())).with_code(vec!(250)) { + match self.send_command(smtp_command::Hello(my_hostname.clone())).with_code(vec!(250)) { Ok(response) => { self.server_info = Some( SmtpServerInfo{ @@ -336,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(commands::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) { + match self.send_command(smtp_command::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) { Ok(response) => { self.server_info = Some( SmtpServerInfo{ @@ -355,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(commands::Mail(StrBuf::from_str(unquote_email_address(from_address.to_owned())), options)).with_code(vec!(250)) { + match self.send_command(smtp_command::Mail(StrBuf::from_str(unquote_email_address(from_address.to_owned())), options)).with_code(vec!(250)) { Ok(response) => { self.state = MailSent; Ok(response) @@ -370,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(commands::Recipient(StrBuf::from_str(unquote_email_address(to_address.to_owned())), options)).with_code(vec!(250)) { + match self.send_command(smtp_command::Recipient(StrBuf::from_str(unquote_email_address(to_address.to_owned())), options)).with_code(vec!(250)) { Ok(response) => { self.state = RcptSent; Ok(response) @@ -385,7 +395,7 @@ impl SmtpClient { pub fn data(&mut self) -> Result, SmtpResponse> { check_state_in!(vec!(RcptSent)); - match self.send_command(commands::Data).with_code(vec!(354)) { + match self.send_command(smtp_command::Data).with_code(vec!(354)) { Ok(response) => { self.state = DataSent; Ok(response) @@ -414,9 +424,8 @@ impl SmtpClient { /// Sends a QUIT command pub fn quit(&mut self) -> Result, SmtpResponse> { check_state_not_in!(vec!(Unconnected)); - match self.send_command(commands::Quit).with_code(vec!(221)) { + match self.send_command(smtp_command::Quit).with_code(vec!(221)) { Ok(response) => { - self.close(); Ok(response) }, Err(response) => { @@ -428,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(commands::Reset).with_code(vec!(250)) { + match self.send_command(smtp_command::Reset).with_code(vec!(250)) { Ok(response) => { if vec!(MailSent, RcptSent, DataSent).contains(&self.state) { self.state = HeloSent; @@ -444,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(commands::Noop).with_code(vec!(250)) + self.send_command(smtp_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(commands::Verify(to_address)).with_code(vec!(250)) + self.send_command(smtp_command::Verify(to_address)).with_code(vec!(250)) } } @@ -489,17 +498,17 @@ impl Writer for SmtpClient { #[cfg(test)] mod test { use super::SmtpServerInfo; - use commands; + use smtp::esmtp_parameter; #[test] fn test_smtp_server_info_fmt() { assert_eq!(format!("{}", SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(commands::EightBitMime)) + esmtp_features: Some(vec!(esmtp_parameter::EightBitMime)) }), "name with [8BITMIME]".to_owned()); assert_eq!(format!("{}", SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(commands::EightBitMime, commands::Size(42))) + esmtp_features: Some(vec!(esmtp_parameter::EightBitMime, esmtp_parameter::Size(42))) }), "name with [8BITMIME, SIZE=42]".to_owned()); assert_eq!(format!("{}", SmtpServerInfo{ name: "name", @@ -510,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!(commands::EightBitMime, commands::Size(42)))); + Some(vec!(esmtp_parameter::EightBitMime, esmtp_parameter::Size(42)))); assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 UNKNON 42"), - Some(vec!(commands::EightBitMime))); + Some(vec!(esmtp_parameter::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!(commands::Size(42), commands::Size(43)))); + Some(vec!(esmtp_parameter::Size(42), esmtp_parameter::Size(43)))); assert_eq!(SmtpServerInfo::parse_esmtp_response(""), None); } @@ -525,19 +534,19 @@ mod test { fn test_smtp_server_info_supports_feature() { assert_eq!(SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(commands::EightBitMime)) - }.supports_feature(commands::EightBitMime), Ok(commands::EightBitMime)); + esmtp_features: Some(vec!(esmtp_parameter::EightBitMime)) + }.supports_feature(esmtp_parameter::EightBitMime), Ok(esmtp_parameter::EightBitMime)); assert_eq!(SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(commands::Size(42), commands::EightBitMime)) - }.supports_feature(commands::EightBitMime), Ok(commands::EightBitMime)); + esmtp_features: Some(vec!(esmtp_parameter::Size(42), esmtp_parameter::EightBitMime)) + }.supports_feature(esmtp_parameter::EightBitMime), Ok(esmtp_parameter::EightBitMime)); assert_eq!(SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(commands::Size(42), commands::EightBitMime)) - }.supports_feature(commands::Size(0)), Ok(commands::Size(42))); + esmtp_features: Some(vec!(esmtp_parameter::Size(42), esmtp_parameter::EightBitMime)) + }.supports_feature(esmtp_parameter::Size(0)), Ok(esmtp_parameter::Size(42))); assert!(SmtpServerInfo{ name: "name", - esmtp_features: Some(vec!(commands::EightBitMime)) - }.supports_feature(commands::Size(42)).is_err()); + esmtp_features: Some(vec!(esmtp_parameter::EightBitMime)) + }.supports_feature(esmtp_parameter::Size(42)).is_err()); } } diff --git a/src/commands.rs b/src/commands.rs deleted file mode 100644 index d2c44df..0000000 --- a/src/commands.rs +++ /dev/null @@ -1,300 +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 commands and ESMTP features library - -use std::fmt::{Show, Formatter, Result}; -use std::result; -use std::io::net::ip::Port; -use std::from_str::FromStr; - -use common::remove_trailing_crlf; - -/// Default SMTP port -pub static SMTP_PORT: Port = 25; -//pub static SMTPS_PORT: Port = 465; -//pub static SUBMISSION_PORT: Port = 587; - -/// 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 -#[deriving(Eq,Clone)] -pub enum SmtpCommand { - /// Extended Hello command - ExtendedHello(T), - /// Hello command - Hello(T), - /// Mail command, takes optionnal options - Mail(T, Option>), - /// Recipient command, takes optionnal options - Recipient(T, Option>), - /// Data command - Data, - /// Reset command - Reset, - /// Verify command - Verify(T), - /// Expand command - Expand(T), - /// Help command, takes optionnal options - Help(Option), - /// Noop command - Noop, - /// Quit command - Quit, - -} - -impl Show for SmtpCommand { - fn fmt(&self, f: &mut Formatter) -> Result { - f.buf.write(match *self { - ExtendedHello(ref my_hostname) => - format!("EHLO {}", my_hostname.clone()), - Hello(ref my_hostname) => - format!("HELO {}", my_hostname.clone()), - Mail(ref from_address, None) => - format!("MAIL FROM:<{}>", from_address.clone()), - Mail(ref from_address, Some(ref options)) => - format!("MAIL FROM:<{}> {}", from_address.clone(), options.connect(" ")), - Recipient(ref to_address, None) => - format!("RCPT TO:<{}>", to_address.clone()), - Recipient(ref to_address, Some(ref options)) => - format!("RCPT TO:<{}> {}", to_address.clone(), options.connect(" ")), - Data => "DATA".to_owned(), - Reset => "RSET".to_owned(), - Verify(ref address) => - format!("VRFY {}", address.clone()), - Expand(ref address) => - format!("EXPN {}", address.clone()), - Help(None) => "HELP".to_owned(), - Help(Some(ref argument)) => - format!("HELP {}", argument.clone()), - Noop => "NOOP".to_owned(), - Quit => "QUIT".to_owned(), - }.as_bytes()) - } -} - -/// Supported ESMTP keywords -#[deriving(Eq,Clone)] -pub enum EsmtpParameter { - /// 8BITMIME keyword - /// - /// RFC 6152 : https://tools.ietf.org/html/rfc6152 - EightBitMime, - /// SIZE keyword - /// - /// RFC 1427 : https://tools.ietf.org/html/rfc1427 - Size(uint) -} - -impl Show for EsmtpParameter { - fn fmt(&self, f: &mut Formatter) -> Result { - f.buf.write( - match self { - &EightBitMime => "8BITMIME".to_owned(), - &Size(ref size) => format!("SIZE={}", size) - }.as_bytes() - ) - } -} - -impl FromStr for EsmtpParameter { - fn from_str(s: &str) -> Option { - let splitted : Vec<&str> = s.splitn(' ', 1).collect(); - match splitted.len() { - 1 => match *splitted.get(0) { - "8BITMIME" => Some(EightBitMime), - _ => None - }, - 2 => match (*splitted.get(0), from_str::(*splitted.get(1))) { - ("SIZE", Some(size)) => Some(Size(size)), - _ => None - }, - _ => None - } - } -} - -impl EsmtpParameter { - /// Checks if the ESMTP keyword is the same - pub fn same_keyword_as(&self, other: EsmtpParameter) -> bool { - if *self == other { - return true; - } - match (*self, other) { - (Size(_), Size(_)) => true, - _ => false - } - } -} - -/// Contains an SMTP reply, with separed code and message -/// -/// We do accept messages containing only a code, to comply with RFC5321 -#[deriving(Clone, Eq)] -pub struct SmtpResponse { - /// Server response code - pub code: u16, - /// Server response string - pub message: Option -} - -impl Show for SmtpResponse { - fn fmt(&self, f: &mut Formatter) -> Result { - f.buf.write( - match self.clone().message { - Some(message) => format!("{} {}", self.code.to_str(), message), - None => self.code.to_str() - }.as_bytes() - ) - } -} - -// FromStr ? -impl FromStr for SmtpResponse { - fn from_str(s: &str) -> Option> { - // If the string is too short to be a response code - if s.len() < 3 { - None - // If we have only a code, with or without a trailing space - } else if s.len() == 3 || (s.len() == 4 && s.slice(3,4) == " ") { - match from_str::(s.slice_to(3)) { - Some(code) => Some(SmtpResponse{ - code: code, - message: None - }), - None => None - - } - // If we have a code and a message - } else { - match ( - from_str::(s.slice_to(3)), - vec!(" ", "-").contains(&s.slice(3,4)), - StrBuf::from_str(remove_trailing_crlf(s.slice_from(4).to_owned())) - ) { - (Some(code), true, message) => Some(SmtpResponse{ - code: code, - message: Some(message) - }), - _ => None - - } - } - } -} - -impl SmtpResponse { - /// Checks the presence of the response code in the array of expected codes. - pub fn with_code(&self, expected_codes: Vec) -> result::Result,SmtpResponse> { - let response = self.clone(); - if expected_codes.contains(&self.code) { - Ok(response) - } else { - Err(response) - } - } -} - -#[cfg(test)] -mod test { - use super::{SmtpCommand, EsmtpParameter, SmtpResponse}; - - #[test] - fn test_command_fmt() { - let noop: SmtpCommand = super::Noop; - assert_eq!(format!("{}", noop), "NOOP".to_owned()); - assert_eq!(format!("{}", super::ExtendedHello("me")), "EHLO me".to_owned()); - assert_eq!(format!("{}", - super::Mail("test", Some(vec!("option")))), "MAIL FROM: option".to_owned() - ); - } - - #[test] - fn test_esmtp_parameter_same_keyword_as() { - assert_eq!(super::EightBitMime.same_keyword_as(super::EightBitMime), true); - assert_eq!(super::Size(42).same_keyword_as(super::Size(42)), true); - assert_eq!(super::Size(42).same_keyword_as(super::Size(43)), true); - assert_eq!(super::Size(42).same_keyword_as(super::EightBitMime), false); - } - - #[test] - fn test_esmtp_parameter_fmt() { - assert_eq!(format!("{}", super::EightBitMime), "8BITMIME".to_owned()); - assert_eq!(format!("{}", super::Size(42)), "SIZE=42".to_owned()); - } - - #[test] - fn test_esmtp_parameter_from_str() { - assert_eq!(from_str::("8BITMIME"), Some(super::EightBitMime)); - assert_eq!(from_str::("SIZE 42"), Some(super::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); - } - - #[test] - fn test_smtp_response_fmt() { - assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message")}), "200 message".to_owned()); - } - - #[test] - fn test_smtp_response_from_str() { - assert_eq!(from_str::>("200 response message"), - Some(SmtpResponse{ - code: 200, - message: Some(StrBuf::from_str("response message")) - }) - ); - assert_eq!(from_str::>("200-response message"), - Some(SmtpResponse{ - code: 200, - message: Some(StrBuf::from_str("response message")) - }) - ); - assert_eq!(from_str::>("200"), - Some(SmtpResponse{ - code: 200, - message: None - }) - ); - assert_eq!(from_str::>("200 "), - Some(SmtpResponse{ - code: 200, - message: None - }) - ); - assert_eq!(from_str::>("200-response\r\nmessage"), - Some(SmtpResponse{ - code: 200, - message: Some(StrBuf::from_str("response\r\nmessage")) - }) - ); - assert_eq!(from_str::>("2000response message"), None); - assert_eq!(from_str::>("20a response message"), None); - assert_eq!(from_str::>("20 "), None); - assert_eq!(from_str::>("20"), None); - assert_eq!(from_str::>("2"), None); - assert_eq!(from_str::>(""), None); - } - - #[test] - fn test_smtp_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)), - Err(SmtpResponse{code: 400, message: Some("message")})); - assert_eq!(SmtpResponse{code: 200, message: Some("message")}.with_code(vec!(200, 300)), - Ok(SmtpResponse{code: 200, message: Some("message")})); - } -} diff --git a/src/common.rs b/src/common.rs index cb1554f..466d4dc 100644 --- a/src/common.rs +++ b/src/common.rs @@ -11,8 +11,18 @@ //! //! Needs to be organized later -pub static SP: &'static str = " "; -pub static CRLF: &'static str = "\r\n"; +use std::io::net::addrinfo::get_host_addresses; +use std::io::net::ip::IpAddr; + +use smtp::CRLF; + +/// Resolves an hostname and returns a random IP +pub fn resolve_host(hostname: ~str) -> Result { + match get_host_addresses(hostname) { + Ok(ip_vector) => Ok(*ip_vector.get(ip_vector.len() - 1)), + Err(..) => Err({}) + } +} /// Adds quotes to emails if needed pub fn quote_email_address(address: ~str) -> ~str { @@ -54,6 +64,13 @@ pub fn get_first_word(string: ~str) -> ~str { #[cfg(test)] mod test { + use std::io::net::ip::IpAddr; + + #[test] + fn test_resolve_host() { + assert_eq!(super::resolve_host("localhost".to_owned()), Ok(from_str::("127.0.0.1").unwrap())); + } + #[test] fn test_quote_email_address() { assert_eq!(super::quote_email_address("address".to_owned()), "
".to_owned()); diff --git a/src/lib.rs b/src/lib.rs index 11126ba..a1522d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -//! # Rust SMTP client +//! # Rust SMTP library //! //! The client does its best to follow RFC 5321 (https://tools.ietf.org/html/rfc5321). //! @@ -63,6 +63,6 @@ #![feature(phase)] #[phase(syntax, link)] extern crate log; -pub mod commands; +pub mod smtp; pub mod common; pub mod client;