From cc17e275c8a0d91e3eed0a9690227092ed285c9d Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Sun, 19 Oct 2014 03:02:37 +0200 Subject: [PATCH] Add client code and reorganize sources --- .gitignore | 3 - .travis.yml | 6 - Cargo.toml | 6 - README.md | 18 +- examples/client.rs | 110 ++++++ src/client/connecter.rs | 38 ++ src/client/mod.rs | 514 ++++++++++++++++++++++++++++ src/{smtpcommon => }/command.rs | 6 +- src/{smtpcommon => }/common.rs | 2 +- src/{smtpcommon => }/extension.rs | 6 +- src/lib.rs | 19 +- src/{smtpcommon => }/response.rs | 6 +- src/smtpcommon/mod.rs | 7 - src/{smtpcommon => }/transaction.rs | 6 +- tests/lib.rs | 5 + 15 files changed, 705 insertions(+), 47 deletions(-) create mode 100644 examples/client.rs create mode 100644 src/client/connecter.rs create mode 100644 src/client/mod.rs rename src/{smtpcommon => }/command.rs (97%) rename src/{smtpcommon => }/common.rs (98%) rename src/{smtpcommon => }/extension.rs (97%) rename src/{smtpcommon => }/response.rs (97%) delete mode 100644 src/smtpcommon/mod.rs rename src/{smtpcommon => }/transaction.rs (98%) create mode 100644 tests/lib.rs diff --git a/.gitignore b/.gitignore index 8807532..e9e2199 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ /target/ -/doc/ /Cargo.lock -*~ -*.swp diff --git a/.travis.yml b/.travis.yml index e19b38b..22761ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1 @@ language: rust -env: - global: - - secure: Lk4jMRKDf9NDdCP82mlGODiaBbImPVuerT71+whL/k0zrYJQjQMROWhKTuOWngZW4EvVjgoNNk18LWjIVneAFQkruuMln459CKuUa3BBmVwNkkjPOxqD+3zzFbnubdIlgkMHf2CgnbrXt5Pwa0wI5qwaz4p0bj78YT//7eG6lDM= -after_script: - - cargo doc - - curl http://www.rust-ci.org/artifacts/put?t=$RUSTCI_TOKEN | sh diff --git a/Cargo.toml b/Cargo.toml index d58ff7a..df74b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,4 @@ name = "smtp" version = "0.0.1-pre" -#readme = "README.md" authors = ["Alexis Mousset "] -#tags = ["smtp", "email", "library"] - -[lib] -name = "smtpcommon" -path = "src/lib.rs" diff --git a/README.md b/README.md index 1ff9613..b93ced3 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,20 @@ This library is designed for Rust 0.13.0-nightly (master). Install ------- -Use Cargo to build this library. +If you're using `Cargo`, just add this to your `Cargo.toml`: - cargo build +```toml +[dependencies.smtp] -Todo ----- +git = "https://github.com/amousset/rust-smtp.git" +``` -- RFC compliance -- SSL/TLS support -- AUTH support +Otherwise, just clone this repo and run `cargo build`. + +Documentation +------------- + +The documentation is available on [GitHub pages](amousset.github.io/rust-smtp/smtp/). License ------- diff --git a/examples/client.rs b/examples/client.rs new file mode 100644 index 0000000..0ea968c --- /dev/null +++ b/examples/client.rs @@ -0,0 +1,110 @@ +// 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. + +extern crate smtp; +extern crate getopts; +use std::io::stdin; +use std::io::net::tcp::TcpStream; +use std::string::String; +use std::io::net::ip::Port; +use std::os; +use smtp::client::SmtpClient; +use getopts::{optopt,optflag,getopts,OptGroup,usage}; + +fn sendmail(source_address: String, recipient_addresses: Vec, message: String, server: String, port: Option, my_hostname: Option) { + let mut email_client: SmtpClient = + SmtpClient::new( + server, + port, + my_hostname); + email_client.send_mail::( + source_address, + recipient_addresses, + message + ); +} + +fn print_usage(description: String, _opts: &[OptGroup]) { + println!("{}", usage(description.as_slice(), _opts)); +} + +fn main() { + let args = os::args(); + + let mut args_string = Vec::new(); + for arg in args.iter() { + args_string.push(arg.clone()); + }; + + let program = args[0].clone(); + let description = format!("Usage: {0} [options...] recipients\n\n\ + This program reads a message on standard input until it reaches EOF,\ + then tries to send it using the given paramters.\n\n\ + Example: {0} -r user@example.org user@example.com < message.txt", program); + + let opts = [ + optopt("r", "reverse-path", "set the sender address", "FROM_ADDRESS"), + optopt("p", "port", "set the port to use, default is 25", "PORT"), + optopt("s", "server", "set the server to use, default is localhost", "SERVER"), + optopt("m", "my-hostname", "set the hostname used by the client", "MY_HOSTNAME"), + optflag("h", "help", "print this help menu"), + optflag("v", "verbose", "display the transaction details"), + ]; + let matches = match getopts(args_string.tail(), opts) { + Ok(m) => { m } + Err(f) => { fail!("{}", f) } + }; + if matches.opt_present("h") { + print_usage(description, opts); + return; + } + + let sender = match matches.opt_str("r") { + Some(sender) => sender, + None => { + println!("The sender option is required"); + print_usage(program, opts); + return; + } + }; + + let server = match matches.opt_str("s") { + Some(server) => server, + None => String::from_str("localhost") + }; + + let my_hostname = match matches.opt_str("m") { + Some(my_hostname) => Some(my_hostname), + None => None + }; + + let port = match matches.opt_str("p") { + Some(port) => from_str::(port.as_slice()), + None => None + + }; + + let recipients_str: &str = if !matches.free.is_empty() { + matches.free[0].as_slice() + } else { + print_usage(description, opts); + return; + }; + let mut recipients = Vec::new(); + for recipient in recipients_str.split(' ') { + recipients.push(String::from_str(recipient)) + } + + let mut message = String::new(); + for line in stdin().lines() { + message.push_str(line.unwrap().as_slice()); + } + + sendmail(sender, recipients, message, server, port, my_hostname); +} diff --git a/src/client/connecter.rs b/src/client/connecter.rs new file mode 100644 index 0000000..c105f11 --- /dev/null +++ b/src/client/connecter.rs @@ -0,0 +1,38 @@ +// 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. + +// Taken fron rust-http + +//! TODO + +use std::io::IoResult; +use std::io::net::ip::SocketAddr; +use std::io::net::tcp::TcpStream; + +/// A trait for the concept of opening a stream connected to a IP socket address. +/// +/// Why is this here? So that we can implement things which must make +/// connections in terms of *anything* that can make such a connection rather +/// than in terms of `TcpStream` only. This is handy for testing and for SSL. +pub trait Connecter { + /// TODO + fn connect(host: &str, port: u16) -> IoResult; + /// TODO + fn peer_name(&mut self) -> IoResult; +} + +impl Connecter for TcpStream { + fn connect(host: &str, port: u16) -> IoResult { + TcpStream::connect(host, port) + } + + fn peer_name(&mut self) -> IoResult { + self.peer_name() + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..6027f46 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,514 @@ +// 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 client + +use std::fmt; +use std::fmt::{Show, Formatter}; +use std::str::from_utf8; +use std::result::Result; +use std::string::String; +use std::io::{IoResult, Reader, Writer}; +use std::io::net::ip::Port; + +use common::{get_first_word, unquote_email_address}; +use response::SmtpResponse; +use extension; +use extension::SmtpExtension; +use command; +use command::SmtpCommand; +use common::{SMTP_PORT, CRLF}; +use transaction; +use transaction::TransactionState; + +use client::connecter::Connecter; + +pub mod connecter; + +/// Information about an SMTP server +#[deriving(Clone)] +struct SmtpServerInfo { + /// Server name + name: String, + /// ESMTP features supported by the server + esmtp_features: Option> +} + +impl Show for SmtpServerInfo { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write( + format!("{} with {}", + self.name, + match self.esmtp_features.clone() { + Some(features) => features.to_string(), + None => format!("no supported features") + } + ).as_bytes() + ) + } +} + +impl SmtpServerInfo { + /// Parses supported ESMTP features + /// + /// TODO: Improve parsing + fn parse_esmtp_response(message: String) -> Option> { + let mut esmtp_features = Vec::new(); + for line in message.as_slice().split_str(CRLF) { + match from_str::(line) { + Some(SmtpResponse{code: 250, message: message}) => { + match from_str::(message.unwrap().as_slice()) { + 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: SmtpExtension) -> Result { + match self.esmtp_features.clone() { + Some(esmtp_features) => { + for feature in esmtp_features.iter() { + if keyword.same_extension_as(*feature) { + return Ok(*feature); + } + } + Err({}) + }, + None => 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: String, + /// Port we are connecting on + port: Port, + /// Our hostname for HELO/EHLO commands + my_hostname: String, + /// Information about the server + /// Value is None before HELO/EHLO + server_info: Option, + /// Transaction state, to check the sequence of commands + state: TransactionState +} + +impl SmtpClient { + /// Creates a new SMTP client + pub fn new(host: String, port: Option, my_hostname: Option) -> SmtpClient { + SmtpClient{ + stream: None, + host: host, + port: port.unwrap_or(SMTP_PORT), + my_hostname: my_hostname.unwrap_or(String::from_str("localhost")), + server_info: None, + state: transaction::Unconnected + } + } +} + +// T : String ou String, selon le support +impl SmtpClient { + + /// TODO + fn smtp_fail_if_err(&mut self, response: Result) { + match response { + Err(response) => { + self.smtp_fail::(response) + }, + Ok(_) => {} + } + } + + /// Connects to the configured server + pub fn connect(&mut self) -> Result { + // connect should not be called when the client is already connected + if !self.stream.is_none() { + fail!("The connection is already established"); + } + + // Try to connect + self.stream = match Connecter::connect(self.host.clone().as_slice(), self.port) { + Ok(stream) => Some(stream), + Err(..) => fail!("Cannot connect to the server") + }; + + // Log the connection + info!("Connection established to {}[{}]:{}", self.host, self.stream.clone().unwrap().peer_name().unwrap().ip, self.port); + + match self.get_reply() { + Some(response) => match response.with_code(vec!(220)) { + Ok(response) => { + self.state = transaction::Connected; + Ok(response) + }, + Err(response) => { + Err(response) + } + }, + None => fail!("No banner on {}", self.host) + } + } + + /// Sends an email + pub fn send_mail(&mut self, from_address: String, to_addresses: Vec, message: String) { + let my_hostname = self.my_hostname.clone(); + let mut smtp_result: Result; + + match self.connect() { + Ok(_) => {}, + Err(response) => fail!("Cannot connect to {:s}:{:u}. Server says: {}", + self.host, + self.port, response + ) + } + + // Extended Hello or Hello + match self.ehlo::(my_hostname.clone().to_string()) { + Err(SmtpResponse{code: 550, message: _}) => { + smtp_result = self.helo::(my_hostname.clone()); + self.smtp_fail_if_err::(smtp_result); + }, + Err(response) => { + self.smtp_fail::(response) + } + _ => {} + } + + debug!("Server {}", self.server_info.clone().unwrap()); + + // Checks message encoding according to the server's capability + // TODO : Add an encoding check. + if ! self.server_info.clone().unwrap().supports_feature(extension::EightBitMime).is_ok() { + if ! message.clone().to_string().is_ascii() { + self.smtp_fail::("Server does not accepts UTF-8 strings"); + } + } + + // Mail + smtp_result = self.mail::(from_address.clone(), None); + self.smtp_fail_if_err::(smtp_result); + + // Log the mail command + info!("from=<{}>, size={}, nrcpt={}", from_address, 42u, to_addresses.len()); + + // Recipient + // TODO Return rejected addresses + // TODO Manage the number of recipients + for to_address in to_addresses.iter() { + smtp_result = self.rcpt::(to_address.clone(), None); + self.smtp_fail_if_err::(smtp_result); + } + + // Data + smtp_result = self.data::(); + self.smtp_fail_if_err::(smtp_result); + + // Message content + let sent = self.message::(message); + + if sent.clone().is_err() { + self.smtp_fail::(sent.clone().err().unwrap()) + } + + info!("to=<{}>, status=sent ({})", to_addresses.clone().connect(">, to=<"), sent.clone().ok().unwrap()); + + // Quit + smtp_result = self.quit::(); + self.smtp_fail_if_err::(smtp_result); + } + + /// Sends an SMTP command + // TODO : ensure this is an ASCII string + fn send_command(&mut self, command: SmtpCommand) -> SmtpResponse { + if !self.state.is_command_possible(command.clone()) { + fail!("Bad command sequence"); + } + self.send_and_get_response(format!("{}", command).as_slice()) + } + + /// Sends an email + fn send_message(&mut self, message: String) -> SmtpResponse { + self.send_and_get_response(format!("{}{:s}.", message, CRLF).as_slice()) + } + + /// Sends 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).as_slice()) { // TODO improve this + Ok(..) => debug!("Wrote: {:s}", string), + Err(..) => fail!("Could not write to stream") + } + + match self.get_reply() { + Some(response) => {debug!("Read: {}", response); response}, + None => fail!("No answer on {:s}", self.host) + } + } + + /// Gets the SMTP response + fn get_reply(&mut self) -> Option { + let response = match self.read_to_string() { + Ok(string) => string, + Err(..) => fail!("No answer") + }; + from_str::(response.as_slice()) + } + + /// Closes the connection and fail with a given messgage + fn smtp_fail(&mut self, reason: T) { + let is_connected = self.is_connected::(); + if is_connected { + match self.quit::() { + Ok(..) => {}, + Err(response) => fail!("Failed: {}", response) + } + } + self.close(); + fail!("Failed: {}", reason); + } + + /// Checks if the server is connected + pub fn is_connected(&mut self) -> bool { + self.noop::().is_ok() + } + + /// 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 = transaction::Unconnected; + self.server_info = None; + } + + /// Send a HELO command + pub fn helo(&mut self, my_hostname: String) -> Result { + match self.send_command(command::Hello(my_hostname.clone())).with_code(vec!(250)) { + Ok(response) => { + self.server_info = Some( + SmtpServerInfo{ + name: get_first_word(response.message.clone().unwrap()), + esmtp_features: None + } + ); + self.state = transaction::HelloSent; + Ok(response) + }, + Err(response) => Err(response) + } + } + + /// Sends a EHLO command + pub fn ehlo(&mut self, my_hostname: String) -> Result { + match self.send_command(command::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) { + Ok(response) => { + self.server_info = Some( + SmtpServerInfo{ + name: get_first_word(response.message.clone().unwrap()), + esmtp_features: SmtpServerInfo::parse_esmtp_response(response.message.clone().unwrap()) + } + ); + self.state = transaction::HelloSent; + Ok(response) + }, + Err(response) => Err(response) + } + } + + /// Sends a MAIL command + pub fn mail(&mut self, from_address: String, options: Option>) -> Result { + match self.send_command(command::Mail(unquote_email_address(from_address.to_string()), options)).with_code(vec!(250)) { + Ok(response) => { + self.state = transaction::MailSent; + Ok(response) + }, + Err(response) => { + Err(response) + } + } + } + + /// Sends a RCPT command + pub fn rcpt(&mut self, to_address: String, options: Option>) -> Result { + match self.send_command(command::Recipient(unquote_email_address(to_address.to_string()), options)).with_code(vec!(250)) { + Ok(response) => { + self.state = transaction::RecipientSent; + Ok(response) + }, + Err(response) => { + Err(response) + } + } + } + + /// Sends a DATA command + pub fn data(&mut self) -> Result { + match self.send_command(command::Data).with_code(vec!(354)) { + Ok(response) => { + self.state = transaction::DataSent; + Ok(response) + }, + Err(response) => { + Err(response) + } + } + } + + /// Sends the message content + pub fn message(&mut self, message_content: String) -> Result { + match self.send_message(message_content).with_code(vec!(250)) { + Ok(response) => { + self.state = transaction::HelloSent; + Ok(response) + }, + Err(response) => { + Err(response) + } + } + } + + /// Sends a QUIT command + pub fn quit(&mut self) -> Result { + match self.send_command(command::Quit).with_code(vec!(221)) { + Ok(response) => { + Ok(response) + }, + Err(response) => { + Err(response) + } + } + } + + /// Sends a RSET command + pub fn rset(&mut self) -> Result { + match self.send_command(command::Reset).with_code(vec!(250)) { + Ok(response) => { + if vec!(transaction::MailSent, transaction::RecipientSent, transaction::DataSent).contains(&self.state) { + self.state = transaction::HelloSent; + } + Ok(response) + }, + Err(response) => { + Err(response) + } + } + } + + /// Sends a NOOP commands + pub fn noop(&mut self) -> Result { + self.send_command(command::Noop).with_code(vec!(250)) + } + + /// Sends a VRFY command + pub fn vrfy(&mut self, to_address: String) -> Result { + self.send_command(command::Verify(to_address, None)).with_code(vec!(250)) + } +} + +impl Reader for SmtpClient { + /// Reads a string from the client socket + fn read(&mut self, buf: &mut [u8]) -> IoResult { + self.stream.clone().unwrap().read(buf) + } + + /// Reads a string from the client socket + // TODO: Size of response ?. + fn read_to_string(&mut self) -> IoResult { + let mut buf = [0u8, ..1034]; + + let response = match self.read(buf) { + Ok(bytes_read) => from_utf8(buf.slice_to(bytes_read - 1)).unwrap(), + Err(..) => fail!("Read error") + }; + + return Ok(response.to_string()); + } +} + +impl Writer for SmtpClient { + /// Sends a string on the client socket + fn write(&mut self, buf: &[u8]) -> IoResult<()> { + self.stream.clone().unwrap().write(buf) + } + + /// Sends a string on the client socket + fn write_str(&mut self, string: &str) -> IoResult<()> { + self.stream.clone().unwrap().write_str(string) + } +} + +#[cfg(test)] +mod test { + use super::SmtpServerInfo; + use extension; + + #[test] + fn test_smtp_server_info_fmt() { + assert_eq!(format!("{}", SmtpServerInfo{ + name: String::from_str("name"), + esmtp_features: Some(vec!(extension::EightBitMime)) + }), "name with [8BITMIME]".to_string()); + assert_eq!(format!("{}", SmtpServerInfo{ + name: String::from_str("name"), + esmtp_features: Some(vec!(extension::EightBitMime, extension::Size(42))) + }), "name with [8BITMIME, SIZE=42]".to_string()); + assert_eq!(format!("{}", SmtpServerInfo{ + name: String::from_str("name"), + esmtp_features: None + }), "name with no supported features".to_string()); + } + + #[test] + fn test_smtp_server_info_parse_esmtp_response() { + assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("me\r\n250-8BITMIME\r\n250 SIZE 42")), + Some(vec!(extension::EightBitMime, extension::Size(42)))); + assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("me\r\n250-8BITMIME\r\n250 UNKNON 42")), + Some(vec!(extension::EightBitMime))); + assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("me\r\n250-9BITMIME\r\n250 SIZE a")), + None); + assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("me\r\n250-SIZE 42\r\n250 SIZE 43")), + Some(vec!(extension::Size(42), extension::Size(43)))); + assert_eq!(SmtpServerInfo::parse_esmtp_response(String::from_str("")), + None); + } + + #[test] + fn test_smtp_server_info_supports_feature() { + assert_eq!(SmtpServerInfo{ + name: String::from_str("name"), + esmtp_features: Some(vec!(extension::EightBitMime)) + }.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime)); + assert_eq!(SmtpServerInfo{ + name: String::from_str("name"), + esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime)) + }.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime)); + assert_eq!(SmtpServerInfo{ + name: String::from_str("name"), + esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime)) + }.supports_feature(extension::Size(0)), Ok(extension::Size(42))); + assert!(SmtpServerInfo{ + name: String::from_str("name"), + esmtp_features: Some(vec!(extension::EightBitMime)) + }.supports_feature(extension::Size(42)).is_err()); + } +} diff --git a/src/smtpcommon/command.rs b/src/command.rs similarity index 97% rename from src/smtpcommon/command.rs rename to src/command.rs index 328b9c8..56bfc6d 100644 --- a/src/smtpcommon/command.rs +++ b/src/command.rs @@ -7,7 +7,9 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -//! Represents a complete SMTP command, ready to be sent to a server +#![unstable] + +//! Represents a valid complete SMTP command, ready to be sent to a server use std::fmt::{Show, Formatter, Result}; /// Supported SMTP commands @@ -80,7 +82,7 @@ impl Show for SmtpCommand { #[cfg(test)] mod test { - use smtpcommon::command; + use command; #[test] fn test_command_fmt() { diff --git a/src/smtpcommon/common.rs b/src/common.rs similarity index 98% rename from src/smtpcommon/common.rs rename to src/common.rs index cc5f22e..3def041 100644 --- a/src/smtpcommon/common.rs +++ b/src/common.rs @@ -103,7 +103,7 @@ mod test { assert_eq!(super::get_first_word("".to_string()), "".to_string()); assert_eq!(super::get_first_word("\r\n".to_string()), "".to_string()); assert_eq!(super::get_first_word("a\r\n".to_string()), "a".to_string()); - // Manage cases of empty line, spaces at the beginning, ... + // Manage cases of empty line, spaces at the beginning //assert_eq!(super::get_first_word(" a".to_string()), "a".to_string()); //assert_eq!(super::get_first_word("\r\n a".to_string()), "a".to_string()); assert_eq!(super::get_first_word(" \r\n".to_string()), "".to_string()); diff --git a/src/smtpcommon/extension.rs b/src/extension.rs similarity index 97% rename from src/smtpcommon/extension.rs rename to src/extension.rs index ef88bc4..c7029bc 100644 --- a/src/smtpcommon/extension.rs +++ b/src/extension.rs @@ -9,6 +9,8 @@ //! SMTP commands and ESMTP features library +#![unstable] + use std::from_str::FromStr; use std::fmt::{Show, Formatter, Result}; @@ -74,8 +76,8 @@ impl SmtpExtension { #[cfg(test)] mod test { - use smtpcommon::extension; - use smtpcommon::extension::SmtpExtension; + use extension; + use extension::SmtpExtension; #[test] fn test_extension_same_extension_as() { diff --git a/src/lib.rs b/src/lib.rs index 48ca459..592e000 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,15 +27,15 @@ //! //! ## Usage //! -//! ```tmp +//! ```ignore //! extern crate smtp; //! use std::io::net::tcp::TcpStream; -//! use smtp::client::SmtpClient; +//! use smtp::smtpc::client::SmtpClient; //! use std::string::String; //! -//! let mut email_client: SmtpClient = +//! let mut email_client: SmtpClient = //! SmtpClient::new(String::from_str("localhost"), None, None); -//! email_client.send_mail( +//! email_client.send_mail::( //! String::from_str("user@example.com"), //! vec!(String::from_str("user@example.org")), //! String::from_str("Test email") @@ -48,7 +48,8 @@ #![desc = "Rust SMTP library"] #![comment = "Simple SMTP client and library"] #![license = "MIT/ASL2"] -#![doc(html_root_url = "http://www.rust-ci.org/amousset/rust-smtp/doc")] +#![doc(html_root_url = "http://amousset.github.io/rust-smtp/smtp/")] +#![experimental] #![feature(macro_rules)] #![feature(phase)] @@ -61,5 +62,9 @@ #![feature(phase)] #[phase(plugin, link)] extern crate log; -pub mod smtpcommon; -//pub mod smtpc; +pub mod client; +pub mod command; +pub mod extension; +pub mod response; +pub mod transaction; +pub mod common; diff --git a/src/smtpcommon/response.rs b/src/response.rs similarity index 97% rename from src/smtpcommon/response.rs rename to src/response.rs index 6262b8a..f257dcb 100644 --- a/src/smtpcommon/response.rs +++ b/src/response.rs @@ -9,11 +9,11 @@ //! SMTP responses, contaiing a mandatory return code, and an optional text message -//extern crate common; +#![unstable] use std::from_str::FromStr; use std::fmt::{Show, Formatter, Result}; -use smtpcommon::common::remove_trailing_crlf; +use common::remove_trailing_crlf; use std::result; /// Contains an SMTP reply, with separed code and message @@ -86,7 +86,7 @@ impl SmtpResponse { #[cfg(test)] mod test { - use smtpcommon::response::SmtpResponse; + use response::SmtpResponse; #[test] fn test_response_fmt() { diff --git a/src/smtpcommon/mod.rs b/src/smtpcommon/mod.rs deleted file mode 100644 index 5f2cea6..0000000 --- a/src/smtpcommon/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! SMTP library - -pub mod common; -pub mod command; -pub mod extension; -pub mod response; -pub mod transaction; diff --git a/src/smtpcommon/transaction.rs b/src/transaction.rs similarity index 98% rename from src/smtpcommon/transaction.rs rename to src/transaction.rs index 9ba949d..503011c 100644 --- a/src/smtpcommon/transaction.rs +++ b/src/transaction.rs @@ -11,8 +11,8 @@ use std::fmt; use std::fmt::{Show, Formatter}; -use smtpcommon::command; -use smtpcommon::command::SmtpCommand; +use command; +use command::SmtpCommand; /// Contains the state of the current transaction #[deriving(PartialEq,Eq,Clone)] @@ -102,7 +102,7 @@ impl TransactionState { #[cfg(test)] mod test { - use smtpcommon::command; + use command; #[test] fn test_transaction_state_is_command_possible() { diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..0455e50 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,5 @@ +#[test] + +fn foo() { + assert!(true); +}