diff --git a/src/smtp/client.rs b/src/smtp/client.rs index 92b73af..68d3833 100644 --- a/src/smtp/client.rs +++ b/src/smtp/client.rs @@ -24,7 +24,7 @@ use commands; use commands::{SMTP_PORT, SmtpCommand, EsmtpParameter}; /// Contains an SMTP reply, with separed code and message -#[deriving(Eq,Clone)] +#[deriving(Clone)] pub struct SmtpResponse { /// Server response code code: uint, @@ -71,8 +71,8 @@ impl SmtpResponse { } /// Information about an SMTP server -#[deriving(Eq,Clone)] -pub struct SmtpServerInfo { +#[deriving(Clone)] +struct SmtpServerInfo { /// Server name name: T, /// ESMTP features supported by the server @@ -89,6 +89,7 @@ impl Show for SmtpServerInfo{ impl SmtpServerInfo { /// Parses supported ESMTP features + /// TODO: Improve parsing fn parse_esmtp_response(message: T) -> Option> { let mut esmtp_features = Vec::new(); for line in message.into_owned().split_str(CRLF) { @@ -121,7 +122,7 @@ impl SmtpServerInfo { /// Contains the state of the current transaction #[deriving(Eq,Clone)] -pub enum SmtpClientState { +enum SmtpClientState { /// The server is unconnected Unconnected, /// The connection and banner were successful @@ -139,7 +140,7 @@ pub enum SmtpClientState { macro_rules! check_state_in( ($expected_states:expr) => ( if ! $expected_states.contains(&self.state) { - fail!("Wrong transaction state for this command."); + fail!("Bad sequence of commands."); } ); ) @@ -147,7 +148,7 @@ macro_rules! check_state_in( macro_rules! check_state_not_in( ($expected_states:expr) => ( if $expected_states.contains(&self.state) { - fail!("Wrong transaction state for this command."); + fail!("Bad sequence of commands."); } ); ) @@ -249,7 +250,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) { - if false { + if message.clone().into_owned().is_ascii() { self.smtp_fail("Server does not accepts UTF-8 strings"); } } @@ -259,6 +260,7 @@ impl SmtpClient { // Recipient // TODO Return rejected addresses + // TODO Manage the number of recipients for to_address in to_addresses.iter() { smtp_fail_if_err!(self.rcpt(to_address.clone(), None)); } @@ -276,6 +278,7 @@ impl SmtpClient { impl SmtpClient { /// Sends an SMTP command + // TODO : ensure this is an ASCII string pub fn send_command(&mut self, command: SmtpCommand) -> SmtpResponse { self.send_and_get_response(format!("{}", command)) } @@ -333,7 +336,7 @@ impl SmtpClient { /// Send a HELO command pub fn helo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { - check_state_in!([Connected]); + check_state_in!(vec!(Connected)); match self.send_command(commands::Hello(my_hostname.clone())).with_code(vec!(250)) { Ok(response) => { @@ -353,7 +356,7 @@ impl SmtpClient { /// Sends a EHLO command pub fn ehlo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { - check_state_not_in!([Unconnected]); + check_state_not_in!(vec!(Unconnected)); match self.send_command(commands::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) { Ok(response) => { @@ -371,8 +374,8 @@ impl SmtpClient { } /// Sends a MAIL command - pub fn mail(&mut self, from_address: StrBuf, options: Option) -> Result, SmtpResponse> { - check_state_in!([HeloSent]); + pub fn mail(&mut self, from_address: StrBuf, options: Option>) -> Result, SmtpResponse> { + check_state_in!(vec!(HeloSent)); match self.send_command(commands::Mail(from_address, options)).with_code(vec!(250)) { Ok(response) => { @@ -386,8 +389,8 @@ impl SmtpClient { } /// Sends a RCPT command - pub fn rcpt(&mut self, to_address: StrBuf, options: Option) -> Result, SmtpResponse> { - check_state_in!([MailSent, RcptSent]); + pub fn rcpt(&mut self, to_address: StrBuf, options: Option>) -> Result, SmtpResponse> { + check_state_in!(vec!(MailSent, RcptSent)); match self.send_command(commands::Recipient(to_address, options)).with_code(vec!(250)) { Ok(response) => { @@ -402,7 +405,7 @@ impl SmtpClient { /// Sends a DATA command pub fn data(&mut self) -> Result, SmtpResponse> { - check_state_in!([RcptSent]); + check_state_in!(vec!(RcptSent)); match self.send_command(commands::Data).with_code(vec!(354)) { Ok(response) => { @@ -417,7 +420,7 @@ impl SmtpClient { /// Sends the message content pub fn message(&mut self, message_content: StrBuf) -> Result, SmtpResponse> { - check_state_in!([DataSent]); + check_state_in!(vec!(DataSent)); match self.send_message(message_content).with_code(vec!(250)) { Ok(response) => { @@ -433,7 +436,7 @@ impl SmtpClient { /// Sends a QUIT command pub fn quit(&mut self) -> Result, SmtpResponse> { - check_state_not_in!([Unconnected]); + check_state_not_in!(vec!(Unconnected)); match self.send_command(commands::Quit).with_code(vec!(221)) { Ok(response) => { self.close(); @@ -447,7 +450,7 @@ impl SmtpClient { /// Sends a RSET command pub fn rset(&mut self) -> Result, SmtpResponse> { - check_state_not_in!([Unconnected]); + check_state_not_in!(vec!(Unconnected)); match self.send_command(commands::Reset).with_code(vec!(250)) { Ok(response) => { if vec!(MailSent, RcptSent, DataSent).contains(&self.state) { @@ -463,13 +466,13 @@ impl SmtpClient { /// Sends a NOOP commands pub fn noop(&mut self) -> Result, SmtpResponse> { - check_state_not_in!([Unconnected]); + check_state_not_in!(vec!(Unconnected)); self.send_command(commands::Noop).with_code(vec!(250)) } /// Sends a VRFY command pub fn vrfy(&mut self, to_address: StrBuf) -> Result, SmtpResponse> { - check_state_not_in!([Unconnected]); + check_state_not_in!(vec!(Unconnected)); self.send_command(commands::Verify(to_address)).with_code(vec!(250)) } } @@ -481,6 +484,7 @@ impl Reader for SmtpClient { } /// Reads a string from the client socket + // TODO: Manage long messages. fn read_to_str(&mut self) -> IoResult<~str> { let mut buf = [0u8, ..1000]; diff --git a/src/smtp/commands.rs b/src/smtp/commands.rs index 7b550e9..0704afc 100644 --- a/src/smtp/commands.rs +++ b/src/smtp/commands.rs @@ -34,9 +34,9 @@ pub enum SmtpCommand { /// Hello command Hello(T), /// Mail command, takes optionnal options - Mail(T, Option), + Mail(T, Option>), /// Recipient command, takes optionnal options - Recipient(T, Option), + Recipient(T, Option>), /// Data command Data, /// Reset command @@ -54,7 +54,7 @@ pub enum SmtpCommand { } -impl Show for SmtpCommand { +impl Show for SmtpCommand { fn fmt(&self, f: &mut Formatter) -> Result { f.buf.write(match *self { ExtendedHello(ref my_hostname) => @@ -64,11 +64,11 @@ impl Show for SmtpCommand { 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.clone()), + 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.clone()), + format!("RCPT TO:{} {}", to_address.clone(), options.connect(" ")), Data => ~"DATA", Reset => ~"RSET", Verify(ref address) => @@ -90,22 +90,34 @@ 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".as_bytes() - } + &EightBitMime => ~"8BITMIME", + &Size(ref size) => format!("SIZE={}", size) + }.as_bytes() ) } } impl FromStr for EsmtpParameter { fn from_str(s: &str) -> Option { - match s.as_slice() { - "8BITMIME" => Some(EightBitMime), + let splitted : ~[&str] = s.splitn(' ', 1).collect(); + match splitted.len() { + 1 => match splitted[0] { + "8BITMIME" => Some(EightBitMime), + _ => None + }, + 2 => match (splitted[0], splitted[1]) { + ("SIZE", size) => Some(Size(from_str::(size).unwrap())), + _ => None + }, _ => None } } @@ -119,16 +131,18 @@ mod test { fn test_command_fmt() { //assert!(format!("{}", super::Noop) == ~"NOOP"); assert!(format!("{}", super::ExtendedHello("me")) == ~"EHLO me"); - assert!(format!("{}", super::Mail("test", Some("option"))) == ~"MAIL FROM:test option"); + assert!(format!("{}", super::Mail("test", Some(vec!("option")))) == ~"MAIL FROM:test option"); } #[test] fn test_esmtp_parameter_fmt() { assert!(format!("{}", super::EightBitMime) == ~"8BITMIME"); + assert!(format!("{}", super::Size(42)) == ~"SIZE=42"); } #[test] - fn test_ehlokeyword_from_str() { + fn test_esmtp_parameter_from_str() { assert!(from_str::("8BITMIME") == Some(super::EightBitMime)); + assert!(from_str::("SIZE 42") == Some(super::Size(42))); } } diff --git a/src/smtp/lib.rs b/src/smtp/lib.rs index 37b440a..05ba5f0 100644 --- a/src/smtp/lib.rs +++ b/src/smtp/lib.rs @@ -10,12 +10,23 @@ /*! SMTP library This library implements a simple SMTP client. -RFC 5321 : https://tools.ietf.org/html/rfc5321#section-4.1 -It does NOT manages email content. +# What this client is NOT made for -It also implements the following extesnions +*Send emails to public email servers.* It is not designed to smartly handle servers responses, +to rate-limit emails, to make retries, and all that complicated stuff needed to politely talk to public +servers. + +What this client does is basically try once to send the email, and say if it worked. It should be +used to transfer emails to a relay server, + +The client tends to follow RFC 5321 (https://tools.ietf.org/html/rfc5321). + +This is an SMTP client, and thus does NOT manages email content but only the enveloppe. + +It also implements the following extensions : 8BITMIME (RFC 6152 : https://tools.ietf.org/html/rfc6152) + SIZE (RFC 1427 : https://tools.ietf.org/html/rfc1427) # Usage @@ -24,17 +35,17 @@ let mut email_client: SmtpClient = SmtpClient::new(StrBuf::fr email_client.send_mail(StrBuf::from_str(""), vec!(StrBuf::from_str("")), StrBuf::from_str("Test email")); ``` -# TODO: - Add SSL/TLS - Add AUTH +# Next steps: + Add SSL/TLS support + Add AUTH support */ #![crate_id = "smtp#0.1-pre"] #![desc = "Rust SMTP client"] -#![comment = "Simple SMTP client"] -#![license = "ASL2"] +#![comment = "Simple SMTP client, without AUTH or SSL/TLS for now"] +#![license = "MIT/ASL2"] #![crate_type = "lib"] #![doc(html_root_url = "http://www.rust-ci.org/amousset/rust-smtp/doc/")]