From 19e85d7cf3308269f0dbe139bb7ccc99e82f0745 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Mon, 28 Apr 2014 08:08:17 +0200 Subject: [PATCH] Various changes, mainly on data types --- LICENSE-APACHE | 192 ++++++++++++++- Makefile | 44 ++-- README.rst | 10 +- src/examples/client.rs | 14 +- src/smtp/client.rs | 543 +++++++++++++++++++++++++++-------------- src/smtp/commands.rs | 235 ++++++------------ src/smtp/common.rs | 41 ++-- src/smtp/lib.rs | 43 +++- 8 files changed, 725 insertions(+), 397 deletions(-) diff --git a/LICENSE-APACHE b/LICENSE-APACHE index da6b827..16fe87b 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -1,10 +1,198 @@ -Copyright 2014 Alexis Mousset + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/Makefile b/Makefile index 891626a..0e53ed5 100644 --- a/Makefile +++ b/Makefile @@ -1,41 +1,47 @@ RUSTC ?= rustc RUSTDOC ?= rustdoc RUSTFLAGS ?= -g -VERSION=0.1-pre +BUILDDIR ?= build +INSTALLDIR ?= /usr/local/lib +DOCDIR ?= doc -libsmtp_so=build/libsmtp-4c61a8ad-0.1-pre.so +SMTP_LIB := src/smtp/lib.rs + +libsmtp=$(shell $(RUSTC) --crate-file-name $(SMTP_LIB)) smtp_files=\ $(wildcard src/smtp/*.rs) \ $(wildcard src/smtp/client/*.rs) example_files=\ - src/examples/client.rs + $(wildcard src/examples/*.rs) -smtp: $(libsmtp_so) +smtp: $(libsmtp) -$(libsmtp_so): $(smtp_files) - mkdir -p build/ - $(RUSTC) $(RUSTFLAGS) src/smtp/lib.rs --out-dir=build +$(libsmtp): $(smtp_files) + mkdir -p $(BUILDDIR) + $(RUSTC) $(RUSTFLAGS) $(SMTP_LIB) --out-dir=$(BUILDDIR) -all: smtp examples docs +all: smtp examples doc -docs: doc/smtp/index.html - -doc/smtp/index.html: $(smtp_files) - $(RUSTDOC) src/smtp/lib.rs +doc: $(smtp_files) + $(RUSTDOC) $(SMTP_LIB) examples: smtp $(example_files) - $(RUSTC) $(RUSTFLAGS) -L build/ src/examples/client.rs -o build/client + $(RUSTC) $(RUSTFLAGS) -L $(BUILDDIR)/ src/examples/client.rs --out-dir=$(BUILDDIR) -build/tests: $(smtp_files) - $(RUSTC) --test -o build/tests src/smtp/lib.rs +$(BUILDDIR)/tests: $(smtp_files) + mkdir -p $(BUILDDIR)/tests + $(RUSTC) --test $(SMTP_LIB) --out-dir=$(BUILDDIR)/tests -check: all build/tests - build/tests --test +check: all $(BUILDDIR)/tests + $(BUILDDIR)/tests/smtp --test + +install: $(libsmtp_so) + install $(libsmtp_so) $(INSTALLDIR) clean: - rm -rf build/ - rm -rf doc/ + rm -rf $(BUILDDIR) + rm -rf $(DOCDIR) .PHONY: all smtp examples docs clean check tests diff --git a/README.rst b/README.rst index 16bbb78..9d78179 100644 --- a/README.rst +++ b/README.rst @@ -27,17 +27,15 @@ To run the example: ./build/client Todo ---- +---- -- Documentation - RFC compliance -- Test corevage - SSL/TLS support -- Client mail and rcpt options +- AUTH support License ------- -This program is distributed under the Apache license (version 2.0). +This program is distributed under the terms of both the MIT license and the Apache License (Version 2.0). -See LICENSE for details. +See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details. diff --git a/src/examples/client.rs b/src/examples/client.rs index 7125125..59a0993 100644 --- a/src/examples/client.rs +++ b/src/examples/client.rs @@ -1,10 +1,20 @@ +// 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; use std::io::net::tcp::TcpStream; use smtp::client::SmtpClient; +use std::strbuf::StrBuf; fn main() { - let mut email_client: SmtpClient = SmtpClient::new("localhost", None, None); - email_client.send_mail("user@localhost", [&"user@localhost"], "Test email"); + let mut email_client: SmtpClient = SmtpClient::new(StrBuf::from_str("localhost"), None, None); + email_client.send_mail(StrBuf::from_str(""), vec!(StrBuf::from_str("")), StrBuf::from_str("Test email")); } diff --git a/src/smtp/client.rs b/src/smtp/client.rs index f000b16..92b73af 100644 --- a/src/smtp/client.rs +++ b/src/smtp/client.rs @@ -1,58 +1,55 @@ -/*! +// 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. -Simple SMTP client. - -# Usage - -``` -let mut email_client: SmtpClient = SmtpClient::new("localhost", None, None); -email_client.send_mail("user@example.org", [&"user@example.com"], "Example email"); -``` - -*/ +/*! A simple SMTP client */ use std::fmt; -use std::from_str; +use std::fmt::{Show, Formatter}; +use std::from_str::FromStr; use std::str::from_utf8; use std::result::Result; -use std::io::{IoResult, IoError}; +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::{SMTP_PORT, CRLF, get_first_word}; +use common::{CRLF, get_first_word}; use commands; -use commands::{Command, SmtpCommand, EhloKeyword}; - -// Define smtp_fail! and smtp_success! +use commands::{SMTP_PORT, SmtpCommand, EsmtpParameter}; /// Contains an SMTP reply, with separed code and message #[deriving(Eq,Clone)] -pub struct SmtpResponse { - /// Server respinse code code +pub struct SmtpResponse { + /// Server response code code: uint, /// Server response string - message: ~str + message: T } -impl fmt::Show for SmtpResponse { - /// Format SMTP response display - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), IoError> { +impl Show for SmtpResponse { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.buf.write( format!("{} {}", self.code.to_str(), self.message).as_bytes() ) } } -impl from_str::FromStr for SmtpResponse { - /// Parse an SMTP response line - fn from_str(s: &str) -> Option { +// FromStr ? +impl FromStr for SmtpResponse { + fn from_str(s: &str) -> Option> { if s.len() < 5 { None } else { - if [" ", "-"].contains(&s.slice(3,4)) { + if vec!(" ", "-").contains(&s.slice(3,4)) { Some(SmtpResponse{ code: from_str(s.slice_to(3)).unwrap(), - message: s.slice_from(4).to_owned() + message: StrBuf::from_str(s.slice_from(4)) }) } else { None @@ -61,36 +58,43 @@ impl from_str::FromStr for SmtpResponse { } } -impl SmtpResponse { - /// Check the response code - fn with_code(&self, expected_codes: &[uint]) -> Result { - let response = SmtpResponse{code: self.code, message: self.message.clone()}; - for &code in expected_codes.iter() { - if code == self.code { - return Ok(response); - } +impl SmtpResponse { + /// Checks the response code + fn with_code(&self, expected_codes: Vec) -> Result,SmtpResponse> { + let response = self.clone(); + if expected_codes.contains(&self.code) { + Ok(response) + } else { + Err(response) } - return Err(response); } } /// Information about an SMTP server #[deriving(Eq,Clone)] -pub struct SmtpServerInfo { +pub struct SmtpServerInfo { /// Server name - name: ~str, + name: T, /// ESMTP features supported by the server - esmtp_features: Option<~[EhloKeyword]> + esmtp_features: Option> } -impl SmtpServerInfo { - /// Parse supported ESMTP features - fn parse_esmtp_response(message: &str) -> Option<~[EhloKeyword]> { - let mut esmtp_features: ~[EhloKeyword] = ~[]; - for line in message.split_str(CRLF) { - match from_str::(line) { +impl Show for SmtpServerInfo{ + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.buf.write( + format!("{} with {}", self.name, self.esmtp_features).as_bytes() + ) + } +} + +impl SmtpServerInfo { + /// Parses supported ESMTP features + fn parse_esmtp_response(message: T) -> Option> { + let mut esmtp_features = Vec::new(); + for line in message.into_owned().split_str(CRLF) { + match from_str::>(line) { Some(SmtpResponse{code: 250, message: message}) => { - match from_str::(message) { + match from_str::(message.into_owned()) { Some(keyword) => esmtp_features.push(keyword), None => () } @@ -99,13 +103,13 @@ impl SmtpServerInfo { } } match esmtp_features.len() { - 0 => None, - _ => Some(esmtp_features) + 0 => None, + _ => Some(esmtp_features) } } /// Checks if the server supports an ESMTP feature - fn supports_feature(&self, keyword: EhloKeyword) -> bool { + fn supports_feature(&self, keyword: EsmtpParameter) -> bool { match self.esmtp_features.clone() { Some(esmtp_features) => { esmtp_features.contains(&keyword) @@ -115,195 +119,368 @@ impl SmtpServerInfo { } } -impl fmt::Show for SmtpServerInfo { - /// Format SMTP server information display - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), IoError> { - f.buf.write( - format!("{:s} with {}", self.name, self.esmtp_features).as_bytes() - ) - } +/// Contains the state of the current transaction +#[deriving(Eq,Clone)] +pub enum SmtpClientState { + /// The server is unconnected + Unconnected, + /// The connection and banner were successful + Connected, + /// An HELO or EHLO was successfully sent + HeloSent, + /// A MAIL command was successful + 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!("Wrong transaction state for this command."); + } + ); +) + +macro_rules! check_state_not_in( + ($expected_states:expr) => ( + if $expected_states.contains(&self.state) { + fail!("Wrong transaction state for this command."); + } + ); +) + +macro_rules! smtp_fail_if_err( + ($response:expr) => ( + match $response { + Err(response) => { + self.smtp_fail(response) + }, + Ok(..) => {} + } + ); +) + /// Structure that implements a simple SMTP client -pub struct SmtpClient { +pub struct SmtpClient { /// TCP stream between client and server stream: Option, /// Host we are connecting to - host: ~str, + host: T, /// Port we are connecting on port: Port, /// Our hostname for HELO/EHLO commands - my_hostname: ~str, + my_hostname: T, /// Information about the server - server_info: Option + /// Value is None before HELO/EHLO + server_info: Option>, + /// Transaction state, permits to check order againt RFCs + state: SmtpClientState } -impl SmtpClient { - /// Create a new SMTP client - pub fn new(host: &str, port: Option, my_hostname: Option<&str>) -> SmtpClient { +impl SmtpClient { + /// Creates a new SMTP client + pub fn new(host: StrBuf, port: Option, my_hostname: Option) -> SmtpClient { SmtpClient{ stream: None, - host: host.to_owned(), + host: host, port: port.unwrap_or(SMTP_PORT), - my_hostname: my_hostname.unwrap_or("localhost").to_owned(), - server_info: None + my_hostname: my_hostname.unwrap_or(StrBuf::from_str("localhost")), + server_info: None, + state: Unconnected } } } -impl SmtpClient { - /// Send an SMTP command - pub fn send_command(&mut self, command: Command, option: Option<~str>) -> SmtpResponse { - self.send_and_get_response(SmtpCommand::new(command, option).to_str()) +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[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) + }; + 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)) + }, + Err(response) => { + self.smtp_fail(response) + } + _ => {} + } + + info!("SMTP 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) { + if false { + self.smtp_fail("Server does not accepts UTF-8 strings"); + } + } + + // Mail + smtp_fail_if_err!(self.mail(from_address, None)); + + // Recipient + // TODO Return rejected addresses + 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 + smtp_fail_if_err!(self.message(message)); + + // Quit + smtp_fail_if_err!(self.quit()); + } +} + +impl SmtpClient { + /// Sends an SMTP command + pub fn send_command(&mut self, command: SmtpCommand) -> SmtpResponse { + self.send_and_get_response(format!("{}", command)) } - /// Send an email - pub fn send_message(&mut self, message: ~str) -> SmtpResponse { - self.send_and_get_response(format!("{:s}{:s}.", message, CRLF)) + /// Sends an email + pub fn send_message(&mut self, message: StrBuf) -> SmtpResponse { + self.send_and_get_response(format!("{}{:s}.", message, CRLF)) } - /// Send a complete message or a command to the server and get the response - fn send_and_get_response(&mut self, string: ~str) -> SmtpResponse { + /// 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!("Write success"), + Ok(..) => debug!("Wrote: {:s}", string), Err(..) => fail!("Could not write to stream") } match self.get_reply() { - Some(response) => response, - None => fail!("No answer on {}", self.host) + Some(response) => {debug!("Read: {:s}", response.to_str()); response}, + None => fail!("No answer on {:s}", self.host) } } - /// Get the SMTP response - fn get_reply(&mut self) -> Option { + /// 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) + from_str::>(response) } - /// Connect to the configured server - pub fn connect(&mut self) -> SmtpResponse { - if !self.stream.is_none() { - fail!("The connection is already established"); - } - let ip = match get_host_addresses(self.host.clone()) { - Ok(ip_vector) => ip_vector[0], - Err(..) => fail!("Cannot resolve {}", self.host) - }; - self.stream = match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) { - Ok(stream) => Some(stream), - Err(..) => fail!("Cannot connect to {}:{}", self.host, self.port) - }; - match self.get_reply() { - Some(response) => response, - None => fail!("No banner on {}", self.host) + /// 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); } - /// Print an SMTP response as info - fn smtp_success(&mut self, response: SmtpResponse) { - info!("{:u} {:s}", response.code, response.message); + /// Checks if the server is connected + pub fn is_connected(&mut self) -> bool { + self.noop().is_ok() } - - /// Send a QUIT command and end the program - fn smtp_fail(&mut self, command: ~str, reason: &str) { - self.send_command(commands::Quit, None); - fail!("{} failed: {:s}", command, reason); + + /// Closes the TCP stream + pub fn close(&mut self) { + drop(self.stream.clone().unwrap()); } - - /// Send an email - pub fn send_mail(&mut self, from_addr: &str, to_addrs: &[&str], message: &str) { - let my_hostname = self.my_hostname.clone(); - - // Connect - match self.connect().with_code([220]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"CONNECT", response.to_str()) - } - - // Extended Hello or Hello - match self.send_command(commands::Ehlo, Some(my_hostname.clone())).with_code([250, 500]) { - Ok(SmtpResponse{code: 250, message: message}) => { + + /// Send a HELO command + pub fn helo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { + check_state_in!([Connected]); + + match self.send_command(commands::Hello(my_hostname.clone())).with_code(vec!(250)) { + Ok(response) => { self.server_info = Some( SmtpServerInfo{ - name: get_first_word(message.clone()), - esmtp_features: SmtpServerInfo::parse_esmtp_response(message.clone()) + name: get_first_word(response.message.clone()), + esmtp_features: None } ); - self.smtp_success(SmtpResponse{code: 250u, message: message}); + self.state = HeloSent; + Ok(response) }, - Ok(..) => { - match self.send_command(commands::Helo, Some(my_hostname.clone())).with_code([250]) { - Ok(response) => { - self.server_info = Some( - SmtpServerInfo{ - name: get_first_word(response.message.clone()), - esmtp_features: None - } - ); - self.smtp_success(response); - }, - Err(response) => self.smtp_fail(~"HELO", response.to_str()) + Err(response) => Err(response) + } + } + + + /// Sends a EHLO command + pub fn ehlo(&mut self, my_hostname: StrBuf) -> Result, SmtpResponse> { + check_state_not_in!([Unconnected]); + + match self.send_command(commands::ExtendedHello(my_hostname.clone())).with_code(vec!(250)) { + Ok(response) => { + self.server_info = Some( + SmtpServerInfo{ + name: get_first_word(response.message.clone()), + esmtp_features: SmtpServerInfo::parse_esmtp_response(response.message.clone()) + } + ); + 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!([HeloSent]); + + match self.send_command(commands::Mail(from_address, 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!([MailSent, RcptSent]); + + match self.send_command(commands::Recipient(to_address, 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!([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!([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!([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!([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) => self.smtp_fail(~"EHLO", response.to_str()) - } - - debug!("SMTP server : {:s}", self.server_info.clone().unwrap().to_str()) - - // Check message encoding according to the server's capability - if ! self.server_info.clone().unwrap().supports_feature(commands::EightBitMime) { - if ! message.is_ascii() { - self.smtp_fail(~"DATA", "Server does not accepts UTF-8 strings") + Err(response) => { + Err(response) } } - - // Mail - match self.send_command(commands::Mail, Some(from_addr.to_owned())).with_code([250]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"MAIL", response.to_str()) - } - - // Recipient - for &to_addr in to_addrs.iter() { - match self.send_command(commands::Rcpt, Some(to_addr.to_owned())).with_code([250]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"RCPT", response.to_str()) - } - } - - // Data - match self.send_command(commands::Data, None).with_code([354]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"DATA", response.to_str()) - } - - // Message content - match self.send_message(message.to_owned()).with_code([250]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"MESSAGE", response.to_str()) - } - - // Quit - match self.send_command(commands::Quit, None).with_code([221]) { - Ok(response) => self.smtp_success(response), - Err(response) => self.smtp_fail(~"DATA", response.to_str()) - } + } + + /// Sends a NOOP commands + pub fn noop(&mut self) -> Result, SmtpResponse> { + check_state_not_in!([Unconnected]); + self.send_command(commands::Noop).with_code(vec!(250)) + } + + /// Sends a VRFY command + pub fn vrfy(&mut self, to_address: StrBuf) -> Result, SmtpResponse> { + check_state_not_in!([Unconnected]); + self.send_command(commands::Verify(to_address)).with_code(vec!(250)) } } -impl Reader for SmtpClient { - /// Read a string from the client socket +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) } - /// Read a string from the client socket + /// Reads a string from the client socket fn read_to_str(&mut self) -> IoResult<~str> { let mut buf = [0u8, ..1000]; @@ -311,21 +488,19 @@ impl Reader for SmtpClient { Ok(bytes_read) => from_utf8(buf.slice_to(bytes_read - 1)).unwrap(), Err(..) => fail!("Read error") }; - debug!("Read: {:s}", response); return Ok(response.to_owned()); } } -impl Writer for SmtpClient { - /// Send a string on the client socket +impl Writer for SmtpClient { + /// Sends a string on the client socket fn write(&mut self, buf: &[u8]) -> IoResult<()> { self.stream.clone().unwrap().write(buf) } - /// Send a string on the client socket + /// Sends a string on the client socket fn write_str(&mut self, string: &str) -> IoResult<()> { - debug!("Wrote: {:s}", string); self.stream.clone().unwrap().write_str(string) } } diff --git a/src/smtp/commands.rs b/src/smtp/commands.rs index 9007b53..7b550e9 100644 --- a/src/smtp/commands.rs +++ b/src/smtp/commands.rs @@ -1,159 +1,99 @@ -/*! - * SMTP commands and ESMTP features library - * - * RFC 5321 : https://tools.ietf.org/html/rfc5321#section-4.1 - */ +// 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. -use std::fmt; -use std::io; -use std::from_str; -use std::io::IoError; +/*! SMTP commands [1] and ESMTP features [2] library -/// List of SMTP commands +[1] https://tools.ietf.org/html/rfc5321#section-4.1 +[2] http://tools.ietf.org/html/rfc1869 + +*/ + +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; + +/// 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 Command { +pub enum SmtpCommand { /// Extended Hello command - Ehlo, + ExtendedHello(T), /// Hello command - Helo, - /// Mail command - Mail, - /// Recipient command - Rcpt, + Hello(T), + /// Mail command, takes optionnal options + Mail(T, Option), + /// Recipient command, takes optionnal options + Recipient(T, Option), /// Data command Data, /// Reset command - Rset, - /// Send command, deprecated in RFC 5321 - Send, - /// Send Or Mail command, deprecated in RFC 5321 - Soml, - /// Send And Mail command, deprecated in RFC 5321 - Saml, + Reset, /// Verify command - Vrfy, + Verify(T), /// Expand command - Expn, - /// Help command - Help, + Expand(T), + /// Help command, takes optionnal options + Help(Option), /// Noop command Noop, /// Quit command Quit, - /// Turn command, deprecated in RFC 5321 - Turn, + } -impl Command { - /// Tell if the command accetps an string argument. - pub fn takes_argument(&self) -> bool{ - match *self { - Ehlo => true, - Helo => true, - Mail => true, - Rcpt => true, - Data => false, - Rset => false, - Send => true, - Soml => true, - Saml => true, - Vrfy => true, - Expn => true, - Help => true, - Noop => false, - Quit => false, - Turn => false, - } - } - - /// Tell if an argument is needed by the command. - pub fn needs_argument(&self) -> bool { - match *self { - Ehlo => true, - Helo => true, - Mail => true, - Rcpt => true, - Data => false, - Rset => false, - Send => true, - Soml => true, - Saml => true, - Vrfy => true, - Expn => true, - Help => false, - Noop => false, - Quit => false, - Turn => false, - } - } -} - -impl fmt::Show for Command { - /// Format SMTP command display - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), io::IoError> { +impl Show for SmtpCommand { + fn fmt(&self, f: &mut Formatter) -> Result { f.buf.write(match *self { - Ehlo => "EHLO", - Helo => "Helo", - Mail => "MAIL FROM:", - Rcpt => "RCPT TO:", - Data => "DATA", - Rset => "RSET", - Send => "SEND TO:", - Soml => "SOML TO:", - Saml => "SAML TO:", - Vrfy => "VRFY", - Expn => "EXPN", - Help => "HELP", - Noop => "NOOP", - Quit => "QUIT", - Turn => "TURN" + 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.clone()), + Recipient(ref to_address, None) => + format!("RCPT TO:{}", to_address.clone()), + Recipient(ref to_address, Some(ref options)) => + format!("RCPT TO:{} {}", to_address.clone(), options.clone()), + Data => ~"DATA", + Reset => ~"RSET", + Verify(ref address) => + format!("VRFY {}", address.clone()), + Expand(ref address) => + format!("EXPN {}", address.clone()), + Help(None) => ~"HELP", + Help(Some(ref argument)) => + format!("HELP {}", argument.clone()), + Noop => ~"NOOP", + Quit => ~"QUIT", }.as_bytes()) } } -/// Structure for a complete SMTP command, containing an optionnal string argument. -pub struct SmtpCommand { - /// The SMTP command (e.g. MAIL, QUIT, ...) - command: Command, - /// An optionnal argument to the command - argument: Option<~str> -} - -impl SmtpCommand { - /// Return a new structure from the name of the command and an optionnal argument. - pub fn new(command: Command, argument: Option<~str>) -> SmtpCommand { - match (command.takes_argument(), command.needs_argument(), argument.clone()) { - (true, true, None) => fail!("Wrong SMTP syntax : argument needed"), - (false, false, Some(x)) => fail!("Wrong SMTP syntax : {:s} not accepted", x), - _ => SmtpCommand {command: command, argument: argument} - } - } -} - -impl fmt::Show for SmtpCommand { - /// Return the formatted command, ready to be used in an SMTP session. - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), io::IoError> { - f.buf.write( - match (self.command.takes_argument(), self.command.needs_argument(), self.argument.clone()) { - (true, _, Some(argument)) => format!("{} {}", self.command, argument), - (_, false, None) => format!("{}", self.command), - _ => fail!("Wrong SMTP syntax") - }.as_bytes() - ) - } -} - /// Supported ESMTP keywords #[deriving(Eq,Clone)] -pub enum EhloKeyword { +pub enum EsmtpParameter { /// 8BITMIME keyword /// RFC 6152 : https://tools.ietf.org/html/rfc6152 EightBitMime, } -impl fmt::Show for EhloKeyword { - /// Format SMTP response display - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), IoError> { +impl Show for EsmtpParameter { + fn fmt(&self, f: &mut Formatter) -> Result { f.buf.write( match self { &EightBitMime => "8BITMIME".as_bytes() @@ -162,10 +102,9 @@ impl fmt::Show for EhloKeyword { } } -impl from_str::FromStr for EhloKeyword { - // Match keywords - fn from_str(s: &str) -> Option { - match s { +impl FromStr for EsmtpParameter { + fn from_str(s: &str) -> Option { + match s.as_slice() { "8BITMIME" => Some(EightBitMime), _ => None } @@ -174,42 +113,22 @@ impl from_str::FromStr for EhloKeyword { #[cfg(test)] mod test { - use super::{SmtpCommand, EhloKeyword}; - - #[test] - fn test_command_parameters() { - assert!((super::Help).takes_argument() == true); - assert!((super::Rset).takes_argument() == false); - assert!((super::Helo).needs_argument() == true); - } - - #[test] - fn test_command_to_str() { - assert!(super::Turn.to_str() == ~"TURN"); - } + use super::{EsmtpParameter}; #[test] fn test_command_fmt() { - assert!(format!("{}", super::Turn) == ~"TURN"); + //assert!(format!("{}", super::Noop) == ~"NOOP"); + assert!(format!("{}", super::ExtendedHello("me")) == ~"EHLO me"); + assert!(format!("{}", super::Mail("test", Some("option"))) == ~"MAIL FROM:test option"); } #[test] - fn test_get_simple_command() { - assert!(SmtpCommand::new(super::Turn, None).to_str() == ~"TURN"); - } - - #[test] - fn test_get_argument_command() { - assert!(SmtpCommand::new(super::Ehlo, Some(~"example.example")).to_str() == ~"EHLO example.example"); - } - - #[test] - fn test_ehlokeyword_fmt() { + fn test_esmtp_parameter_fmt() { assert!(format!("{}", super::EightBitMime) == ~"8BITMIME"); } #[test] fn test_ehlokeyword_from_str() { - assert!(from_str::("8BITMIME") == Some(super::EightBitMime)); + assert!(from_str::("8BITMIME") == Some(super::EightBitMime)); } } diff --git a/src/smtp/common.rs b/src/smtp/common.rs index 59f18f7..aa95457 100644 --- a/src/smtp/common.rs +++ b/src/smtp/common.rs @@ -1,20 +1,24 @@ -/*! - * Common definitions for SMTP - * - * Needs to be organized later. - */ +// 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. -use std::io::net::ip::Port; +/*! Common definitions for SMTP -/// Default SMTP port -pub static SMTP_PORT: Port = 25; -//pub static SMTPS_PORT: Port = 465; -//pub static SUBMISSION_PORT: Port = 587; +Needs to be organized later. -/// End of SMTP commands +*/ + +use std::strbuf::StrBuf; + +pub static SP: &'static str = " "; pub static CRLF: &'static str = "\r\n"; -/// Add quotes to emails +/// Adds quotes to emails pub fn quote_email_address(addr: &str) -> ~str { match (addr.slice_to(1), addr.slice_from(addr.len()-1)) { ("<", ">") => addr.to_owned(), @@ -22,7 +26,7 @@ pub fn quote_email_address(addr: &str) -> ~str { } } -/// Remove quotes from emails +/// Removes quotes from emails pub fn unquote_email_address(addr: &str) -> ~str { match (addr.slice_to(1), addr.slice_from(addr.len() - 1)) { ("<", ">") => addr.slice(1, addr.len() - 1).to_owned(), @@ -31,8 +35,8 @@ pub fn unquote_email_address(addr: &str) -> ~str { } /// Returns the first word of a string, or the string if it contains no space -pub fn get_first_word(string: &str) -> ~str { - string.split_str(CRLF).next().unwrap().splitn(' ', 1).next().unwrap().to_owned() +pub fn get_first_word(string: T) -> StrBuf { + StrBuf::from_str(string.into_owned().split_str(CRLF).next().unwrap().splitn(' ', 1).next().unwrap()) } #[cfg(test)] @@ -47,12 +51,13 @@ mod test { fn test_unquote_email_address() { assert!(super::unquote_email_address("") == ~"plop"); assert!(super::unquote_email_address("plop") == ~"plop"); + assert!(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. + +/*! SMTP library + +This library implements a simple SMTP client. +RFC 5321 : https://tools.ietf.org/html/rfc5321#section-4.1 + +It does NOT manages email content. + +It also implements the following extesnions + 8BITMIME (RFC 6152 : https://tools.ietf.org/html/rfc6152) + +# Usage + +``` +let mut email_client: SmtpClient = SmtpClient::new(StrBuf::from_str("localhost"), None, None); +email_client.send_mail(StrBuf::from_str(""), vec!(StrBuf::from_str("")), StrBuf::from_str("Test email")); +``` + +# TODO: + Add SSL/TLS + Add AUTH + +*/ #![crate_id = "smtp#0.1-pre"] -#![comment = "Rust SMTP client"] +#![desc = "Rust SMTP client"] +#![comment = "Simple SMTP client"] #![license = "ASL2"] #![crate_type = "lib"] -//#[crate_type = "dylib"]; -//#[crate_type = "rlib"]; +#![doc(html_root_url = "http://www.rust-ci.org/amousset/rust-smtp/doc/")] +#![feature(macro_rules)] #![deny(non_camel_case_types)] #![deny(missing_doc)]