From c33abcb1aaaa180517384e27794b3c5edd25e552 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Sun, 13 Jul 2014 18:41:35 +0200 Subject: [PATCH] Reorganization --- .gitignore | 5 +- Cargo.toml | 16 + Makefile | 66 ---- README.rst => README.md | 14 +- examples/client.rs | 107 ------- src/client.rs | 557 ---------------------------------- src/common.rs | 100 ------ src/smtp.rs | 429 -------------------------- src/smtpc/lib.rs | 0 src/smtpcommon/command.rs | 93 ++++++ src/smtpcommon/common.rs | 112 +++++++ src/smtpcommon/extension.rs | 103 +++++++ src/{ => smtpcommon}/lib.rs | 28 +- src/smtpcommon/response.rs | 143 +++++++++ src/smtpcommon/transaction.rs | 121 ++++++++ 15 files changed, 605 insertions(+), 1289 deletions(-) create mode 100644 Cargo.toml delete mode 100644 Makefile rename README.rst => README.md (72%) delete mode 100644 examples/client.rs delete mode 100644 src/client.rs delete mode 100644 src/common.rs delete mode 100644 src/smtp.rs create mode 100644 src/smtpc/lib.rs create mode 100644 src/smtpcommon/command.rs create mode 100644 src/smtpcommon/common.rs create mode 100644 src/smtpcommon/extension.rs rename src/{ => smtpcommon}/lib.rs (76%) create mode 100644 src/smtpcommon/response.rs create mode 100644 src/smtpcommon/transaction.rs diff --git a/.gitignore b/.gitignore index db42833..bdcfb9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ target/ -bin/ +TAGS doc/ -*~ -*.sh -.directory diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a22b6ef --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] + +name = "rust-smtp" +version = "0.0.1" +#readme = "README.md" +authors = [ "Alexis Mousset " ] +#tags = ["smtp", "email", "library"] + +[[lib]] +name = "smtpcommon" +path = "src/smtpcommon/lib.rs" + +#[[lib]] +#name = "smtpc" +#path = "src/smtpc/lib.rs" + diff --git a/Makefile b/Makefile deleted file mode 100644 index 4a2b199..0000000 --- a/Makefile +++ /dev/null @@ -1,66 +0,0 @@ -RUSTC ?= rustc -RUSTDOC ?= rustdoc -RUSTC_FLAGS ?= -g -SHELL = /bin/sh - -BIN_DIR = bin -DOC_DIR = doc -SRC_DIR = src -TARGET_DIR = target -EXAMPLES_DIR = examples -LIB = src/lib.rs - -EXAMPLE_FILES := $(EXAMPLES_DIR)/*.rs -SOURCE_FILES := $(shell test -e src/ && find src -type f) - -TARGET := $(shell rustc --version | awk "/host:/ { print \$$2 }") -TARGET_LIB_DIR := $(TARGET_DIR)/$(TARGET)/lib - -RLIB_FILE := $(shell rustc --crate-type=rlib --crate-file-name "src/lib.rs" 2> /dev/null) -RLIB := $(TARGET_LIB_DIR)/$(RLIB_FILE) -DYLIB_FILE := $(shell rustc --crate-type=dylib --crate-file-name "src/lib.rs" 2> /dev/null) -DYLIB := $(TARGET_LIB_DIR)/$(DYLIB_FILE) - -all: lib - -lib: rlib dylib - -rlib: $(RLIB) - -$(RLIB): $(SOURCE_FILES) | $(LIB) $(TARGET_LIB_DIR) - $(RUSTC) --target $(TARGET) $(RUSTC_FLAGS) --crate-type=rlib $(LIB) --out-dir $(TARGET_LIB_DIR) - -dylib: $(DYLIB) - -$(DYLIB): $(SOURCE_FILES) | $(LIB) $(TARGET_LIB_DIR) - $(RUSTC) --target $(TARGET) $(RUSTC_FLAGS) --crate-type=dylib $(LIB) --out-dir $(TARGET_LIB_DIR) - -$(TARGET_LIB_DIR): - mkdir -p $(TARGET_LIB_DIR) - -test: $(BIN_DIR)/test - -$(BIN_DIR)/test: $(SOURCE_FILES) | rlib $(BIN_DIR) - $(RUSTC) --target $(TARGET) $(RUSTC_FLAGS) --test $(LIB) -o $(BIN_DIR)/test -L $(TARGET_LIB_DIR) - -doc: $(SOURCE_FILES) - mkdir -p $(DOC_DIR) - $(RUSTDOC) $(LIB) -L $(TARGET_LIB_DIR) -o $(DOC_DIR) - -examples: $(EXAMPLE_FILES) - -$(EXAMPLE_FILES): lib | $(BIN_DIR) - $(RUSTC) --target $(TARGET) $(RUSTC_FLAGS) $@ -L $(TARGET_LIB_DIR) --out-dir $(BIN_DIR) - -$(BIN_DIR): - mkdir -p $(BIN_DIR) - -check: test - $(BIN_DIR)/test --test - -clean: - rm -rf $(TARGET_DIR) - rm -rf $(DOC_DIR) - rm -rf $(BIN_DIR) - -.PHONY: all lib rlib dylib test doc examples clean check diff --git a/README.rst b/README.md similarity index 72% rename from README.rst rename to README.md index 7ce7f3d..f34f0e6 100644 --- a/README.rst +++ b/README.md @@ -9,22 +9,14 @@ This library implements an SMTP client, and maybe later a simple SMTP server. Rust versions ------------- -This library is designed for Rust 0.11-pre (master). +This library is designed for Rust 0.11.0-nightly (master). Install ------ -Build the library: +Use Cargo to build this library. - make - -To build the example command-line client code: - - make examples - -To run the example's help: - - ./build/client -h + cargo build Todo ---- diff --git a/examples/client.rs b/examples/client.rs deleted file mode 100644 index a67d108..0000000 --- a/examples/client.rs +++ /dev/null @@ -1,107 +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. - -#![crate_id = "client"] - -extern crate smtp; -extern crate getopts; -use std::io::stdin; -use std::io::net::tcp::TcpStream; -use std::strbuf::StrBuf; -use std::io::net::ip::Port; -use std::os; -use smtp::client::SmtpClient; -use getopts::{optopt,optflag,getopts,OptGroup,usage}; - -fn sendmail(source_address: StrBuf, recipient_addresses: Vec, message: StrBuf, server: StrBuf, 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: &str, _opts: &[OptGroup]) { - println!("{}", usage(description, _opts)); -} - -fn main() { - let args = os::args(); - - let program = args.get(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.tail(), opts) { - Ok(m) => { m } - Err(f) => { fail!(f.to_err_msg()) } - }; - if matches.opt_present("h") { - print_usage(description, opts); - return; - } - - let sender = match matches.opt_str("r") { - Some(sender) => StrBuf::from_str(sender), - None => { - println!("The sender option is required"); - print_usage(program, opts); - return; - } - }; - - let server = match matches.opt_str("s") { - Some(server) => StrBuf::from_str(server), - None => StrBuf::from_str("localhost") - }; - - let my_hostname = match matches.opt_str("m") { - Some(my_hostname) => Some(StrBuf::from_str(my_hostname)), - None => None - }; - - let port = match matches.opt_str("p") { - Some(port) => from_str::(port), - None => None - - }; - - let recipients_str: &str = if !matches.free.is_empty() { - (*matches.free.get(0)).clone() - } else { - print_usage(description, opts); - return; - }; - let mut recipients = Vec::new(); - for recipient in recipients_str.split(' ') { - recipients.push(StrBuf::from_str(recipient)) - } - - let mut message = StrBuf::new(); - for line in stdin().lines() { - message = message.append(line.unwrap().to_str()); - } - - sendmail(sender, recipients, message, server, port, my_hostname); -} diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 4e766cb..0000000 --- a/src/client.rs +++ /dev/null @@ -1,557 +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 client - -use std::fmt; -use std::fmt::{Show, Formatter}; -use std::str::from_utf8; -use std::result::Result; -use std::strbuf::StrBuf; -use std::io::{IoResult, Reader, Writer}; -use std::io::net::ip::Port; -use std::io::net::tcp::TcpStream; - -use common::{get_first_word, unquote_email_address}; -use smtp::response::SmtpResponse; -use smtp::extension; -use smtp::extension::SmtpExtension; -use smtp::command; -use smtp::command::SmtpCommand; -use smtp::{SMTP_PORT, CRLF}; - -/// Information about an SMTP server -#[deriving(Clone)] -struct SmtpServerInfo { - /// Server name - name: T, - /// 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_str(), - None => format!("no supported features") - } - ).as_bytes() - ) - } -} - -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.as_slice().split_str(CRLF) { - match from_str::>(line) { - Some(SmtpResponse{code: 250, message: message}) => { - match from_str::(message.unwrap().into_owned()) { - 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({}) - } - } -} - -/// Contains the state of the current transaction -#[deriving(Eq,Clone)] -enum SmtpClientState { - /// The server is unconnected - Unconnected, - /// The connection was successful and the banner was received - Connected, - /// An HELO or EHLO was successful - HeloSent, - /// A MAIL command was successful send - MailSent, - /// At least one RCPT command was sucessful - RcptSent, - /// A DATA command was successful - DataSent -} - -macro_rules! check_state_in( - ($expected_states:expr) => ( - if ! $expected_states.contains(&self.state) { - fail!("Bad sequence of commands."); - } - ); -) - -macro_rules! check_state_not_in( - ($expected_states:expr) => ( - if $expected_states.contains(&self.state) { - fail!("Bad sequence of commands."); - } - ); -) - -macro_rules! smtp_fail_if_err( - ($response:expr) => ( - match $response { - Err(response) => { - self.smtp_fail(response) - }, - Ok(_) => {} - } - ); -) - -/// 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, - /// Port we are connecting on - port: Port, - /// Our hostname for HELO/EHLO commands - my_hostname: T, - /// Information about the server - /// Value is None before HELO/EHLO - server_info: Option>, - /// Transaction state, to check the sequence of commands - state: SmtpClientState -} - -impl SmtpClient { - /// Creates a new SMTP client - pub fn new(host: StrBuf, port: Option, my_hostname: Option) -> SmtpClient { - SmtpClient{ - stream: None, - host: host, - port: port.unwrap_or(SMTP_PORT), - my_hostname: my_hostname.unwrap_or(StrBuf::from_str("localhost")), - server_info: None, - state: Unconnected - } - } -} - -impl SmtpClient { - /// Connects to the configured server - pub fn connect(&mut self) -> IoResult { - // 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 - TcpStream::connect(self.host.clone().as_slice(), self.port) - - - } - - /// bla - pub fn get_banner(&mut self) -> Result, SmtpResponse> { - match self.get_reply() { - Some(response) => match response.with_code(vec!(220)) { - Ok(response) => { - self.state = 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: StrBuf, to_addresses: Vec, message: StrBuf) { - let my_hostname = self.my_hostname.clone(); - - // Connect - match self.connect() { - Ok(stream) => self.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_banner() { - 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()) { - Err(SmtpResponse{code: 550, message: _}) => { - smtp_fail_if_err!(self.helo(my_hostname.clone())) - }, - Err(response) => { - self.smtp_fail(response) - } - _ => {} - } - - debug!("Server {:s}", self.server_info.clone().unwrap().to_str()); - - // 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().into_owned().is_ascii() { - self.smtp_fail("Server does not accepts UTF-8 strings"); - } - } - - // Mail - smtp_fail_if_err!(self.mail(from_address.clone(), None)); - - // Log the mail command - info!("from=<{}>, size={}, nrcpt={}", from_address, 42, to_addresses.len()); - - // 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)); - } - - // Data - smtp_fail_if_err!(self.data()); - - // 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_fail_if_err!(self.quit()); - } -} - -impl SmtpClient { - /// Sends an SMTP command - // TODO : ensure this is an ASCII string - fn send_command(&mut self, command: SmtpCommand) -> SmtpResponse { - self.send_and_get_response(format!("{}", command)) - } - - /// Sends an email - fn send_message(&mut self, message: StrBuf) -> SmtpResponse { - self.send_and_get_response(format!("{}{:s}.", message, CRLF)) - } - - /// 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)) { - Ok(..) => debug!("Wrote: {:s}", string), - Err(..) => fail!("Could not write to stream") - } - - match self.get_reply() { - Some(response) => {debug!("Read: {:s}", response.to_str()); 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_str() { - Ok(string) => string, - Err(..) => fail!("No answer") - }; - from_str::>(response) - } - - /// Closes the connection and fail with a given messgage - fn smtp_fail(&mut self, reason: T) { - if self.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 = 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(command::Hello(my_hostname.clone())).with_code(vec!(250)) { - Ok(response) => { - self.server_info = Some( - SmtpServerInfo{ - name: StrBuf::from_str(get_first_word(response.message.clone().unwrap().into_owned())), - esmtp_features: None - } - ); - self.state = HeloSent; - Ok(response) - }, - Err(response) => Err(response) - } - } - - /// Sends a EHLO command - pub fn ehlo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { - check_state_not_in!(vec!(Unconnected)); - - match self.send_command(command::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) { - Ok(response) => { - self.server_info = Some( - SmtpServerInfo{ - name: StrBuf::from_str(get_first_word(response.message.clone().unwrap().to_owned())), - esmtp_features: SmtpServerInfo::parse_esmtp_response(response.message.clone().unwrap()) - } - ); - self.state = HeloSent; - Ok(response) - }, - Err(response) => Err(response) - } - } - - /// Sends a MAIL command - pub fn mail(&mut self, from_address: StrBuf, options: Option>) -> Result, SmtpResponse> { - check_state_in!(vec!(HeloSent)); - - match self.send_command(command::Mail(StrBuf::from_str(unquote_email_address(from_address.to_owned())), options)).with_code(vec!(250)) { - Ok(response) => { - self.state = MailSent; - Ok(response) - }, - Err(response) => { - Err(response) - } - } - } - - /// Sends a RCPT command - pub fn rcpt(&mut self, to_address: StrBuf, options: Option>) -> Result, SmtpResponse> { - check_state_in!(vec!(MailSent, RcptSent)); - - match self.send_command(command::Recipient(StrBuf::from_str(unquote_email_address(to_address.to_owned())), options)).with_code(vec!(250)) { - Ok(response) => { - self.state = RcptSent; - Ok(response) - }, - Err(response) => { - Err(response) - } - } - } - - /// Sends a DATA command - pub fn data(&mut self) -> Result, SmtpResponse> { - check_state_in!(vec!(RcptSent)); - - match self.send_command(command::Data).with_code(vec!(354)) { - Ok(response) => { - self.state = DataSent; - Ok(response) - }, - Err(response) => { - Err(response) - } - } - } - - /// Sends the message content - pub fn message(&mut self, message_content: StrBuf) -> Result, SmtpResponse> { - check_state_in!(vec!(DataSent)); - - match self.send_message(message_content).with_code(vec!(250)) { - Ok(response) => { - self.state = HeloSent; - Ok(response) - }, - Err(response) => { - Err(response) - } - } - } - - /// Sends a QUIT command - pub fn quit(&mut self) -> Result, SmtpResponse> { - check_state_not_in!(vec!(Unconnected)); - 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, SmtpResponse> { - check_state_not_in!(vec!(Unconnected)); - match self.send_command(command::Reset).with_code(vec!(250)) { - Ok(response) => { - if vec!(MailSent, RcptSent, DataSent).contains(&self.state) { - self.state = HeloSent; - } - Ok(response) - }, - Err(response) => { - Err(response) - } - } - } - - /// Sends a NOOP commands - pub fn noop(&mut self) -> Result, SmtpResponse> { - check_state_not_in!(vec!(Unconnected)); - self.send_command(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(command::Verify(to_address)).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_str(&mut self) -> IoResult<~str> { - let mut buf = [0u8, ..1000]; - - 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_owned()); - } -} - -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 smtp::extension; - - #[test] - fn test_smtp_server_info_fmt() { - assert_eq!(format!("{}", SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(extension::EightBitMime)) - }), "name with [8BITMIME]".to_owned()); - assert_eq!(format!("{}", SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(extension::EightBitMime, extension::Size(42))) - }), "name with [8BITMIME, SIZE=42]".to_owned()); - assert_eq!(format!("{}", SmtpServerInfo{ - name: "name", - esmtp_features: None - }), "name with no supported features".to_owned()); - } - - #[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!(extension::EightBitMime, extension::Size(42)))); - assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 UNKNON 42"), - Some(vec!(extension::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!(extension::Size(42), extension::Size(43)))); - assert_eq!(SmtpServerInfo::parse_esmtp_response(""), - None); - } - - #[test] - fn test_smtp_server_info_supports_feature() { - assert_eq!(SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(extension::EightBitMime)) - }.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime)); - assert_eq!(SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime)) - }.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime)); - assert_eq!(SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime)) - }.supports_feature(extension::Size(0)), Ok(extension::Size(42))); - assert!(SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(extension::EightBitMime)) - }.supports_feature(extension::Size(42)).is_err()); - } -} diff --git a/src/common.rs b/src/common.rs deleted file mode 100644 index 21322d5..0000000 --- a/src/common.rs +++ /dev/null @@ -1,100 +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. - -//! Common definitions for SMTP -//! -//! Needs to be organized later - -use smtp::CRLF; - -/// Adds quotes to emails if needed -pub fn quote_email_address(address: ~str) -> ~str { - match address.len() { - 0..1 => format!("<{:s}>", address), - _ => match (address.slice_to(1), address.slice_from(address.len() - 1)) { - ("<", ">") => address, - _ => format!("<{:s}>", address) - } - } -} - -/// Removes quotes from emails if needed -pub fn unquote_email_address(address: ~str) -> ~str { - match address.len() { - 0..1 => address, - _ => match (address.slice_to(1), address.slice_from(address.len() - 1)) { - ("<", ">") => address.slice(1, address.len() - 1).to_owned(), - _ => address - } - } -} - -/// Removes the trailing line return at the end of a string -pub fn remove_trailing_crlf(string: ~str) -> ~str { - if string.len() > 1 && string.slice_from(string.len() - 2) == CRLF { - string.slice_to(string.len() - 2).to_owned() - } else if string.len() > 0 && string.slice_from(string.len() - 1) == "\r" { - string.slice_to(string.len() - 1).to_owned() - } else { - string - } -} - -/// 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] - fn test_quote_email_address() { - assert_eq!(super::quote_email_address("address".to_owned()), "
".to_owned()); - assert_eq!(super::quote_email_address("
".to_owned()), "
".to_owned()); - assert_eq!(super::quote_email_address("a".to_owned()), "".to_owned()); - assert_eq!(super::quote_email_address("".to_owned()), "<>".to_owned()); - } - - #[test] - fn test_unquote_email_address() { - assert_eq!(super::unquote_email_address("
".to_owned()), "address".to_owned()); - assert_eq!(super::unquote_email_address("address".to_owned()), "address".to_owned()); - assert_eq!(super::unquote_email_address("".to_owned()), "".to_owned()); - assert_eq!(super::unquote_email_address("a".to_owned()), "a".to_owned()); - assert_eq!(super::unquote_email_address("".to_owned()), "".to_owned()); - } - - #[test] - fn test_remove_trailing_crlf() { - assert_eq!(super::remove_trailing_crlf("word".to_owned()), "word".to_owned()); - assert_eq!(super::remove_trailing_crlf("word\r\n".to_owned()), "word".to_owned()); - assert_eq!(super::remove_trailing_crlf("word\r\n ".to_owned()), "word\r\n ".to_owned()); - assert_eq!(super::remove_trailing_crlf("word\r".to_owned()), "word".to_owned()); - assert_eq!(super::remove_trailing_crlf("\r\n".to_owned()), "".to_owned()); - assert_eq!(super::remove_trailing_crlf("\r".to_owned()), "".to_owned()); - assert_eq!(super::remove_trailing_crlf("a".to_owned()), "a".to_owned()); - assert_eq!(super::remove_trailing_crlf("".to_owned()), "".to_owned()); - } - - #[test] - fn test_get_first_word() { - assert_eq!(super::get_first_word("first word".to_owned()), "first".to_owned()); - assert_eq!(super::get_first_word("first word\r\ntest".to_owned()), "first".to_owned()); - assert_eq!(super::get_first_word("first".to_owned()), "first".to_owned()); - assert_eq!(super::get_first_word("".to_owned()), "".to_owned()); - assert_eq!(super::get_first_word("\r\n".to_owned()), "".to_owned()); - assert_eq!(super::get_first_word("a\r\n".to_owned()), "a".to_owned()); - // Manage cases of empty line, spaces at the beginning, ... - //assert_eq!(super::get_first_word(" a".to_owned()), "a".to_owned()); - //assert_eq!(super::get_first_word("\r\n a".to_owned()), "a".to_owned()); - assert_eq!(super::get_first_word(" \r\n".to_owned()), "".to_owned()); - assert_eq!(super::get_first_word("\r\n ".to_owned()), "".to_owned()); - } -} diff --git a/src/smtp.rs b/src/smtp.rs deleted file mode 100644 index e60cd2b..0000000 --- a/src/smtp.rs +++ /dev/null @@ -1,429 +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::io::net::ip::Port; - -/// Default SMTP port -pub static SMTP_PORT: Port = 25; -//pub static SMTPS_PORT: Port = 465; -//pub static SUBMISSION_PORT: Port = 587; - -/// The word separator for SMTP transactions -pub static SP: &'static str = " "; - -/// The line ending for SMTP transactions -pub static CRLF: &'static str = "\r\n"; - -/// A module -pub mod command { - use std::fmt::{Show, Formatter, Result}; - - /// 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 { - /// A fake command - Connect, - /// 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.write(match *self { - Connect => "CONNECT".to_owned(), - 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()) - } - } -} - -/// This is a module -pub mod extension { - use std::from_str::FromStr; - use std::fmt::{Show, Formatter, Result}; - - /// Supported ESMTP keywords - #[deriving(Eq,Clone)] - pub enum SmtpExtension { - /// 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 SmtpExtension { - fn fmt(&self, f: &mut Formatter) -> Result { - f.write( - match self { - &EightBitMime => "8BITMIME".to_owned(), - &Size(ref size) => format!("SIZE={}", size) - }.as_bytes() - ) - } - } - - impl FromStr for SmtpExtension { - 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 SmtpExtension { - /// Checks if the ESMTP keyword is the same - pub fn same_extension_as(&self, other: SmtpExtension) -> bool { - if *self == other { - return true; - } - match (*self, other) { - (Size(_), Size(_)) => true, - _ => false - } - } - } -} -/// This is a module -pub mod response { - use std::from_str::FromStr; - use std::fmt::{Show, Formatter, Result}; - use common::remove_trailing_crlf; - use std::result; - - /// 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.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) - } - } - } -} - -/// a module -pub mod transaction_state { - use std::fmt; - use std::fmt::{Show, Formatter}; - use super::command; - use super::command::SmtpCommand; - - /// Contains the state of the current transaction - #[deriving(Eq,Clone)] - pub enum TransactionState { - /// The connection was successful and the banner was received - OutOfTransaction, - /// An HELO or EHLO was successful - HelloSent, - /// A MAIL command was successful send - MailSent, - /// At least one RCPT command was sucessful - RecipientSent, - /// A DATA command was successful - DataSent - } - - impl Show for TransactionState { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.write( - match *self { - OutOfTransaction => "OutOfTransaction", - HelloSent => "HelloSent", - MailSent => "MailSent", - RecipientSent => "RecipientSent", - DataSent => "DataSent" - }.as_bytes() - ) - } - } - - impl TransactionState { - /// bla bla - pub fn is_command_possible(&self, command: SmtpCommand) -> bool { - match (*self, command) { - // Only the message content can be sent in this state - (DataSent, _) => false, - // Commands that can be issued everytime - (_, command::ExtendedHello(_)) => true, - (_, command::Hello(_)) => true, - (_, command::Reset) => true, - (_, command::Verify(_)) => true, - (_, command::Expand(_)) => true, - (_, command::Help(_)) => true, - (_, command::Noop) => true, - (_, command::Quit) => true, - // Commands that require a particular state - (HelloSent, command::Mail(_, _)) => true, - (MailSent, command::Recipient(_, _)) => true, - (RecipientSent, command::Recipient(_, _)) => true, - (RecipientSent, command::Data) => true, - // Everything else - (_, _) => false - } - } - - /// a method - pub fn next_state(&mut self, command: SmtpCommand) -> Option { - match (*self, command) { - (DataSent, _) => None, - // Commands that can be issued everytime - (_, command::ExtendedHello(_)) => Some(HelloSent), - (_, command::Hello(_)) => Some(HelloSent), - (_, command::Reset) => Some(OutOfTransaction), - (state, command::Verify(_)) => Some(state), - (state, command::Expand(_)) => Some(state), - (state, command::Help(_)) => Some(state), - (state, command::Noop) => Some(state), - (_, command::Quit) => Some(OutOfTransaction), - // Commands that require a particular state - (HelloSent, command::Mail(_, _)) => Some(MailSent), - (MailSent, command::Recipient(_, _)) => Some(RecipientSent), - (RecipientSent, command::Recipient(_, _)) => Some(RecipientSent), - (RecipientSent, command::Data) => Some(DataSent), - // Everything else - (_, _) => None - } - } - } -} - -#[cfg(test)] -mod test { - use super::response::SmtpResponse; - use super::extension; - use super::extension::SmtpExtension; - use super::command; - use super::command::SmtpCommand; - use super::transaction_state; - - #[test] - fn test_command_fmt() { - let noop: SmtpCommand = command::Noop; - assert_eq!(format!("{}", noop), "NOOP".to_owned()); - assert_eq!(format!("{}", command::ExtendedHello("me")), "EHLO me".to_owned()); - assert_eq!(format!("{}", - command::Mail("test", Some(vec!("option")))), "MAIL FROM: option".to_owned() - ); - } - - #[test] - fn test_extension_same_extension_as() { - assert_eq!(extension::EightBitMime.same_extension_as(extension::EightBitMime), true); - assert_eq!(extension::Size(42).same_extension_as(extension::Size(42)), true); - assert_eq!(extension::Size(42).same_extension_as(extension::Size(43)), true); - assert_eq!(extension::Size(42).same_extension_as(extension::EightBitMime), false); - } - - #[test] - fn test_extension_fmt() { - assert_eq!(format!("{}", extension::EightBitMime), "8BITMIME".to_owned()); - assert_eq!(format!("{}", extension::Size(42)), "SIZE=42".to_owned()); - } - - #[test] - fn test_extension_from_str() { - assert_eq!(from_str::("8BITMIME"), Some(extension::EightBitMime)); - assert_eq!(from_str::("SIZE 42"), Some(extension::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_response_fmt() { - assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message")}), "200 message".to_owned()); - } - - #[test] - fn test_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_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")})); - } - - #[test] - fn test_transaction_state_is_command_possible() { - let noop: SmtpCommand = command::Noop; - assert!(transaction_state::OutOfTransaction.is_command_possible(noop.clone())); - assert!(! transaction_state::DataSent.is_command_possible(noop)); - assert!(transaction_state::HelloSent.is_command_possible(command::Mail("", None))); - assert!(! transaction_state::MailSent.is_command_possible(command::Mail("", None))); - } - - #[test] - fn test_transaction_state_next_state() { - let noop: SmtpCommand = command::Noop; - assert_eq!(transaction_state::MailSent.next_state(noop), Some(transaction_state::MailSent)); - assert_eq!(transaction_state::HelloSent.next_state(command::Mail("", None)), Some(transaction_state::MailSent)); - assert_eq!(transaction_state::MailSent.next_state(command::Mail("", None)), None); - } -} diff --git a/src/smtpc/lib.rs b/src/smtpc/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/smtpcommon/command.rs b/src/smtpcommon/command.rs new file mode 100644 index 0000000..2ae100a --- /dev/null +++ b/src/smtpcommon/command.rs @@ -0,0 +1,93 @@ +// 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. + +//! Represents a complete SMTP command, ready to be sent to a server + +use std::fmt::{Show, Formatter, Result}; +/// 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(PartialEq,Eq,Clone)] +pub enum SmtpCommand { + /// A fake command to represent the connection step + Connect, + /// Extended Hello command + ExtendedHello(String), + /// Hello command + Hello(String), + /// Mail command, takes optionnal options + Mail(String, Option>), + /// Recipient command, takes optionnal options + Recipient(String, Option>), + /// Data command + Data, + /// Reset command + Reset, + /// Verify command, takes optionnal options + Verify(String, Option>), + /// Expand command, takes optionnal options + Expand(String, Option>), + /// 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.write(match *self { + Connect => "CONNECT".to_string(), + 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_string(), + Reset => "RSET".to_string(), + Verify(ref address, None) => + format!("VRFY {}", address.clone()), + Verify(ref address, Some(ref options)) => + format!("VRFY {} {}", address.clone(), options.connect(" ")), + Expand(ref address, None) => + format!("EXPN {}", address.clone()), + Expand(ref address, Some(ref options)) => + format!("EXPN {} {}", address.clone(), options.connect(" ")), + Help(None) => "HELP".to_string(), + Help(Some(ref argument)) => + format!("HELP {}", argument.clone()), + Noop => "NOOP".to_string(), + Quit => "QUIT".to_string(), + }.as_bytes()) + } +} + +#[cfg(test)] +mod test { + use command; + + #[test] + fn test_command_fmt() { + assert_eq!(format!("{}", command::Noop), "NOOP".to_string()); + assert_eq!(format!("{}", command::ExtendedHello("this".to_string())), "EHLO this".to_string()); + assert_eq!(format!("{}", + command::Mail("test".to_string(), Some(vec!("option".to_string())))), "MAIL FROM: option".to_string() + ); + } +} diff --git a/src/smtpcommon/common.rs b/src/smtpcommon/common.rs new file mode 100644 index 0000000..16e7a43 --- /dev/null +++ b/src/smtpcommon/common.rs @@ -0,0 +1,112 @@ +// 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. + +//! Contains mixed-up tools for SMTP +//! +//! TODO : Clean up and split this file + +use std::io::net::ip::Port; +use std::string::String; + +/// Default SMTP port +pub static SMTP_PORT: Port = 25; +//pub static SMTPS_PORT: Port = 465; +//pub static SUBMISSION_PORT: Port = 587; + +/// The word separator for SMTP transactions +pub static SP: &'static str = " "; + +/// The line ending for SMTP transactions +pub static CRLF: &'static str = "\r\n"; + +/// Adds quotes to emails if needed +pub fn quote_email_address(address: String) -> String { + match address.len() { + 0..1 => format!("<{:s}>", address), + _ => match (address.as_slice().slice_to(1), address.as_slice().slice_from(address.len() - 1)) { + ("<", ">") => address, + _ => format!("<{:s}>", address) + } + } +} + +/// Removes quotes from emails if needed +pub fn unquote_email_address(address: String) -> String { + match address.len() { + 0..1 => address, + _ => match (address.as_slice().slice_to(1), address.as_slice().slice_from(address.len() - 1)) { + ("<", ">") => address.as_slice().slice(1, address.len() - 1).to_string(), + _ => address + } + } +} + +/// Removes the trailing line return at the end of a string +pub fn remove_trailing_crlf(string: String) -> String { + if string.len() > 1 && string.as_slice().slice_from(string.len() - 2) == CRLF { + string.as_slice().slice_to(string.len() - 2).to_string() + } else if string.len() > 0 && string.as_slice().slice_from(string.len() - 1) == "\r" { + string.as_slice().slice_to(string.len() - 1).to_string() + } else { + string + } +} + +/// Returns the first word of a string, or the string if it contains no space +pub fn get_first_word(string: String) -> String { + string.as_slice().split_str(CRLF).next().unwrap().splitn(' ', 1).next().unwrap().to_string() +} + +#[cfg(test)] +mod test { + #[test] + fn test_quote_email_address() { + assert_eq!(super::quote_email_address("address".to_string()), "
".to_string()); + assert_eq!(super::quote_email_address("
".to_string()), "
".to_string()); + assert_eq!(super::quote_email_address("a".to_string()), "".to_string()); + assert_eq!(super::quote_email_address("".to_string()), "<>".to_string()); + } + + #[test] + fn test_unquote_email_address() { + assert_eq!(super::unquote_email_address("
".to_string()), "address".to_string()); + assert_eq!(super::unquote_email_address("address".to_string()), "address".to_string()); + assert_eq!(super::unquote_email_address("".to_string()), "".to_string()); + assert_eq!(super::unquote_email_address("a".to_string()), "a".to_string()); + assert_eq!(super::unquote_email_address("".to_string()), "".to_string()); + } + + #[test] + fn test_remove_trailing_crlf() { + assert_eq!(super::remove_trailing_crlf("word".to_string()), "word".to_string()); + assert_eq!(super::remove_trailing_crlf("word\r\n".to_string()), "word".to_string()); + assert_eq!(super::remove_trailing_crlf("word\r\n ".to_string()), "word\r\n ".to_string()); + assert_eq!(super::remove_trailing_crlf("word\r".to_string()), "word".to_string()); + assert_eq!(super::remove_trailing_crlf("\r\n".to_string()), "".to_string()); + assert_eq!(super::remove_trailing_crlf("\r".to_string()), "".to_string()); + assert_eq!(super::remove_trailing_crlf("a".to_string()), "a".to_string()); + assert_eq!(super::remove_trailing_crlf("".to_string()), "".to_string()); + } + + #[test] + fn test_get_first_word() { + assert_eq!(super::get_first_word("first word".to_string()), "first".to_string()); + assert_eq!(super::get_first_word("first word\r\ntest".to_string()), "first".to_string()); + assert_eq!(super::get_first_word("first".to_string()), "first".to_string()); + 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, ... + //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()); + assert_eq!(super::get_first_word("\r\n ".to_string()), "".to_string()); + } +} diff --git a/src/smtpcommon/extension.rs b/src/smtpcommon/extension.rs new file mode 100644 index 0000000..f872c63 --- /dev/null +++ b/src/smtpcommon/extension.rs @@ -0,0 +1,103 @@ +// 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::from_str::FromStr; +use std::fmt::{Show, Formatter, Result}; + +/// Supported ESMTP keywords +#[deriving(PartialEq,Eq,Clone)] +pub enum SmtpExtension { + /// 8BITMIME keyword + /// + /// RFC 6152 : https://tools.ietf.org/html/rfc6152 + EightBitMime, + /// SMTPUTF8 keyword + /// + /// RFC 6531 : https://tools.ietf.org/html/rfc6531 + SmtpUtfEight, + /// SIZE keyword + /// + /// RFC 1427 : https://tools.ietf.org/html/rfc1427 + Size(uint) +} + +impl Show for SmtpExtension { + fn fmt(&self, f: &mut Formatter) -> Result { + f.write( + match self { + &EightBitMime => "8BITMIME".to_string(), + &SmtpUtfEight => "SMTPUTF8".to_string(), + &Size(ref size) => format!("SIZE={}", size) + }.as_bytes() + ) + } +} + +impl FromStr for SmtpExtension { + 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), + "SMTPUTF8" => Some(SmtpUtfEight), + _ => None + }, + 2 => match (*splitted.get(0), from_str::(*splitted.get(1))) { + ("SIZE", Some(size)) => Some(Size(size)), + _ => None + }, + _ => None + } + } +} + +impl SmtpExtension { + /// Checks if the ESMTP keyword is the same + pub fn same_extension_as(&self, other: SmtpExtension) -> bool { + if *self == other { + return true; + } + match (*self, other) { + (Size(_), Size(_)) => true, + _ => false + } + } +} + +#[cfg(test)] +mod test { + use extension; + use extension::SmtpExtension; + + #[test] + fn test_extension_same_extension_as() { + assert_eq!(extension::EightBitMime.same_extension_as(extension::EightBitMime), true); + assert_eq!(extension::Size(42).same_extension_as(extension::Size(42)), true); + assert_eq!(extension::Size(42).same_extension_as(extension::Size(43)), true); + assert_eq!(extension::Size(42).same_extension_as(extension::EightBitMime), false); + } + + #[test] + fn test_extension_fmt() { + assert_eq!(format!("{}", extension::EightBitMime), "8BITMIME".to_string()); + assert_eq!(format!("{}", extension::Size(42)), "SIZE=42".to_string()); + } + + #[test] + fn test_extension_from_str() { + assert_eq!(from_str::("8BITMIME"), Some(extension::EightBitMime)); + assert_eq!(from_str::("SIZE 42"), Some(extension::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); + } +} diff --git a/src/lib.rs b/src/smtpcommon/lib.rs similarity index 76% rename from src/lib.rs rename to src/smtpcommon/lib.rs index a1522d3..6c17c6d 100644 --- a/src/lib.rs +++ b/src/smtpcommon/lib.rs @@ -31,25 +31,24 @@ //! extern crate smtp; //! use std::io::net::tcp::TcpStream; //! use smtp::client::SmtpClient; -//! use std::strbuf::StrBuf; +//! use std::string::String; //! -//! let mut email_client: SmtpClient = -//! SmtpClient::new(StrBuf::from_str("localhost"), None, None); +//! let mut email_client: SmtpClient = +//! SmtpClient::new(String::from_str("localhost"), None, None); //! email_client.send_mail( -//! StrBuf::from_str("user@example.com"), -//! vec!(StrBuf::from_str("user@example.org")), -//! StrBuf::from_str("Test email") +//! String::from_str("user@example.com"), +//! vec!(String::from_str("user@example.org")), +//! String::from_str("Test email") //! ); //! ``` -#![crate_id = "smtp#0.1-pre"] #![crate_type = "rlib"] #![crate_type = "dylib"] -#![desc = "Rust SMTP client"] -#![comment = "Simple SMTP client"] +#![desc = "Rust SMTP library"] +#![comment = "Simple and modern SMTP library"] #![license = "MIT/ASL2"] -#![doc(html_root_url = "http://amousset.github.io/rust-smtp/smtp/")] +#![doc(html_root_url = "http://amousset.github.io/rust-smtp/smtpcommon/")] #![feature(macro_rules)] #![feature(phase)] @@ -59,10 +58,9 @@ #![deny(non_uppercase_statics)] #![deny(unnecessary_typecast)] #![deny(unused_result)] -#![deny(deprecated_owned_vector)] -#![feature(phase)] #[phase(syntax, link)] extern crate log; - -pub mod smtp; pub mod common; -pub mod client; +pub mod command; +pub mod extension; +pub mod response; +pub mod transaction; diff --git a/src/smtpcommon/response.rs b/src/smtpcommon/response.rs new file mode 100644 index 0000000..bfc3b78 --- /dev/null +++ b/src/smtpcommon/response.rs @@ -0,0 +1,143 @@ +// 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 responses, contaiing a mandatory return code, and an optional text message + +use std::from_str::FromStr; +use std::fmt::{Show, Formatter, Result}; +use common::remove_trailing_crlf; +use std::result; + +/// Contains an SMTP reply, with separed code and message +/// +/// We do accept messages containing only a code, to comply with RFC5321 +#[deriving(PartialEq,Eq,Clone)] +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.write( + match self.clone().message { + Some(message) => format!("{} {}", self.code, message), + None => format!("{}", self.code) + }.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)), + (remove_trailing_crlf(s.slice_from(4).to_string())) + ) { + (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 { + let response = self.clone(); + if expected_codes.contains(&self.code) { + Ok(response) + } else { + Err(response) + } + } +} + +#[cfg(test)] +mod test { + use response::SmtpResponse; + + #[test] + fn test_response_fmt() { + assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message".to_string())}), "200 message".to_string()); + } + + #[test] + fn test_response_from_str() { + assert_eq!(from_str::("200 response message"), + Some(SmtpResponse{ + code: 200, + message: Some("response message".to_string()) + }) + ); + assert_eq!(from_str::("200-response message"), + Some(SmtpResponse{ + code: 200, + message: Some("response message".to_string()) + }) + ); + 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("response\r\nmessage".to_string()) + }) + ); + 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_response_with_code() { + assert_eq!(SmtpResponse{code: 200, message: Some("message".to_string())}.with_code(vec!(200)), + Ok(SmtpResponse{code: 200, message: Some("message".to_string())})); + assert_eq!(SmtpResponse{code: 400, message: Some("message".to_string())}.with_code(vec!(200)), + Err(SmtpResponse{code: 400, message: Some("message".to_string())})); + assert_eq!(SmtpResponse{code: 200, message: Some("message".to_string())}.with_code(vec!(200, 300)), + Ok(SmtpResponse{code: 200, message: Some("message".to_string())})); + } +} diff --git a/src/smtpcommon/transaction.rs b/src/smtpcommon/transaction.rs new file mode 100644 index 0000000..503011c --- /dev/null +++ b/src/smtpcommon/transaction.rs @@ -0,0 +1,121 @@ +// 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; +use std::fmt::{Show, Formatter}; +use command; +use command::SmtpCommand; + +/// Contains the state of the current transaction +#[deriving(PartialEq,Eq,Clone)] +pub enum TransactionState { + /// No connection was established + Unconnected, + /// The connection was successful and the banner was received + Connected, + /// An HELO or EHLO was successful + HelloSent, + /// A MAIL command was successful send + MailSent, + /// At least one RCPT command was sucessful + RecipientSent, + /// A DATA command was successful + DataSent +} + +impl Show for TransactionState { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write( + match *self { + Unconnected => "Unconnected", + Connected => "Connected", + HelloSent => "HelloSent", + MailSent => "MailSent", + RecipientSent => "RecipientSent", + DataSent => "DataSent" + }.as_bytes() + ) + } +} + +impl TransactionState { + /// bla bla + pub fn is_command_possible(&self, command: SmtpCommand) -> bool { + match (*self, command) { + (Unconnected, command::Connect) => true, + (Unconnected, _) => false, + // Only the message content can be sent in this state + (DataSent, _) => false, + // Commands that can be issued everytime + (_, command::ExtendedHello(_)) => true, + (_, command::Hello(_)) => true, + (_, command::Reset) => true, + (_, command::Verify(_, _)) => true, + (_, command::Expand(_, _)) => true, + (_, command::Help(_)) => true, + (_, command::Noop) => true, + (_, command::Quit) => true, + // Commands that require a particular state + (HelloSent, command::Mail(_, _)) => true, + (MailSent, command::Recipient(_, _)) => true, + (RecipientSent, command::Recipient(_, _)) => true, + (RecipientSent, command::Data) => true, + // Everything else + (_, _) => false + } + } + + /// a method + pub fn next_state(&mut self, command: SmtpCommand) -> Option { + match (*self, command) { + (Unconnected, command::Connect) => Some(Connected), + (Unconnected, _) => None, + (DataSent, _) => None, + // Commands that can be issued everytime + (_, command::ExtendedHello(_)) => Some(HelloSent), + (_, command::Hello(_)) => Some(HelloSent), + (Connected, command::Reset) => Some(Connected), + (_, command::Reset) => Some(HelloSent), + (state, command::Verify(_, _)) => Some(state), + (state, command::Expand(_, _)) => Some(state), + (state, command::Help(_)) => Some(state), + (state, command::Noop) => Some(state), + (_, command::Quit) => Some(Unconnected), + // Commands that require a particular state + (HelloSent, command::Mail(_, _)) => Some(MailSent), + (MailSent, command::Recipient(_, _)) => Some(RecipientSent), + (RecipientSent, command::Recipient(_, _)) => Some(RecipientSent), + (RecipientSent, command::Data) => Some(DataSent), + // Everything else + (_, _) => None + } + } +} + +#[cfg(test)] +mod test { + use command; + + #[test] + fn test_transaction_state_is_command_possible() { + assert!(!super::Unconnected.is_command_possible(command::Noop)); + assert!(!super::DataSent.is_command_possible(command::Noop)); + assert!(super::HelloSent.is_command_possible(command::Mail("".to_string(), None))); + assert!(!super::MailSent.is_command_possible(command::Mail("".to_string(), None))); + } + + #[test] + fn test_super_next_state() { + assert_eq!(super::MailSent.next_state(command::Noop), Some(super::MailSent)); + assert_eq!(super::HelloSent.next_state(command::Mail("".to_string(), None)), Some(super::MailSent)); + assert_eq!(super::MailSent.next_state(command::Mail("".to_string(), None)), None); + } +}