Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
build/
|
||||
doc/
|
||||
13
LICENSE
Normal file
13
LICENSE
Normal 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
42
Makefile
Normal 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
|
||||
19
README.rst
Normal file
19
README.rst
Normal 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
9
src/examples/client.rs
Normal 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
3
src/smtp/.directory
Normal file
@@ -0,0 +1,3 @@
|
||||
[Dolphin]
|
||||
Timestamp=2014,2,3,2,12,31
|
||||
Version=3
|
||||
144
src/smtp/client.rs
Normal file
144
src/smtp/client.rs
Normal 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
216
src/smtp/commands.rs
Normal 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
18
src/smtp/common.rs
Normal 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
15
src/smtp/lib.rs
Normal 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;
|
||||
Reference in New Issue
Block a user