Initial commit

This commit is contained in:
Alexis Mousset
2014-02-03 20:13:35 +01:00
parent 11e289a880
commit 270efd193a
11 changed files with 481 additions and 4 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
build/
doc/

13
LICENSE Normal file
View File

@@ -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.

42
Makefile Normal file
View File

@@ -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

View File

@@ -1,4 +0,0 @@
rust-smtp
=========
SMTP library for Rust

19
README.rst Normal file
View File

@@ -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.

9
src/examples/client.rs Normal file
View File

@@ -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");
}

3
src/smtp/.directory Normal file
View File

@@ -0,0 +1,3 @@
[Dolphin]
Timestamp=2014,2,3,2,12,31
Version=3

144
src/smtp/client.rs Normal file
View File

@@ -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<TcpStream>,
/// 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<SmtpResponse> {
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<Port>, 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);
}
}

216
src/smtp/commands.rs Normal file
View File

@@ -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 <SP> <domain> <CRLF>
* MAIL <SP> FROM:<reverse-path> <CRLF>
* RCPT <SP> TO:<forward-path> <CRLF>
* DATA <CRLF>
* RSET <CRLF>
* SEND <SP> FROM:<reverse-path> <CRLF>
* SOML <SP> FROM:<reverse-path> <CRLF>
* SAML <SP> FROM:<reverse-path> <CRLF>
* VRFY <SP> <string> <CRLF>
* EXPN <SP> <string> <CRLF>
* HELP [<SP> <string>] <CRLF>
* NOOP <CRLF>
* QUIT <CRLF>
* TURN <CRLF>
*/
/// 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<Command> {
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>(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));
}
}

18
src/smtp/common.rs Normal file
View File

@@ -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();
}

15
src/smtp/lib.rs Normal file
View File

@@ -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;