From 89940874a6a266c9841c43b3da1a23a6a6fe57b0 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Sun, 20 Apr 2014 22:07:09 +0200 Subject: [PATCH] Prepare 0.1 version --- .gitignore | 3 +- Makefile | 1 - README.rst | 31 ++++++- src/examples/client.rs | 4 +- src/smtp/client.rs | 200 ++++++++++++++++++++++++---------------- src/smtp/commands.rs | 202 ++++++++++++++++++++++------------------- src/smtp/common.rs | 12 +++ src/smtp/lib.rs | 14 +-- 8 files changed, 279 insertions(+), 188 deletions(-) diff --git a/.gitignore b/.gitignore index 529b3ec..573320f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ doc/ -.kate* +*~ +*.sh .directory diff --git a/Makefile b/Makefile index 9ec6e92..891626a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ RUSTC ?= rustc RUSTDOC ?= rustdoc -RUSTPKG ?= rustpkg RUSTFLAGS ?= -g VERSION=0.1-pre diff --git a/README.rst b/README.rst index 02626d5..16bbb78 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,39 @@ Rust SMTP library ================= -This library implements an SMTP client, and maybe later a simple SMTP server. +.. image:: https://travis-ci.org/amousset/rust-smtp.png?branch=master + :target: https://travis-ci.org/amousset/rust-smtp -It does not support ESMTP nor SSL/TLS for now, and is basically an RFC821 client. +This library implements an SMTP client, and maybe later a simple SMTP server. Rust versions ------------- -This library follows rust master. +This library is designed for Rust 0.11-pre (master). + +Install +------ + +Build the library: + + make + +To build the example client code: + + make examples + +To run the example: + + ./build/client + +Todo +--- + +- Documentation +- RFC compliance +- Test corevage +- SSL/TLS support +- Client mail and rcpt options License ------- diff --git a/src/examples/client.rs b/src/examples/client.rs index c1de87c..7125125 100644 --- a/src/examples/client.rs +++ b/src/examples/client.rs @@ -1,4 +1,4 @@ -#[crate_id = "client"]; +#![crate_id = "client"] extern crate smtp; use std::io::net::tcp::TcpStream; @@ -6,5 +6,5 @@ use smtp::client::SmtpClient; fn main() { let mut email_client: SmtpClient = SmtpClient::new("localhost", None, None); - email_client.send_mail("amousset@localhost", [&"amousset@localhost"], "Test email"); + email_client.send_mail("user@localhost", [&"user@localhost"], "Test email"); } diff --git a/src/smtp/client.rs b/src/smtp/client.rs index 89026f0..f000b16 100644 --- a/src/smtp/client.rs +++ b/src/smtp/client.rs @@ -1,38 +1,25 @@ /*! -Simple SMTP client, without ESMTP and SSL/TLS support for now. +Simple SMTP client. # Usage ``` -let mut email_client: SmtpClient = SmtpClient::new("localhost", None, "myhost.example.org"); -email_client.send_mail("user@example.org", [&"user@localhost"], "Message content."); +let mut email_client: SmtpClient = SmtpClient::new("localhost", None, None); +email_client.send_mail("user@example.org", [&"user@example.com"], "Example email"); ``` -# TODO - -Think about RFC compliance in the SMTP library. - - - -Support ESMTP : Parse server answer, and manage mail and rcpt options. - -* Client options: `mail_options` and `rcpt_options` lists - -* Server options: helo/ehlo, parse and store ehlo response - -Support SSL/TLS - */ use std::fmt; +use std::from_str; 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 common::{SMTP_PORT, CRLF, get_first_word}; use commands; use commands::{Command, SmtpCommand, EhloKeyword}; @@ -56,6 +43,24 @@ impl fmt::Show for SmtpResponse { } } +impl from_str::FromStr for SmtpResponse { + /// Parse an SMTP response line + fn from_str(s: &str) -> Option { + if s.len() < 5 { + None + } else { + if [" ", "-"].contains(&s.slice(3,4)) { + Some(SmtpResponse{ + code: from_str(s.slice_to(3)).unwrap(), + message: s.slice_from(4).to_owned() + }) + } else { + None + } + } + } +} + impl SmtpResponse { /// Check the response code fn with_code(&self, expected_codes: &[uint]) -> Result { @@ -74,12 +79,50 @@ impl SmtpResponse { pub struct SmtpServerInfo { /// Server name name: ~str, - /// Does the server supports ESMTP - does_esmtp: bool, /// ESMTP features supported by the server esmtp_features: Option<~[EhloKeyword]> } +impl SmtpServerInfo { + /// Parse supported ESMTP features + fn parse_esmtp_response(message: &str) -> Option<~[EhloKeyword]> { + let mut esmtp_features: ~[EhloKeyword] = ~[]; + for line in message.split_str(CRLF) { + match from_str::(line) { + Some(SmtpResponse{code: 250, message: message}) => { + match from_str::(message) { + Some(keyword) => esmtp_features.push(keyword), + None => () + } + }, + _ => () + } + } + match esmtp_features.len() { + 0 => None, + _ => Some(esmtp_features) + } + } + + /// Checks if the server supports an ESMTP feature + fn supports_feature(&self, keyword: EhloKeyword) -> bool { + match self.esmtp_features.clone() { + Some(esmtp_features) => { + esmtp_features.contains(&keyword) + }, + None => false + } + } +} + +impl fmt::Show for SmtpServerInfo { + /// Format SMTP server information display + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), IoError> { + f.buf.write( + format!("{:s} with {}", self.name, self.esmtp_features).as_bytes() + ) + } +} /// Structure that implements a simple SMTP client pub struct SmtpClient { @@ -106,19 +149,6 @@ impl SmtpClient { server_info: None } } - - -// pub fn does_esmtp_feature(keyword: EhloKeyword) -// fn parse_ehello_or_hello_response(response: &str) { -// // split -// } -// pub fn parse_ehlo_features(response: &str) -> { -// -// -// // split \n -// // let b: ~[int] = a.iter().map(|&x| x).to_owned_vec(); -// } - } impl SmtpClient { @@ -136,31 +166,24 @@ impl SmtpClient { 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)) { - Err(..) => fail!("Could not write to stream"), - Ok(..) => debug!("Write success") + Ok(..) => debug!("Write success"), + Err(..) => fail!("Could not write to stream") } match self.get_reply() { - None => fail!("No answer on {}", self.host), - Some(response) => response + Some(response) => response, + None => fail!("No answer on {}", self.host) } } /// Get the SMTP response fn get_reply(&mut self) -> Option { let response = match self.read_to_str() { - Err(..) => fail!("No answer"), - Ok(string) => string + Ok(string) => string, + Err(..) => fail!("No answer") }; - if response.len() > 4 { - Some(SmtpResponse { - code: from_str(response.slice_to(3)).unwrap(), - message: response.slice_from(4).to_owned() - }) - } else { - None - } + from_str::(response) } /// Connect to the configured server @@ -170,15 +193,15 @@ impl SmtpClient { } let ip = match get_host_addresses(self.host.clone()) { Ok(ip_vector) => ip_vector[0], - Err(..) => fail!("Cannot resolve {}", self.host) + 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) + Ok(stream) => Some(stream), + Err(..) => fail!("Cannot connect to {}:{}", self.host, self.port) }; match self.get_reply() { - None => fail!("No banner on {}", self.host), - Some(response) => response + Some(response) => response, + None => fail!("No banner on {}", self.host) } } @@ -188,9 +211,9 @@ impl SmtpClient { } /// 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); + fn smtp_fail(&mut self, command: ~str, reason: &str) { + self.send_command(commands::Quit, None); + fail!("{} failed: {:s}", command, reason); } /// Send an email @@ -199,58 +222,77 @@ impl SmtpClient { // Connect match self.connect().with_code([220]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"CONNECT", response) + Ok(response) => self.smtp_success(response), + Err(response) => self.smtp_fail(~"CONNECT", response.to_str()) } // Extended Hello or Hello - match self.send_command(commands::EHLO, Some(my_hostname.clone())).with_code([250, 500]) { + match self.send_command(commands::Ehlo, Some(my_hostname.clone())).with_code([250, 500]) { Ok(SmtpResponse{code: 250, message: message}) => { - self.server_info = Some(SmtpServerInfo{name: message.clone(), does_esmtp: true, esmtp_features: None}); + self.server_info = Some( + SmtpServerInfo{ + name: get_first_word(message.clone()), + esmtp_features: SmtpServerInfo::parse_esmtp_response(message.clone()) + } + ); self.smtp_success(SmtpResponse{code: 250u, message: message}); }, Ok(..) => { - match self.send_command(commands::HELO, Some(my_hostname.clone())).with_code([250]) { + match self.send_command(commands::Helo, Some(my_hostname.clone())).with_code([250]) { Ok(response) => { - self.server_info = Some(SmtpServerInfo{name: response.message.clone(), does_esmtp: false, esmtp_features: None}); + self.server_info = Some( + SmtpServerInfo{ + name: get_first_word(response.message.clone()), + esmtp_features: None + } + ); self.smtp_success(response); }, - Err(response) => self.smtp_fail(~"HELO", response) + Err(response) => self.smtp_fail(~"HELO", response.to_str()) } }, - Err(response) => self.smtp_fail(~"EHLO", response) + Err(response) => self.smtp_fail(~"EHLO", response.to_str()) + } + + debug!("SMTP server : {:s}", self.server_info.clone().unwrap().to_str()) + + // Check message encoding according to the server's capability + if ! self.server_info.clone().unwrap().supports_feature(commands::EightBitMime) { + if ! message.is_ascii() { + self.smtp_fail(~"DATA", "Server does not accepts UTF-8 strings") + } } // Mail - match self.send_command(commands::MAIL, Some(from_addr.to_owned())).with_code([250]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"MAIL", response) + match self.send_command(commands::Mail, Some(from_addr.to_owned())).with_code([250]) { + Ok(response) => self.smtp_success(response), + Err(response) => self.smtp_fail(~"MAIL", response.to_str()) } // Recipient for &to_addr in to_addrs.iter() { - match self.send_command(commands::RCPT, Some(to_addr.to_owned())).with_code([250]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"RCPT", response) + match self.send_command(commands::Rcpt, Some(to_addr.to_owned())).with_code([250]) { + Ok(response) => self.smtp_success(response), + Err(response) => self.smtp_fail(~"RCPT", response.to_str()) } } // Data - match self.send_command(commands::DATA, None).with_code([354]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"DATA", response) + match self.send_command(commands::Data, None).with_code([354]) { + Ok(response) => self.smtp_success(response), + Err(response) => self.smtp_fail(~"DATA", response.to_str()) } // Message content match self.send_message(message.to_owned()).with_code([250]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"MESSAGE", response) + Ok(response) => self.smtp_success(response), + Err(response) => self.smtp_fail(~"MESSAGE", response.to_str()) } // Quit - match self.send_command(commands::QUIT, None).with_code([221]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"DATA", response) + match self.send_command(commands::Quit, None).with_code([221]) { + Ok(response) => self.smtp_success(response), + Err(response) => self.smtp_fail(~"DATA", response.to_str()) } } } @@ -266,8 +308,8 @@ impl Reader for SmtpClient { let mut buf = [0u8, ..1000]; let response = match self.read(buf) { - Err(..) => fail!("Read error"), - Ok(bytes_read) => from_utf8(buf.slice_to(bytes_read - 1)).unwrap() + Ok(bytes_read) => from_utf8(buf.slice_to(bytes_read - 1)).unwrap(), + Err(..) => fail!("Read error") }; debug!("Read: {:s}", response); diff --git a/src/smtp/commands.rs b/src/smtp/commands.rs index 9f555c2..9007b53 100644 --- a/src/smtp/commands.rs +++ b/src/smtp/commands.rs @@ -1,104 +1,89 @@ /*! - * SMTP commands library + * SMTP commands and ESMTP features library * - * RFC 5321 : http://tools.ietf.org/html/rfc5321#section-4.1 + * RFC 5321 : https://tools.ietf.org/html/rfc5321#section-4.1 */ use std::fmt; use std::io; - -/* - * HELO - * MAIL FROM: - * RCPT TO: - * DATA - * RSET - * SEND FROM: - * SOML FROM: - * SAML FROM: - * VRFY - * EXPN - * HELP [ ] - * NOOP - * QUIT - * TURN - */ +use std::from_str; +use std::io::IoError; /// List of SMTP commands #[deriving(Eq,Clone)] pub enum Command { - /// Hello command - HELO, /// Extended Hello command - EHLO, + Ehlo, + /// Hello command + Helo, /// Mail command - MAIL, + Mail, /// Recipient command - RCPT, + Rcpt, /// Data command - DATA, + Data, /// Reset command - RSET, + Rset, /// Send command, deprecated in RFC 5321 - SEND, + Send, /// Send Or Mail command, deprecated in RFC 5321 - SOML, + Soml, /// Send And Mail command, deprecated in RFC 5321 - SAML, + Saml, /// Verify command - VRFY, + Vrfy, /// Expand command - EXPN, + Expn, /// Help command - HELP, + Help, /// Noop command - NOOP, + Noop, /// Quit command - QUIT, + Quit, /// Turn command, deprecated in RFC 5321 - TURN, + Turn, } impl Command { /// Tell if the command accetps an string argument. pub fn takes_argument(&self) -> bool{ match *self { - EHLO => true, - HELO => true, - MAIL => true, - RCPT => true, - DATA => false, - RSET => false, - SEND => true, - SOML => true, - SAML => true, - VRFY => true, - EXPN => true, - HELP => true, - NOOP => false, - QUIT => false, - TURN => false, + Ehlo => true, + Helo => true, + Mail => true, + Rcpt => true, + Data => false, + Rset => false, + Send => true, + Soml => true, + Saml => true, + Vrfy => true, + Expn => true, + Help => true, + Noop => false, + Quit => false, + Turn => false, } } /// Tell if an argument is needed by the command. pub fn needs_argument(&self) -> bool { match *self { - EHLO => true, - HELO => true, - MAIL => true, - RCPT => true, - DATA => false, - RSET => false, - SEND => true, - SOML => true, - SAML => true, - VRFY => true, - EXPN => true, - HELP => false, - NOOP => false, - QUIT => false, - TURN => false, + Ehlo => true, + Helo => true, + Mail => true, + Rcpt => true, + Data => false, + Rset => false, + Send => true, + Soml => true, + Saml => true, + Vrfy => true, + Expn => true, + Help => false, + Noop => false, + Quit => false, + Turn => false, } } } @@ -107,21 +92,21 @@ impl fmt::Show for Command { /// Format SMTP command display fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), io::IoError> { f.buf.write(match *self { - EHLO => "EHLO", - HELO => "HELO", - MAIL => "MAIL FROM:", - RCPT => "RCPT TO:", - DATA => "DATA", - RSET => "RSET", - SEND => "SEND TO:", - SOML => "SOML TO:", - SAML => "SAML TO:", - VRFY => "VRFY", - EXPN => "EXPN", - HELP => "HELP", - NOOP => "NOOP", - QUIT => "QUIT", - TURN => "TURN" + Ehlo => "EHLO", + Helo => "Helo", + Mail => "MAIL FROM:", + Rcpt => "RCPT TO:", + Data => "DATA", + Rset => "RSET", + Send => "SEND TO:", + Soml => "SOML TO:", + Saml => "SAML TO:", + Vrfy => "VRFY", + Expn => "EXPN", + Help => "HELP", + Noop => "NOOP", + Quit => "QUIT", + Turn => "TURN" }.as_bytes()) } } @@ -161,43 +146,70 @@ impl fmt::Show for SmtpCommand { /// Supported ESMTP keywords #[deriving(Eq,Clone)] pub enum EhloKeyword { - /// STARTTLS keyword - STARTTLS, /// 8BITMIME keyword - BITMIME, - /// SMTP authentification - AUTH + /// RFC 6152 : https://tools.ietf.org/html/rfc6152 + EightBitMime, } +impl fmt::Show for EhloKeyword { + /// Format SMTP response display + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), IoError> { + f.buf.write( + match self { + &EightBitMime => "8BITMIME".as_bytes() + } + ) + } +} + +impl from_str::FromStr for EhloKeyword { + // Match keywords + fn from_str(s: &str) -> Option { + match s { + "8BITMIME" => Some(EightBitMime), + _ => None + } + } +} #[cfg(test)] mod test { - use super::{Command, SmtpCommand}; + use super::{SmtpCommand, EhloKeyword}; #[test] fn test_command_parameters() { - assert!((super::HELP).takes_argument() == true); - assert!((super::RSET).takes_argument() == false); - assert!((super::HELO).needs_argument() == true); + assert!((super::Help).takes_argument() == true); + assert!((super::Rset).takes_argument() == false); + assert!((super::Helo).needs_argument() == true); } #[test] - fn test_to_str() { - assert!(super::TURN.to_str() == ~"TURN"); + fn test_command_to_str() { + assert!(super::Turn.to_str() == ~"TURN"); } #[test] - fn test_fmt() { - assert!(format!("{}", super::TURN) == ~"TURN"); + fn test_command_fmt() { + assert!(format!("{}", super::Turn) == ~"TURN"); } #[test] fn test_get_simple_command() { - assert!(SmtpCommand::new(super::TURN, None).to_str() == ~"TURN"); + assert!(SmtpCommand::new(super::Turn, None).to_str() == ~"TURN"); } #[test] fn test_get_argument_command() { - assert!(SmtpCommand::new(super::EHLO, Some(~"example.example")).to_str() == ~"EHLO example.example"); + assert!(SmtpCommand::new(super::Ehlo, Some(~"example.example")).to_str() == ~"EHLO example.example"); + } + + #[test] + fn test_ehlokeyword_fmt() { + assert!(format!("{}", super::EightBitMime) == ~"8BITMIME"); + } + + #[test] + fn test_ehlokeyword_from_str() { + assert!(from_str::("8BITMIME") == Some(super::EightBitMime)); } } diff --git a/src/smtp/common.rs b/src/smtp/common.rs index d5a4363..59f18f7 100644 --- a/src/smtp/common.rs +++ b/src/smtp/common.rs @@ -30,6 +30,11 @@ pub fn unquote_email_address(addr: &str) -> ~str { } } +/// Returns the first word of a string, or the string if it contains no space +pub fn get_first_word(string: &str) -> ~str { + string.split_str(CRLF).next().unwrap().splitn(' ', 1).next().unwrap().to_owned() +} + #[cfg(test)] mod test { #[test] @@ -43,4 +48,11 @@ mod test { assert!(super::unquote_email_address("") == ~"plop"); assert!(super::unquote_email_address("plop") == ~"plop"); } + + #[test] + fn test_get_first_word() { + assert!(super::get_first_word("first word") == ~"first"); + assert!(super::get_first_word("first word\ntest") == ~"first"); + assert!(super::get_first_word("first") == ~"first"); + } } diff --git a/src/smtp/lib.rs b/src/smtp/lib.rs index 3518bef..05349c2 100644 --- a/src/smtp/lib.rs +++ b/src/smtp/lib.rs @@ -4,19 +4,19 @@ * For now, contains only a basic and uncomplete SMTP client and some common general functions. */ -#[crate_id = "smtp#0.1-pre"]; +#![crate_id = "smtp#0.1-pre"] -#[comment = "Rust SMTP client"]; -#[license = "ASL2"]; -#[crate_type = "lib"]; +#![comment = "Rust SMTP client"] +#![license = "ASL2"] +#![crate_type = "lib"] //#[crate_type = "dylib"]; //#[crate_type = "rlib"]; -#[deny(non_camel_case_types)]; -#[deny(missing_doc)]; +#![deny(non_camel_case_types)] +#![deny(missing_doc)] -#[feature(phase)]; +#![feature(phase)] #[phase(syntax, link)] extern crate log; pub mod commands;