From 7b7f658420d0ea30c7be0c2906ec9dd096496e9e Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Mon, 12 May 2014 00:43:15 +0200 Subject: [PATCH] Reorganize sources and improve makefile (inspiring from rust-empty) --- Makefile | 78 +++-- src/examples/client.rs | 107 ------- src/smtp/client.rs | 665 ----------------------------------------- src/smtp/commands.rs | 174 ----------- src/smtp/common.rs | 78 ----- src/smtp/lib.rs | 68 ----- 6 files changed, 48 insertions(+), 1122 deletions(-) delete mode 100644 src/examples/client.rs delete mode 100644 src/smtp/client.rs delete mode 100644 src/smtp/commands.rs delete mode 100644 src/smtp/common.rs delete mode 100644 src/smtp/lib.rs diff --git a/Makefile b/Makefile index fa9039e..26ea897 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,65 @@ RUSTC ?= rustc RUSTDOC ?= rustdoc -RUSTFLAGS ?= -g -BUILDDIR ?= build -INSTALLDIR ?= /usr/local/lib -DOCDIR ?= doc +RUSTC_FLAGS ?= -g -SMTP_LIB := src/smtp/lib.rs +BIN_DIR = bin +DOC_DIR = doc +SRC_DIR = src +TARGET_DIR = target +EXAMPLES_DIR = examples +LIB = src/lib.rs -libsmtp=$(shell $(RUSTC) --crate-file-name $(SMTP_LIB)) +EXAMPLE_FILES := $(EXAMPLES_DIR)/*.rs +SOURCE_FILES := $(shell test -e src/ && find src -type f) -smtp_files=\ - $(wildcard src/smtp/*.rs) \ - $(wildcard src/smtp/client/*.rs) +TARGET := $(shell rustc --version | awk "/host:/ { print \$$2 }") +TARGET_LIB_DIR := $(TARGET_DIR)/$(TARGET)/lib -example_files=\ - $(wildcard src/examples/*.rs) +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) -smtp: $(libsmtp) +all: lib -$(libsmtp): $(smtp_files) - mkdir -p $(BUILDDIR) - $(RUSTC) $(RUSTFLAGS) $(SMTP_LIB) --out-dir=$(BUILDDIR) +lib: rlib dylib -all: smtp doc +rlib: $(RLIB) -doc: $(smtp_files) - $(RUSTDOC) $(SMTP_LIB) +$(RLIB): $(SOURCE_FILES) | $(LIB) $(TARGET_LIB_DIR) + $(RUSTC) --target $(TARGET) $(RUSTC_FLAGS) --crate-type=rlib $(LIB) --out-dir $(TARGET_LIB_DIR) -examples: smtp $(example_files) - $(RUSTC) $(RUSTFLAGS) -L $(BUILDDIR)/ src/examples/client.rs --out-dir=$(BUILDDIR) +dylib: $(DYLIB) -$(BUILDDIR)/tests: $(smtp_files) - mkdir -p $(BUILDDIR)/tests - $(RUSTC) --test $(SMTP_LIB) --out-dir=$(BUILDDIR)/tests +$(DYLIB): $(SOURCE_FILES) | $(LIB) $(TARGET_LIB_DIR) + $(RUSTC) --target $(TARGET) $(RUSTC_FLAGS) --crate-type=dylib $(LIB) --out-dir $(TARGET_LIB_DIR) -check: all $(BUILDDIR)/tests - $(BUILDDIR)/tests/smtp --test +$(TARGET_LIB_DIR): + mkdir -p $(TARGET_LIB_DIR) -install: $(libsmtp_so) - install $(libsmtp_so) $(INSTALLDIR) +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 $(BUILDDIR) - rm -rf $(DOCDIR) + rm -rf $(TARGET_DIR) + rm -rf $(DOC_DIR) + rm -rf $(BIN_DIR) -.PHONY: all smtp examples docs clean check +.PHONY: all lib rlib dylib test doc examples clean check diff --git a/src/examples/client.rs b/src/examples/client.rs deleted file mode 100644 index a67d108..0000000 --- a/src/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/smtp/client.rs b/src/smtp/client.rs deleted file mode 100644 index 27b7eec..0000000 --- a/src/smtp/client.rs +++ /dev/null @@ -1,665 +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::from_str::FromStr; -use std::str::from_utf8; -use std::result::Result; -use std::strbuf::StrBuf; -use std::io::{IoResult, Reader, Writer}; -use std::io::net::ip::{SocketAddr, Port}; -use std::io::net::tcp::TcpStream; -use std::io::net::addrinfo::get_host_addresses; -use common::{CRLF, get_first_word, unquote_email_address, remove_trailing_crlf}; -use commands; -use commands::{SMTP_PORT, SmtpCommand, EsmtpParameter}; - -/// 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) -> fmt::Result { - f.buf.write( - match self.clone().message { - Some(message) => format!("{} {}", self.code.to_str(), message), - None => self.code.to_str() - }.as_bytes() - ) - } -} - -// FromStr ? -impl FromStr for SmtpResponse { - fn from_str(s: &str) -> Option> { - // If the string is too short to be a response code - if s.len() < 3 { - None - // If we have only a code, with or without a trailing space - } else if s.len() == 3 || (s.len() == 4 && s.slice(3,4) == " ") { - match from_str::(s.slice_to(3)) { - Some(code) => Some(SmtpResponse{ - code: code, - message: None - }), - None => None - - } - // If we have a code and a message - } else { - match ( - from_str::(s.slice_to(3)), - vec!(" ", "-").contains(&s.slice(3,4)), - StrBuf::from_str(remove_trailing_crlf(s.slice_from(4).to_owned())) - ) { - (Some(code), true, message) => Some(SmtpResponse{ - code: code, - message: Some(message) - }), - _ => None - - } - } - } -} - -impl SmtpResponse { - /// Checks the presence of the response code in the array of expected codes. - fn with_code(&self, expected_codes: Vec) -> Result,SmtpResponse> { - let response = self.clone(); - if expected_codes.contains(&self.code) { - Ok(response) - } else { - Err(response) - } - } -} - -/// 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.buf.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: EsmtpParameter) -> Result { - match self.esmtp_features.clone() { - Some(esmtp_features) => { - for feature in esmtp_features.iter() { - if keyword.same_keyword_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 - 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, permits to check order againt RFCs - 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) -> Result, SmtpResponse> { - if !self.stream.is_none() { - fail!("The connection is already established"); - } - let ip = match get_host_addresses(self.host.clone().into_owned()) { - Ok(ip_vector) => *ip_vector.get(0), // TODO : select a random ip - Err(..) => fail!("Cannot resolve {:s}", self.host) - }; - self.stream = match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) { - Ok(stream) => Some(stream), - Err(..) => fail!("Cannot connect to {:s}:{:u}", self.host, self.port) - }; - - // Log the connection - info!("Connection established to {}[{}]:{}", self.my_hostname.clone(), ip, self.port); - - 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(_) => {}, - 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(commands::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) { - drop(self.stream.clone().unwrap()); - } - - /// Send a HELO command - pub fn helo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { - check_state_in!(vec!(Connected)); - - match self.send_command(commands::Hello(my_hostname.clone())).with_code(vec!(250)) { - 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(commands::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(commands::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(commands::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(commands::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(commands::Quit).with_code(vec!(221)) { - Ok(response) => { - self.close(); - 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(commands::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(commands::Noop).with_code(vec!(250)) - } - - /// Sends a VRFY command - pub fn vrfy(&mut self, to_address: StrBuf) -> Result, SmtpResponse> { - check_state_not_in!(vec!(Unconnected)); - self.send_command(commands::Verify(to_address)).with_code(vec!(250)) - } -} - -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::{SmtpResponse, SmtpServerInfo}; - use commands; - - #[test] - fn test_smtp_response_fmt() { - assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message")}), "200 message".to_owned()); - } - - #[test] - fn test_smtp_response_from_str() { - assert_eq!(from_str::>("200 response message"), - Some(SmtpResponse{ - code: 200, - message: Some(StrBuf::from_str("response message")) - }) - ); - assert_eq!(from_str::>("200-response message"), - Some(SmtpResponse{ - code: 200, - message: Some(StrBuf::from_str("response message")) - }) - ); - assert_eq!(from_str::>("200"), - Some(SmtpResponse{ - code: 200, - message: None - }) - ); - assert_eq!(from_str::>("200 "), - Some(SmtpResponse{ - code: 200, - message: None - }) - ); - assert_eq!(from_str::>("200-response\r\nmessage"), - Some(SmtpResponse{ - code: 200, - message: Some(StrBuf::from_str("response\r\nmessage")) - }) - ); - assert_eq!(from_str::>("2000response message"), None); - assert_eq!(from_str::>("20a response message"), None); - assert_eq!(from_str::>("20 "), None); - assert_eq!(from_str::>("20"), None); - assert_eq!(from_str::>("2"), None); - assert_eq!(from_str::>(""), None); - } - - #[test] - fn test_smtp_response_with_code() { - assert_eq!(SmtpResponse{code: 200, message: Some("message")}.with_code(vec!(200)), - Ok(SmtpResponse{code: 200, message: Some("message")})); - assert_eq!(SmtpResponse{code: 400, message: Some("message")}.with_code(vec!(200)), - Err(SmtpResponse{code: 400, message: Some("message")})); - assert_eq!(SmtpResponse{code: 200, message: Some("message")}.with_code(vec!(200, 300)), - Ok(SmtpResponse{code: 200, message: Some("message")})); - } - - #[test] - fn test_smtp_server_info_fmt() { - assert_eq!(format!("{}", SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(commands::EightBitMime)) - }), "name with [8BITMIME]".to_owned()); - assert_eq!(format!("{}", SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(commands::EightBitMime, commands::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!(commands::EightBitMime, commands::Size(42)))); - assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 UNKNON 42"), - Some(vec!(commands::EightBitMime))); - assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-9BITMIME\r\n250 SIZE a"), - None); - assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-SIZE 42\r\n250 SIZE 43"), - Some(vec!(commands::Size(42), commands::Size(43)))); - } - - #[test] - fn test_smtp_server_info_supports_feature() { - assert_eq!(SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(commands::EightBitMime)) - }.supports_feature(commands::EightBitMime), Ok(commands::EightBitMime)); - assert_eq!(SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(commands::Size(42), commands::EightBitMime)) - }.supports_feature(commands::EightBitMime), Ok(commands::EightBitMime)); - assert_eq!(SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(commands::Size(42), commands::EightBitMime)) - }.supports_feature(commands::Size(0)), Ok(commands::Size(42))); - assert!(SmtpServerInfo{ - name: "name", - esmtp_features: Some(vec!(commands::EightBitMime)) - }.supports_feature(commands::Size(42)).is_err()); - } -} diff --git a/src/smtp/commands.rs b/src/smtp/commands.rs deleted file mode 100644 index 5fd380b..0000000 --- a/src/smtp/commands.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2014 Alexis Mousset. See the COPYRIGHT -// file at the top-level directory of this distribution. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! SMTP commands and ESMTP features library - -use std::fmt::{Show, Formatter, Result}; -use std::io::net::ip::Port; -use std::from_str::FromStr; - -/// Default SMTP port -pub static SMTP_PORT: Port = 25; -//pub static SMTPS_PORT: Port = 465; -//pub static SUBMISSION_PORT: Port = 587; - -/// Supported SMTP commands -/// -/// We do not implement the following SMTP commands, as they were deprecated in RFC 5321 -/// and must not be used by clients: -/// SEND, SOML, SAML, TURN -#[deriving(Eq,Clone)] -pub enum SmtpCommand { - /// Extended Hello command - ExtendedHello(T), - /// Hello command - Hello(T), - /// Mail command, takes optionnal options - Mail(T, Option>), - /// Recipient command, takes optionnal options - Recipient(T, Option>), - /// Data command - Data, - /// Reset command - Reset, - /// Verify command - Verify(T), - /// Expand command - Expand(T), - /// Help command, takes optionnal options - Help(Option), - /// Noop command - Noop, - /// Quit command - Quit, - -} - -impl Show for SmtpCommand { - fn fmt(&self, f: &mut Formatter) -> Result { - f.buf.write(match *self { - ExtendedHello(ref my_hostname) => - format!("EHLO {}", my_hostname.clone()), - Hello(ref my_hostname) => - format!("HELO {}", my_hostname.clone()), - Mail(ref from_address, None) => - format!("MAIL FROM:<{}>", from_address.clone()), - Mail(ref from_address, Some(ref options)) => - format!("MAIL FROM:<{}> {}", from_address.clone(), options.connect(" ")), - Recipient(ref to_address, None) => - format!("RCPT TO:<{}>", to_address.clone()), - Recipient(ref to_address, Some(ref options)) => - format!("RCPT TO:<{}> {}", to_address.clone(), options.connect(" ")), - Data => "DATA".to_owned(), - Reset => "RSET".to_owned(), - Verify(ref address) => - format!("VRFY {}", address.clone()), - Expand(ref address) => - format!("EXPN {}", address.clone()), - Help(None) => "HELP".to_owned(), - Help(Some(ref argument)) => - format!("HELP {}", argument.clone()), - Noop => "NOOP".to_owned(), - Quit => "QUIT".to_owned(), - }.as_bytes()) - } -} - -/// Supported ESMTP keywords -#[deriving(Eq,Clone)] -pub enum EsmtpParameter { - /// 8BITMIME keyword - /// - /// RFC 6152 : https://tools.ietf.org/html/rfc6152 - EightBitMime, - /// SIZE keyword - /// - /// RFC 1427 : https://tools.ietf.org/html/rfc1427 - Size(uint) -} - -impl Show for EsmtpParameter { - fn fmt(&self, f: &mut Formatter) -> Result { - f.buf.write( - match self { - &EightBitMime => "8BITMIME".to_owned(), - &Size(ref size) => format!("SIZE={}", size) - }.as_bytes() - ) - } -} - -impl FromStr for EsmtpParameter { - fn from_str(s: &str) -> Option { - let splitted : Vec<&str> = s.splitn(' ', 1).collect(); - match splitted.len() { - 1 => match *splitted.get(0) { - "8BITMIME" => Some(EightBitMime), - _ => None - }, - 2 => match (*splitted.get(0), from_str::(*splitted.get(1))) { - ("SIZE", Some(size)) => Some(Size(size)), - _ => None - }, - _ => None - } - } -} - -impl EsmtpParameter { - /// Checks if the ESMTP keyword is the same - pub fn same_keyword_as(&self, other: EsmtpParameter) -> bool { - if *self == other { - return true; - } - match (*self, other) { - (Size(_), Size(_)) => true, - _ => false - } - } -} - -#[cfg(test)] -mod test { - use super::{SmtpCommand, EsmtpParameter}; - - #[test] - fn test_command_fmt() { - let noop: SmtpCommand = super::Noop; - assert_eq!(format!("{}", noop), "NOOP".to_owned()); - assert_eq!(format!("{}", super::ExtendedHello("me")), "EHLO me".to_owned()); - assert_eq!(format!("{}", - super::Mail("test", Some(vec!("option")))), "MAIL FROM: option".to_owned() - ); - } - - #[test] - fn test_esmtp_parameter_same_keyword_as() { - assert_eq!(super::EightBitMime.same_keyword_as(super::EightBitMime), true); - assert_eq!(super::Size(42).same_keyword_as(super::Size(42)), true); - assert_eq!(super::Size(42).same_keyword_as(super::Size(43)), true); - assert_eq!(super::Size(42).same_keyword_as(super::EightBitMime), false); - } - - #[test] - fn test_esmtp_parameter_fmt() { - assert_eq!(format!("{}", super::EightBitMime), "8BITMIME".to_owned()); - assert_eq!(format!("{}", super::Size(42)), "SIZE=42".to_owned()); - } - - #[test] - fn test_esmtp_parameter_from_str() { - assert_eq!(from_str::("8BITMIME"), Some(super::EightBitMime)); - assert_eq!(from_str::("SIZE 42"), Some(super::Size(42))); - assert_eq!(from_str::("SIZ 42"), None); - assert_eq!(from_str::("SIZE 4a2"), None); - // TODO: accept trailing spaces ? - assert_eq!(from_str::("SIZE 42 "), None); - } -} diff --git a/src/smtp/common.rs b/src/smtp/common.rs deleted file mode 100644 index 459f52a..0000000 --- a/src/smtp/common.rs +++ /dev/null @@ -1,78 +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 - -pub static SP: &'static str = " "; -pub static CRLF: &'static str = "\r\n"; - -/// Adds quotes to emails if needed -pub fn quote_email_address(address: ~str) -> ~str { - match (address.as_slice().slice_to(1), address.as_slice().slice_from(address.as_slice().len()-1)) { - ("<", ">") => address, - _ => format!("<{:s}>", address) - } -} - -/// Removes quotes from emails if needed -pub fn unquote_email_address(address: ~str) -> ~str { - match (address.as_slice().slice_to(1), address.as_slice().slice_from(address.as_slice().len() - 1)) { - ("<", ">") => address.as_slice().slice(1, address.as_slice().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.as_slice().slice_from(string.as_slice().len() - 2) == CRLF { - string.as_slice().slice_to(string.as_slice().len() - 2).to_owned() - } else if string.as_slice().slice_from(string.as_slice().len() - 1) == "\r" { - string.as_slice().slice_to(string.as_slice().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()); - } - - #[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(" or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! # Rust SMTP client -//! -//! The client does its best to follow RFC 5321 (https://tools.ietf.org/html/rfc5321). -//! -//! It also implements the following extensions : -//! -//! * 8BITMIME (RFC 6152 : https://tools.ietf.org/html/rfc6152) -//! * SIZE (RFC 1427 : https://tools.ietf.org/html/rfc1427) -//! -//! ## What this client is NOT made for -//! -//! Send emails to public email servers. It is not designed to smartly handle servers responses, -//! to rate-limit emails, to make retries, and all that complicated stuff needed to politely -//! talk to public servers. -//! -//! What this client does is basically try once to send the email, and say if it worked. -//! It should only be used to transfer emails to a relay server. -//! -//! ## Usage -//! -//! ```rust -//! extern crate smtp; -//! use std::io::net::tcp::TcpStream; -//! use smtp::client::SmtpClient; -//! use std::strbuf::StrBuf; -//! -//! let mut email_client: SmtpClient = -//! SmtpClient::new(StrBuf::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") -//! ); -//! ``` - -#![crate_id = "smtp#0.1-pre"] -#![crate_type = "rlib"] -#![crate_type = "dylib"] - -#![desc = "Rust SMTP client"] -#![comment = "Simple SMTP client"] -#![license = "MIT/ASL2"] -#![doc(html_root_url = "http://amousset.github.io/rust-smtp/smtp/")] - -#![feature(macro_rules)] -#![feature(phase)] -#![deny(non_camel_case_types)] -#![deny(missing_doc)] -#![deny(unnecessary_qualification)] -#![deny(non_uppercase_statics)] -#![deny(unnecessary_typecast)] -#![deny(unused_result)] -#![deny(deprecated_owned_vector)] - -#![feature(phase)] #[phase(syntax, link)] extern crate log; - -pub mod commands; -pub mod common; -pub mod client;