From 270efd193a11e66dce14700a50d3c42c12e725bc Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Mon, 3 Feb 2014 20:13:35 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + LICENSE | 13 +++ Makefile | 42 ++++++++ README.md | 4 - README.rst | 19 ++++ src/examples/client.rs | 9 ++ src/smtp/.directory | 3 + src/smtp/client.rs | 144 +++++++++++++++++++++++++++ src/smtp/commands.rs | 216 +++++++++++++++++++++++++++++++++++++++++ src/smtp/common.rs | 18 ++++ src/smtp/lib.rs | 15 +++ 11 files changed, 481 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile delete mode 100644 README.md create mode 100644 README.rst create mode 100644 src/examples/client.rs create mode 100644 src/smtp/.directory create mode 100644 src/smtp/client.rs create mode 100644 src/smtp/commands.rs create mode 100644 src/smtp/common.rs create mode 100644 src/smtp/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b0d536 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +doc/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..da6b827 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2014 Alexis Mousset + +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 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e3c7e68 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +RUSTC ?= rustc +RUSTDOC ?= rustdoc +RUSTPKG ?= rustpkg +RUSTFLAGS ?= -O -Z debug-info +VERSION=0.1-pre + +libsmtp_so=build/libsmtp-4c61a8ad-0.1-pre.so + +smtp_files=\ + $(wildcard src/smtp/*.rs) \ + $(wildcard src/smtp/client/*.rs) + +example_files=\ + src/examples/client.rs + +smtp: $(libsmtp_so) + +$(libsmtp_so): $(smtp_files) + mkdir -p build/ + $(RUSTC) $(RUSTFLAGS) src/smtp/lib.rs --out-dir=build + +all: smtp examples docs + +docs: doc/smtp/index.html + +doc/smtp/index.html: $(smtp_files) + $(RUSTDOC) src/smtp/lib.rs + +examples: smtp $(example_files) + $(RUSTC) $(RUSTFLAGS) -L build/ src/examples/client.rs -o build/client + +tests: $(smtp_files) + $(RUSTC) --test -o build/tests src/smtp/lib.rs + +check: all build/tests + build/tests --test + +clean: + rm -rf build/ + rm -rf doc/ + +.PHONY: all smtp examples docs clean check tests diff --git a/README.md b/README.md deleted file mode 100644 index 5acb4ea..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -rust-smtp -========= - -SMTP library for Rust diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..6fa63ce --- /dev/null +++ b/README.rst @@ -0,0 +1,19 @@ +Rust SMTP library +================= + +This library implements an SMTP client, and maybe later a simple SMTP server. + +It does not support ESMTP nor SSL/TLS for now, and is only an RFC821 + client. + +Rust versions +------------- + +This library follows rust master. + +License +------- + +This program is distributed under the Apache license (version 2.0). + +See LICENSE for details. diff --git a/src/examples/client.rs b/src/examples/client.rs new file mode 100644 index 0000000..fbcab73 --- /dev/null +++ b/src/examples/client.rs @@ -0,0 +1,9 @@ +#[crate_id = "client"]; + +extern mod smtp; +use smtp::client::SmtpClient; + +fn main() { + let mut email_client: SmtpClient = SmtpClient::new("localhost", None, None); + email_client.send_mail("user@example.org", [&"user@localhost"], "plop"); +} \ No newline at end of file diff --git a/src/smtp/.directory b/src/smtp/.directory new file mode 100644 index 0000000..e2a451c --- /dev/null +++ b/src/smtp/.directory @@ -0,0 +1,3 @@ +[Dolphin] +Timestamp=2014,2,3,2,12,31 +Version=3 diff --git a/src/smtp/client.rs b/src/smtp/client.rs new file mode 100644 index 0000000..9b4300c --- /dev/null +++ b/src/smtp/client.rs @@ -0,0 +1,144 @@ +/*! + +Simple SMTP client, without ESMTP and SSL/TLS support for now. + +# Usage + +``` +let mut email_client: SmtpClient = SmtpClient::new("localhost", None, "myhost.example.org"); +email_client.send_mail("user@example.org", [&"user@localhost"], "Message content."); +``` + +# TODO + + Support ESMTP : Parse server answer, and manage mail and rcpt options. + +* Client options: `mail_options` and `rcpt_options` lists + +* Server options: helo/ehlo, parse and store ehlo response + +Manage errors + +Support SSL/TLS + +*/ + +use std::str::from_utf8; +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; +use commands::SmtpCommand; + +/// Contains an SMTP reply, with separed code and message +pub struct SmtpResponse { + /// Server respinse code code + code: uint, + /// Server response string + message: ~str +} + +impl ToStr for SmtpResponse { + /// Get the server reply + fn to_str(&self) -> ~str { + return format!("{} {}", self.code.to_str(), self.message); + } +} + +/// Structure that implements a simple SMTP client +pub struct SmtpClient { + /// TCP socket between client and server + socket: Option, + /// Reading buffer + buf: [u8, ..1000], + /// Host we are connecting to + host: ~str, + /// Port we are connecting on + port: Port, + /// Our hostname for HELO/EHLO commands + my_hostname: ~str +} + +impl SmtpClient { + + /// Connect to the configured server + pub fn connect(&mut self) -> SmtpResponse { + let ips = get_host_addresses(self.host.clone()); + let ip = ips.expect(format!("Cannot resolve {}", self.host))[0]; + + match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) { + None => fail!("Cannot connect to {}:{}", self.host, self.port), + Some(s) => self.socket = Some(s) + } + + match self.get_reply() { + None => fail!("No banner on {}", self.host), + Some(response) => response + } + } + + /// Send an SMTP command + pub fn send_command(&mut self, command: ~str, option: Option<~str>) -> SmtpResponse { + + self.send(SmtpCommand::new(command, option).get_formatted_command()); + let response = self.get_reply(); + + match response { + None => fail!("No answer on {}", self.host), + Some(response) => response + } + } + + /// Send a string on the client socket + fn send(&mut self, string: ~str) { + self.socket.write_str(string); + debug!("{:s}", string); + } + + /// Get the SMTP response + fn get_reply(&mut self) -> Option { + self.buf = [0u8, ..1000]; + + let response = match self.socket.read(self.buf) { + None => fail!("Read error"), + Some(bytes_read) => self.buf.slice_to(bytes_read - 1) + }; + + debug!("{:s}", from_utf8(response).unwrap()); + + if response.len() > 4 { + Some(SmtpResponse { + code: from_str(from_utf8(response.slice_to(3)).unwrap()).unwrap(), + message: from_utf8(response.slice_from(4)).unwrap().to_owned() + }) + } else { + None + } + } + + /// Create a new SMTP client + pub fn new(host: &str, port: Option, my_hostname: Option<&str>) -> SmtpClient { + SmtpClient{ + socket: None, + host: host.to_owned(), + port: port.unwrap_or(SMTP_PORT), + my_hostname: my_hostname.unwrap_or("localhost").to_owned(), + buf: [0u8, ..1000] + } + } + + /// Send an email + pub fn send_mail(&mut self, from_addr: &str, to_addrs: &[&str], message: &str) { + let my_hostname = self.my_hostname.clone(); + self.connect(); + self.send_command(~"HELO", Some(my_hostname)); + self.send_command(~"MAIL", Some(from_addr.to_owned())); + for &to_addr in to_addrs.iter() { + self.send_command(~"RCPT", Some(to_addr.to_owned())); + } + self.send_command(~"DATA", None); + self.send(message.to_owned()); + self.send(~"\r\n.\r\n"); + self.send_command(~"QUIT", None); + } +} diff --git a/src/smtp/commands.rs b/src/smtp/commands.rs new file mode 100644 index 0000000..08bb96f --- /dev/null +++ b/src/smtp/commands.rs @@ -0,0 +1,216 @@ +/*! + * SMTP commands library + * + * RFC 5321 : http://tools.ietf.org/html/rfc5321#section-4.1 + */ + +use std::fmt; +use common::CRLF; + +/* + * HELO + * MAIL FROM: + * RCPT TO: + * DATA + * RSET + * SEND FROM: + * SOML FROM: + * SAML FROM: + * VRFY + * EXPN + * HELP [ ] + * NOOP + * QUIT + * TURN + */ + +/// List of SMTP commands +#[deriving(Eq,Clone)] +pub enum Command { + Hello, + Ehello, + Mail, + Recipient, + Data, + Reset, + SendMail, + SendOrMail, + SendAndMail, + Verify, + Expand, + Help, + Noop, + Quit, + /// Deprecated in RFC 5321 + Turn, +} + +impl Command { + /// Tell if the command accetps an string argument. + pub fn takes_argument(&self) -> bool{ + match *self { + Ehello => true, + Hello => true, + Mail => true, + Recipient => true, + Data => false, + Reset => false, + SendMail => true, + SendOrMail => true, + SendAndMail => true, + Verify => true, + Expand => 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 { + Ehello => true, + Hello => true, + Mail => true, + Recipient => true, + Data => false, + Reset => false, + SendMail => true, + SendOrMail => true, + SendAndMail => true, + Verify => true, + Expand => true, + Help => false, + Noop => false, + Quit => false, + Turn => false, + } + } +} + +impl ToStr for Command { + /// Get the name of a command. + fn to_str(&self) -> ~str { + match *self { + Hello => ~"HELO", + Ehello => ~"EHLO", + Mail => ~"MAIL", + Recipient => ~"RCPT", + Data => ~"DATA", + Reset => ~"RSET", + SendMail => ~"SEND", + SendOrMail => ~"SOML", + SendAndMail => ~"SAML", + Verify => ~"VRFY", + Expand => ~"EXPN", + Help => ~"HELP", + Noop => ~"NOOP", + Quit => ~"QUIT", + Turn => ~"TURN", + } + } +} + +impl FromStr for Command { + /// Get the Command from its name. + fn from_str(command: &str) -> Option { + if !command.is_ascii() { + return None; + } + match command { + "HELO" => Some(Hello), + "EHLO" => Some(Ehello), + "MAIL" => Some(Mail), + "RCPT" => Some(Recipient), + "DATA" => Some(Data), + "RSET" => Some(Reset), + "SEND" => Some(SendMail), + "SOML" => Some(SendOrMail), + "SAML" => Some(SendAndMail), + "VRFY" => Some(Verify), + "EXPN" => Some(Expand), + "HELP" => Some(Help), + "NOOP" => Some(Noop), + "QUIT" => Some(Quit), + "TURN" => Some(Turn), + _ => None, + } + } +} + +impl fmt::Show for Command { + /// Format SMTP command display + fn fmt(s: &Command, f: &mut fmt::Formatter) { + f.buf.write(match *s { + Ehello => "EHLO".as_bytes(), + Hello => "HELO".as_bytes(), + Mail => "MAIL FROM:".as_bytes(), + Recipient => "RCPT TO:".as_bytes(), + Data => "DATA".as_bytes(), + Reset => "RSET".as_bytes(), + SendMail => "SEND TO:".as_bytes(), + SendOrMail => "SOML TO:".as_bytes(), + SendAndMail => "SAML TO:".as_bytes(), + Verify => "VRFY".as_bytes(), + Expand => "EXPN".as_bytes(), + Help => "HELP".as_bytes(), + Noop => "NOOP".as_bytes(), + Quit => "QUIT".as_bytes(), + Turn => "TURN".as_bytes() + }) + } +} + +/// Structure for a complete SMTP command, containing an optionnal string argument. +pub struct SmtpCommand { + command: Command, + argument: Option<~str> +} + +impl SmtpCommand { + /// Return a new structure from the name of the command and an optionnal argument. + pub fn new(command_str: ~str, argument: Option<~str>) -> SmtpCommand { + let command = match from_str::(command_str) { + Some(x) => x, + None => fail!("Unrecognized SMTP command") + }; + + 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} + } + } + + /// Return the formatted command, ready to be used in an SMTP session. + pub fn get_formatted_command(&self) -> ~str { + match (self.command.takes_argument(), self.command.needs_argument(), self.argument.clone()) { + (true, _, Some(argument)) => format!("{} {}{}", self.command, argument, CRLF), + (_, false, None) => format!("{}{}", self.command, CRLF), + _ => fail!("Wrong SMTP syntax") + } + } +} + +#[cfg(test)] +mod test { + use super::SmtpCommand; + + #[test] + fn test_command_parameters() { + assert!((super::Help).takes_argument() == true); + assert!((super::Reset).takes_argument() == false); + assert!((super::Hello).needs_argument() == true); + } + + #[test] + fn test_get_simple_command() { + assert!(SmtpCommand::new(~"TURN", None).get_formatted_command() == format!("TURN{}", ::common::CRLF)); + } + + #[test] + fn test_get_argument_command() { + assert!(SmtpCommand::new(~"EHLO", Some(~"example.example")).get_formatted_command() == format!("EHLO example.example{}", ::common::CRLF)); + } +} \ No newline at end of file diff --git a/src/smtp/common.rs b/src/smtp/common.rs new file mode 100644 index 0000000..9f1ae07 --- /dev/null +++ b/src/smtp/common.rs @@ -0,0 +1,18 @@ +/*! + * Common definitions for SMTP + */ + +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; + +/// End of SMTP commands +pub static CRLF: &'static str = "\r\n"; + +/// Add quotes to emails +pub fn quote_email_address(addr: &str) -> ~str { + return format!("<{:s}>", addr).to_owned(); +} diff --git a/src/smtp/lib.rs b/src/smtp/lib.rs new file mode 100644 index 0000000..84146b5 --- /dev/null +++ b/src/smtp/lib.rs @@ -0,0 +1,15 @@ +#[crate_id = "smtp#0.1-pre"]; + +#[comment = "Rust SMTP client"]; +#[license = "MIT/ASL2"]; +#[crate_type = "lib"]; + +//#[crate_type = "dylib"]; +//#[crate_type = "rlib"]; + +#[deny(non_camel_case_types)]; +//#[deny(missing_doc)]; + +pub mod commands; +pub mod common; +pub mod client;