Reorganization
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,3 @@
|
|||||||
target/
|
target/
|
||||||
bin/
|
TAGS
|
||||||
doc/
|
doc/
|
||||||
*~
|
|
||||||
*.sh
|
|
||||||
.directory
|
|
||||||
|
|||||||
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
|
||||||
|
name = "rust-smtp"
|
||||||
|
version = "0.0.1"
|
||||||
|
#readme = "README.md"
|
||||||
|
authors = [ "Alexis Mousset <contact@amousset.eu>" ]
|
||||||
|
#tags = ["smtp", "email", "library"]
|
||||||
|
|
||||||
|
[[lib]]
|
||||||
|
name = "smtpcommon"
|
||||||
|
path = "src/smtpcommon/lib.rs"
|
||||||
|
|
||||||
|
#[[lib]]
|
||||||
|
#name = "smtpc"
|
||||||
|
#path = "src/smtpc/lib.rs"
|
||||||
|
|
||||||
66
Makefile
66
Makefile
@@ -1,66 +0,0 @@
|
|||||||
RUSTC ?= rustc
|
|
||||||
RUSTDOC ?= rustdoc
|
|
||||||
RUSTC_FLAGS ?= -g
|
|
||||||
SHELL = /bin/sh
|
|
||||||
|
|
||||||
BIN_DIR = bin
|
|
||||||
DOC_DIR = doc
|
|
||||||
SRC_DIR = src
|
|
||||||
TARGET_DIR = target
|
|
||||||
EXAMPLES_DIR = examples
|
|
||||||
LIB = src/lib.rs
|
|
||||||
|
|
||||||
EXAMPLE_FILES := $(EXAMPLES_DIR)/*.rs
|
|
||||||
SOURCE_FILES := $(shell test -e src/ && find src -type f)
|
|
||||||
|
|
||||||
TARGET := $(shell rustc --version | awk "/host:/ { print \$$2 }")
|
|
||||||
TARGET_LIB_DIR := $(TARGET_DIR)/$(TARGET)/lib
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
all: lib
|
|
||||||
|
|
||||||
lib: rlib dylib
|
|
||||||
|
|
||||||
rlib: $(RLIB)
|
|
||||||
|
|
||||||
$(RLIB): $(SOURCE_FILES) | $(LIB) $(TARGET_LIB_DIR)
|
|
||||||
$(RUSTC) --target $(TARGET) $(RUSTC_FLAGS) --crate-type=rlib $(LIB) --out-dir $(TARGET_LIB_DIR)
|
|
||||||
|
|
||||||
dylib: $(DYLIB)
|
|
||||||
|
|
||||||
$(DYLIB): $(SOURCE_FILES) | $(LIB) $(TARGET_LIB_DIR)
|
|
||||||
$(RUSTC) --target $(TARGET) $(RUSTC_FLAGS) --crate-type=dylib $(LIB) --out-dir $(TARGET_LIB_DIR)
|
|
||||||
|
|
||||||
$(TARGET_LIB_DIR):
|
|
||||||
mkdir -p $(TARGET_LIB_DIR)
|
|
||||||
|
|
||||||
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 $(TARGET_DIR)
|
|
||||||
rm -rf $(DOC_DIR)
|
|
||||||
rm -rf $(BIN_DIR)
|
|
||||||
|
|
||||||
.PHONY: all lib rlib dylib test doc examples clean check
|
|
||||||
@@ -9,22 +9,14 @@ This library implements an SMTP client, and maybe later a simple SMTP server.
|
|||||||
Rust versions
|
Rust versions
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
This library is designed for Rust 0.11-pre (master).
|
This library is designed for Rust 0.11.0-nightly (master).
|
||||||
|
|
||||||
Install
|
Install
|
||||||
------
|
------
|
||||||
|
|
||||||
Build the library:
|
Use Cargo to build this library.
|
||||||
|
|
||||||
make
|
cargo build
|
||||||
|
|
||||||
To build the example command-line client code:
|
|
||||||
|
|
||||||
make examples
|
|
||||||
|
|
||||||
To run the example's help:
|
|
||||||
|
|
||||||
./build/client -h
|
|
||||||
|
|
||||||
Todo
|
Todo
|
||||||
----
|
----
|
||||||
@@ -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 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<StrBuf>, message: StrBuf, server: StrBuf, port: Option<Port>, my_hostname: Option<StrBuf>) {
|
|
||||||
let mut email_client: SmtpClient<StrBuf, TcpStream> =
|
|
||||||
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>(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);
|
|
||||||
}
|
|
||||||
557
src/client.rs
557
src/client.rs
@@ -1,557 +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 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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::str::from_utf8;
|
|
||||||
use std::result::Result;
|
|
||||||
use std::strbuf::StrBuf;
|
|
||||||
use std::io::{IoResult, Reader, Writer};
|
|
||||||
use std::io::net::ip::Port;
|
|
||||||
use std::io::net::tcp::TcpStream;
|
|
||||||
|
|
||||||
use common::{get_first_word, unquote_email_address};
|
|
||||||
use smtp::response::SmtpResponse;
|
|
||||||
use smtp::extension;
|
|
||||||
use smtp::extension::SmtpExtension;
|
|
||||||
use smtp::command;
|
|
||||||
use smtp::command::SmtpCommand;
|
|
||||||
use smtp::{SMTP_PORT, CRLF};
|
|
||||||
|
|
||||||
/// Information about an SMTP server
|
|
||||||
#[deriving(Clone)]
|
|
||||||
struct SmtpServerInfo<T> {
|
|
||||||
/// Server name
|
|
||||||
name: T,
|
|
||||||
/// ESMTP features supported by the server
|
|
||||||
esmtp_features: Option<Vec<SmtpExtension>>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Show> Show for SmtpServerInfo<T>{
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
f.write(
|
|
||||||
format!("{} with {}",
|
|
||||||
self.name,
|
|
||||||
match self.esmtp_features.clone() {
|
|
||||||
Some(features) => features.to_str(),
|
|
||||||
None => format!("no supported features")
|
|
||||||
}
|
|
||||||
).as_bytes()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Str> SmtpServerInfo<T> {
|
|
||||||
/// Parses supported ESMTP features
|
|
||||||
///
|
|
||||||
/// TODO: Improve parsing
|
|
||||||
fn parse_esmtp_response(message: T) -> Option<Vec<SmtpExtension>> {
|
|
||||||
let mut esmtp_features = Vec::new();
|
|
||||||
for line in message.as_slice().split_str(CRLF) {
|
|
||||||
match from_str::<SmtpResponse<StrBuf>>(line) {
|
|
||||||
Some(SmtpResponse{code: 250, message: message}) => {
|
|
||||||
match from_str::<SmtpExtension>(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: SmtpExtension) -> Result<SmtpExtension, ()> {
|
|
||||||
match self.esmtp_features.clone() {
|
|
||||||
Some(esmtp_features) => {
|
|
||||||
for feature in esmtp_features.iter() {
|
|
||||||
if keyword.same_extension_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<T, S> {
|
|
||||||
/// TCP stream between client and server
|
|
||||||
/// Value is None before connection
|
|
||||||
stream: Option<S>,
|
|
||||||
/// 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<SmtpServerInfo<T>>,
|
|
||||||
/// Transaction state, to check the sequence of commands
|
|
||||||
state: SmtpClientState
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> SmtpClient<StrBuf, S> {
|
|
||||||
/// Creates a new SMTP client
|
|
||||||
pub fn new(host: StrBuf, port: Option<Port>, my_hostname: Option<StrBuf>) -> SmtpClient<StrBuf, S> {
|
|
||||||
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<StrBuf, TcpStream> {
|
|
||||||
/// Connects to the configured server
|
|
||||||
pub fn connect(&mut self) -> IoResult<TcpStream> {
|
|
||||||
// connect should not be called when the client is already connected
|
|
||||||
if !self.stream.is_none() {
|
|
||||||
fail!("The connection is already established");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to connect
|
|
||||||
TcpStream::connect(self.host.clone().as_slice(), self.port)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// bla
|
|
||||||
pub fn get_banner(&mut self) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
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<StrBuf>, message: StrBuf) {
|
|
||||||
let my_hostname = self.my_hostname.clone();
|
|
||||||
|
|
||||||
// Connect
|
|
||||||
match self.connect() {
|
|
||||||
Ok(stream) => self.stream = Some(stream),
|
|
||||||
Err(..) => fail!("Cannot connect to the server")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the connection
|
|
||||||
info!("Connection established to {}[{}]:{}", self.host, self.stream.clone().unwrap().peer_name().unwrap().ip, self.port);
|
|
||||||
|
|
||||||
match self.get_banner() {
|
|
||||||
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(extension::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<S: Writer + Reader + Clone> SmtpClient<StrBuf, S> {
|
|
||||||
/// Sends an SMTP command
|
|
||||||
// TODO : ensure this is an ASCII string
|
|
||||||
fn send_command(&mut self, command: SmtpCommand<StrBuf>) -> SmtpResponse<StrBuf> {
|
|
||||||
self.send_and_get_response(format!("{}", command))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends an email
|
|
||||||
fn send_message(&mut self, message: StrBuf) -> SmtpResponse<StrBuf> {
|
|
||||||
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<StrBuf> {
|
|
||||||
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<SmtpResponse<StrBuf>> {
|
|
||||||
let response = match self.read_to_str() {
|
|
||||||
Ok(string) => string,
|
|
||||||
Err(..) => fail!("No answer")
|
|
||||||
};
|
|
||||||
from_str::<SmtpResponse<StrBuf>>(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Closes the connection and fail with a given messgage
|
|
||||||
fn smtp_fail<T: Show>(&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) {
|
|
||||||
// Close the TCP connection
|
|
||||||
drop(self.stream.clone().unwrap());
|
|
||||||
// Reset client state
|
|
||||||
self.stream = None;
|
|
||||||
self.state = Unconnected;
|
|
||||||
self.server_info = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a HELO command
|
|
||||||
pub fn helo(&mut self, my_hostname: StrBuf) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_in!(vec!(Connected));
|
|
||||||
|
|
||||||
match self.send_command(command::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<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_not_in!(vec!(Unconnected));
|
|
||||||
|
|
||||||
match self.send_command(command::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<Vec<StrBuf>>) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_in!(vec!(HeloSent));
|
|
||||||
|
|
||||||
match self.send_command(command::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<Vec<StrBuf>>) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_in!(vec!(MailSent, RcptSent));
|
|
||||||
|
|
||||||
match self.send_command(command::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<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_in!(vec!(RcptSent));
|
|
||||||
|
|
||||||
match self.send_command(command::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<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
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<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_not_in!(vec!(Unconnected));
|
|
||||||
match self.send_command(command::Quit).with_code(vec!(221)) {
|
|
||||||
Ok(response) => {
|
|
||||||
Ok(response)
|
|
||||||
},
|
|
||||||
Err(response) => {
|
|
||||||
Err(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a RSET command
|
|
||||||
pub fn rset(&mut self) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_not_in!(vec!(Unconnected));
|
|
||||||
match self.send_command(command::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<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_not_in!(vec!(Unconnected));
|
|
||||||
self.send_command(command::Noop).with_code(vec!(250))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a VRFY command
|
|
||||||
pub fn vrfy(&mut self, to_address: StrBuf) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
|
|
||||||
check_state_not_in!(vec!(Unconnected));
|
|
||||||
self.send_command(command::Verify(to_address)).with_code(vec!(250))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, S: Reader + Clone> Reader for SmtpClient<T, S> {
|
|
||||||
/// Reads a string from the client socket
|
|
||||||
fn read(&mut self, buf: &mut [u8]) -> IoResult<uint> {
|
|
||||||
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<T, S: Writer + Clone> Writer for SmtpClient<T, S> {
|
|
||||||
/// 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::SmtpServerInfo;
|
|
||||||
use smtp::extension;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_smtp_server_info_fmt() {
|
|
||||||
assert_eq!(format!("{}", SmtpServerInfo{
|
|
||||||
name: "name",
|
|
||||||
esmtp_features: Some(vec!(extension::EightBitMime))
|
|
||||||
}), "name with [8BITMIME]".to_owned());
|
|
||||||
assert_eq!(format!("{}", SmtpServerInfo{
|
|
||||||
name: "name",
|
|
||||||
esmtp_features: Some(vec!(extension::EightBitMime, extension::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!(extension::EightBitMime, extension::Size(42))));
|
|
||||||
assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 UNKNON 42"),
|
|
||||||
Some(vec!(extension::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!(extension::Size(42), extension::Size(43))));
|
|
||||||
assert_eq!(SmtpServerInfo::parse_esmtp_response(""),
|
|
||||||
None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_smtp_server_info_supports_feature() {
|
|
||||||
assert_eq!(SmtpServerInfo{
|
|
||||||
name: "name",
|
|
||||||
esmtp_features: Some(vec!(extension::EightBitMime))
|
|
||||||
}.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime));
|
|
||||||
assert_eq!(SmtpServerInfo{
|
|
||||||
name: "name",
|
|
||||||
esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime))
|
|
||||||
}.supports_feature(extension::EightBitMime), Ok(extension::EightBitMime));
|
|
||||||
assert_eq!(SmtpServerInfo{
|
|
||||||
name: "name",
|
|
||||||
esmtp_features: Some(vec!(extension::Size(42), extension::EightBitMime))
|
|
||||||
}.supports_feature(extension::Size(0)), Ok(extension::Size(42)));
|
|
||||||
assert!(SmtpServerInfo{
|
|
||||||
name: "name",
|
|
||||||
esmtp_features: Some(vec!(extension::EightBitMime))
|
|
||||||
}.supports_feature(extension::Size(42)).is_err());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
100
src/common.rs
100
src/common.rs
@@ -1,100 +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 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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
|
|
||||||
|
|
||||||
use smtp::CRLF;
|
|
||||||
|
|
||||||
/// Adds quotes to emails if needed
|
|
||||||
pub fn quote_email_address(address: ~str) -> ~str {
|
|
||||||
match address.len() {
|
|
||||||
0..1 => format!("<{:s}>", address),
|
|
||||||
_ => match (address.slice_to(1), address.slice_from(address.len() - 1)) {
|
|
||||||
("<", ">") => address,
|
|
||||||
_ => format!("<{:s}>", address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes quotes from emails if needed
|
|
||||||
pub fn unquote_email_address(address: ~str) -> ~str {
|
|
||||||
match address.len() {
|
|
||||||
0..1 => address,
|
|
||||||
_ => match (address.slice_to(1), address.slice_from(address.len() - 1)) {
|
|
||||||
("<", ">") => address.slice(1, address.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.len() > 1 && string.slice_from(string.len() - 2) == CRLF {
|
|
||||||
string.slice_to(string.len() - 2).to_owned()
|
|
||||||
} else if string.len() > 0 && string.slice_from(string.len() - 1) == "\r" {
|
|
||||||
string.slice_to(string.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()), "<address>".to_owned());
|
|
||||||
assert_eq!(super::quote_email_address("<address>".to_owned()), "<address>".to_owned());
|
|
||||||
assert_eq!(super::quote_email_address("a".to_owned()), "<a>".to_owned());
|
|
||||||
assert_eq!(super::quote_email_address("".to_owned()), "<>".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unquote_email_address() {
|
|
||||||
assert_eq!(super::unquote_email_address("<address>".to_owned()), "address".to_owned());
|
|
||||||
assert_eq!(super::unquote_email_address("address".to_owned()), "address".to_owned());
|
|
||||||
assert_eq!(super::unquote_email_address("<address".to_owned()), "<address".to_owned());
|
|
||||||
assert_eq!(super::unquote_email_address("<>".to_owned()), "".to_owned());
|
|
||||||
assert_eq!(super::unquote_email_address("a".to_owned()), "a".to_owned());
|
|
||||||
assert_eq!(super::unquote_email_address("".to_owned()), "".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_trailing_crlf() {
|
|
||||||
assert_eq!(super::remove_trailing_crlf("word".to_owned()), "word".to_owned());
|
|
||||||
assert_eq!(super::remove_trailing_crlf("word\r\n".to_owned()), "word".to_owned());
|
|
||||||
assert_eq!(super::remove_trailing_crlf("word\r\n ".to_owned()), "word\r\n ".to_owned());
|
|
||||||
assert_eq!(super::remove_trailing_crlf("word\r".to_owned()), "word".to_owned());
|
|
||||||
assert_eq!(super::remove_trailing_crlf("\r\n".to_owned()), "".to_owned());
|
|
||||||
assert_eq!(super::remove_trailing_crlf("\r".to_owned()), "".to_owned());
|
|
||||||
assert_eq!(super::remove_trailing_crlf("a".to_owned()), "a".to_owned());
|
|
||||||
assert_eq!(super::remove_trailing_crlf("".to_owned()), "".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_first_word() {
|
|
||||||
assert_eq!(super::get_first_word("first word".to_owned()), "first".to_owned());
|
|
||||||
assert_eq!(super::get_first_word("first word\r\ntest".to_owned()), "first".to_owned());
|
|
||||||
assert_eq!(super::get_first_word("first".to_owned()), "first".to_owned());
|
|
||||||
assert_eq!(super::get_first_word("".to_owned()), "".to_owned());
|
|
||||||
assert_eq!(super::get_first_word("\r\n".to_owned()), "".to_owned());
|
|
||||||
assert_eq!(super::get_first_word("a\r\n".to_owned()), "a".to_owned());
|
|
||||||
// Manage cases of empty line, spaces at the beginning, ...
|
|
||||||
//assert_eq!(super::get_first_word(" a".to_owned()), "a".to_owned());
|
|
||||||
//assert_eq!(super::get_first_word("\r\n a".to_owned()), "a".to_owned());
|
|
||||||
assert_eq!(super::get_first_word(" \r\n".to_owned()), "".to_owned());
|
|
||||||
assert_eq!(super::get_first_word("\r\n ".to_owned()), "".to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
429
src/smtp.rs
429
src/smtp.rs
@@ -1,429 +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 <LICENSE-APACHE or
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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::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;
|
|
||||||
|
|
||||||
/// The word separator for SMTP transactions
|
|
||||||
pub static SP: &'static str = " ";
|
|
||||||
|
|
||||||
/// The line ending for SMTP transactions
|
|
||||||
pub static CRLF: &'static str = "\r\n";
|
|
||||||
|
|
||||||
/// A module
|
|
||||||
pub mod command {
|
|
||||||
use std::fmt::{Show, Formatter, Result};
|
|
||||||
|
|
||||||
/// 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<T> {
|
|
||||||
/// A fake command
|
|
||||||
Connect,
|
|
||||||
/// Extended Hello command
|
|
||||||
ExtendedHello(T),
|
|
||||||
/// Hello command
|
|
||||||
Hello(T),
|
|
||||||
/// Mail command, takes optionnal options
|
|
||||||
Mail(T, Option<Vec<T>>),
|
|
||||||
/// Recipient command, takes optionnal options
|
|
||||||
Recipient(T, Option<Vec<T>>),
|
|
||||||
/// Data command
|
|
||||||
Data,
|
|
||||||
/// Reset command
|
|
||||||
Reset,
|
|
||||||
/// Verify command
|
|
||||||
Verify(T),
|
|
||||||
/// Expand command
|
|
||||||
Expand(T),
|
|
||||||
/// Help command, takes optionnal options
|
|
||||||
Help(Option<T>),
|
|
||||||
/// Noop command
|
|
||||||
Noop,
|
|
||||||
/// Quit command
|
|
||||||
Quit,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Show + Str> Show for SmtpCommand<T> {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
f.write(match *self {
|
|
||||||
Connect => "CONNECT".to_owned(),
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is a module
|
|
||||||
pub mod extension {
|
|
||||||
use std::from_str::FromStr;
|
|
||||||
use std::fmt::{Show, Formatter, Result};
|
|
||||||
|
|
||||||
/// Supported ESMTP keywords
|
|
||||||
#[deriving(Eq,Clone)]
|
|
||||||
pub enum SmtpExtension {
|
|
||||||
/// 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 SmtpExtension {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
f.write(
|
|
||||||
match self {
|
|
||||||
&EightBitMime => "8BITMIME".to_owned(),
|
|
||||||
&Size(ref size) => format!("SIZE={}", size)
|
|
||||||
}.as_bytes()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for SmtpExtension {
|
|
||||||
fn from_str(s: &str) -> Option<SmtpExtension> {
|
|
||||||
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::<uint>(*splitted.get(1))) {
|
|
||||||
("SIZE", Some(size)) => Some(Size(size)),
|
|
||||||
_ => None
|
|
||||||
},
|
|
||||||
_ => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SmtpExtension {
|
|
||||||
/// Checks if the ESMTP keyword is the same
|
|
||||||
pub fn same_extension_as(&self, other: SmtpExtension) -> bool {
|
|
||||||
if *self == other {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
match (*self, other) {
|
|
||||||
(Size(_), Size(_)) => true,
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// This is a module
|
|
||||||
pub mod response {
|
|
||||||
use std::from_str::FromStr;
|
|
||||||
use std::fmt::{Show, Formatter, Result};
|
|
||||||
use common::remove_trailing_crlf;
|
|
||||||
use std::result;
|
|
||||||
|
|
||||||
/// 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<T> {
|
|
||||||
/// Server response code
|
|
||||||
pub code: u16,
|
|
||||||
/// Server response string
|
|
||||||
pub message: Option<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Show + Clone> Show for SmtpResponse<T> {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
|
||||||
f.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<StrBuf> {
|
|
||||||
fn from_str(s: &str) -> Option<SmtpResponse<StrBuf>> {
|
|
||||||
// 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::<u16>(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::<u16>(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<T: Clone> SmtpResponse<T> {
|
|
||||||
/// Checks the presence of the response code in the array of expected codes.
|
|
||||||
pub fn with_code(&self, expected_codes: Vec<u16>) -> result::Result<SmtpResponse<T>,SmtpResponse<T>> {
|
|
||||||
let response = self.clone();
|
|
||||||
if expected_codes.contains(&self.code) {
|
|
||||||
Ok(response)
|
|
||||||
} else {
|
|
||||||
Err(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// a module
|
|
||||||
pub mod transaction_state {
|
|
||||||
use std::fmt;
|
|
||||||
use std::fmt::{Show, Formatter};
|
|
||||||
use super::command;
|
|
||||||
use super::command::SmtpCommand;
|
|
||||||
|
|
||||||
/// Contains the state of the current transaction
|
|
||||||
#[deriving(Eq,Clone)]
|
|
||||||
pub enum TransactionState {
|
|
||||||
/// The connection was successful and the banner was received
|
|
||||||
OutOfTransaction,
|
|
||||||
/// An HELO or EHLO was successful
|
|
||||||
HelloSent,
|
|
||||||
/// A MAIL command was successful send
|
|
||||||
MailSent,
|
|
||||||
/// At least one RCPT command was sucessful
|
|
||||||
RecipientSent,
|
|
||||||
/// A DATA command was successful
|
|
||||||
DataSent
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Show for TransactionState {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
f.write(
|
|
||||||
match *self {
|
|
||||||
OutOfTransaction => "OutOfTransaction",
|
|
||||||
HelloSent => "HelloSent",
|
|
||||||
MailSent => "MailSent",
|
|
||||||
RecipientSent => "RecipientSent",
|
|
||||||
DataSent => "DataSent"
|
|
||||||
}.as_bytes()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransactionState {
|
|
||||||
/// bla bla
|
|
||||||
pub fn is_command_possible<T>(&self, command: SmtpCommand<T>) -> bool {
|
|
||||||
match (*self, command) {
|
|
||||||
// Only the message content can be sent in this state
|
|
||||||
(DataSent, _) => false,
|
|
||||||
// Commands that can be issued everytime
|
|
||||||
(_, command::ExtendedHello(_)) => true,
|
|
||||||
(_, command::Hello(_)) => true,
|
|
||||||
(_, command::Reset) => true,
|
|
||||||
(_, command::Verify(_)) => true,
|
|
||||||
(_, command::Expand(_)) => true,
|
|
||||||
(_, command::Help(_)) => true,
|
|
||||||
(_, command::Noop) => true,
|
|
||||||
(_, command::Quit) => true,
|
|
||||||
// Commands that require a particular state
|
|
||||||
(HelloSent, command::Mail(_, _)) => true,
|
|
||||||
(MailSent, command::Recipient(_, _)) => true,
|
|
||||||
(RecipientSent, command::Recipient(_, _)) => true,
|
|
||||||
(RecipientSent, command::Data) => true,
|
|
||||||
// Everything else
|
|
||||||
(_, _) => false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// a method
|
|
||||||
pub fn next_state<T>(&mut self, command: SmtpCommand<T>) -> Option<TransactionState> {
|
|
||||||
match (*self, command) {
|
|
||||||
(DataSent, _) => None,
|
|
||||||
// Commands that can be issued everytime
|
|
||||||
(_, command::ExtendedHello(_)) => Some(HelloSent),
|
|
||||||
(_, command::Hello(_)) => Some(HelloSent),
|
|
||||||
(_, command::Reset) => Some(OutOfTransaction),
|
|
||||||
(state, command::Verify(_)) => Some(state),
|
|
||||||
(state, command::Expand(_)) => Some(state),
|
|
||||||
(state, command::Help(_)) => Some(state),
|
|
||||||
(state, command::Noop) => Some(state),
|
|
||||||
(_, command::Quit) => Some(OutOfTransaction),
|
|
||||||
// Commands that require a particular state
|
|
||||||
(HelloSent, command::Mail(_, _)) => Some(MailSent),
|
|
||||||
(MailSent, command::Recipient(_, _)) => Some(RecipientSent),
|
|
||||||
(RecipientSent, command::Recipient(_, _)) => Some(RecipientSent),
|
|
||||||
(RecipientSent, command::Data) => Some(DataSent),
|
|
||||||
// Everything else
|
|
||||||
(_, _) => None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::response::SmtpResponse;
|
|
||||||
use super::extension;
|
|
||||||
use super::extension::SmtpExtension;
|
|
||||||
use super::command;
|
|
||||||
use super::command::SmtpCommand;
|
|
||||||
use super::transaction_state;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_command_fmt() {
|
|
||||||
let noop: SmtpCommand<StrBuf> = command::Noop;
|
|
||||||
assert_eq!(format!("{}", noop), "NOOP".to_owned());
|
|
||||||
assert_eq!(format!("{}", command::ExtendedHello("me")), "EHLO me".to_owned());
|
|
||||||
assert_eq!(format!("{}",
|
|
||||||
command::Mail("test", Some(vec!("option")))), "MAIL FROM:<test> option".to_owned()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extension_same_extension_as() {
|
|
||||||
assert_eq!(extension::EightBitMime.same_extension_as(extension::EightBitMime), true);
|
|
||||||
assert_eq!(extension::Size(42).same_extension_as(extension::Size(42)), true);
|
|
||||||
assert_eq!(extension::Size(42).same_extension_as(extension::Size(43)), true);
|
|
||||||
assert_eq!(extension::Size(42).same_extension_as(extension::EightBitMime), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extension_fmt() {
|
|
||||||
assert_eq!(format!("{}", extension::EightBitMime), "8BITMIME".to_owned());
|
|
||||||
assert_eq!(format!("{}", extension::Size(42)), "SIZE=42".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_extension_from_str() {
|
|
||||||
assert_eq!(from_str::<SmtpExtension>("8BITMIME"), Some(extension::EightBitMime));
|
|
||||||
assert_eq!(from_str::<SmtpExtension>("SIZE 42"), Some(extension::Size(42)));
|
|
||||||
assert_eq!(from_str::<SmtpExtension>("SIZ 42"), None);
|
|
||||||
assert_eq!(from_str::<SmtpExtension>("SIZE 4a2"), None);
|
|
||||||
// TODO: accept trailing spaces ?
|
|
||||||
assert_eq!(from_str::<SmtpExtension>("SIZE 42 "), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_response_fmt() {
|
|
||||||
assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message")}), "200 message".to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_response_from_str() {
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("200 response message"),
|
|
||||||
Some(SmtpResponse{
|
|
||||||
code: 200,
|
|
||||||
message: Some(StrBuf::from_str("response message"))
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("200-response message"),
|
|
||||||
Some(SmtpResponse{
|
|
||||||
code: 200,
|
|
||||||
message: Some(StrBuf::from_str("response message"))
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("200"),
|
|
||||||
Some(SmtpResponse{
|
|
||||||
code: 200,
|
|
||||||
message: None
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("200 "),
|
|
||||||
Some(SmtpResponse{
|
|
||||||
code: 200,
|
|
||||||
message: None
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("200-response\r\nmessage"),
|
|
||||||
Some(SmtpResponse{
|
|
||||||
code: 200,
|
|
||||||
message: Some(StrBuf::from_str("response\r\nmessage"))
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("2000response message"), None);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("20a response message"), None);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("20 "), None);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("20"), None);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>("2"), None);
|
|
||||||
assert_eq!(from_str::<SmtpResponse<StrBuf>>(""), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_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_transaction_state_is_command_possible() {
|
|
||||||
let noop: SmtpCommand<StrBuf> = command::Noop;
|
|
||||||
assert!(transaction_state::OutOfTransaction.is_command_possible(noop.clone()));
|
|
||||||
assert!(! transaction_state::DataSent.is_command_possible(noop));
|
|
||||||
assert!(transaction_state::HelloSent.is_command_possible(command::Mail("", None)));
|
|
||||||
assert!(! transaction_state::MailSent.is_command_possible(command::Mail("", None)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_transaction_state_next_state() {
|
|
||||||
let noop: SmtpCommand<StrBuf> = command::Noop;
|
|
||||||
assert_eq!(transaction_state::MailSent.next_state(noop), Some(transaction_state::MailSent));
|
|
||||||
assert_eq!(transaction_state::HelloSent.next_state(command::Mail("", None)), Some(transaction_state::MailSent));
|
|
||||||
assert_eq!(transaction_state::MailSent.next_state(command::Mail("", None)), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
src/smtpc/lib.rs
Normal file
0
src/smtpc/lib.rs
Normal file
93
src/smtpcommon/command.rs
Normal file
93
src/smtpcommon/command.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||||
|
// file at the top-level directory of this distribution.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||||
|
// option. This file may not be copied, modified, or distributed
|
||||||
|
// except according to those terms.
|
||||||
|
|
||||||
|
//! Represents a complete SMTP command, ready to be sent to a server
|
||||||
|
|
||||||
|
use std::fmt::{Show, Formatter, Result};
|
||||||
|
/// 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(PartialEq,Eq,Clone)]
|
||||||
|
pub enum SmtpCommand {
|
||||||
|
/// A fake command to represent the connection step
|
||||||
|
Connect,
|
||||||
|
/// Extended Hello command
|
||||||
|
ExtendedHello(String),
|
||||||
|
/// Hello command
|
||||||
|
Hello(String),
|
||||||
|
/// Mail command, takes optionnal options
|
||||||
|
Mail(String, Option<Vec<String>>),
|
||||||
|
/// Recipient command, takes optionnal options
|
||||||
|
Recipient(String, Option<Vec<String>>),
|
||||||
|
/// Data command
|
||||||
|
Data,
|
||||||
|
/// Reset command
|
||||||
|
Reset,
|
||||||
|
/// Verify command, takes optionnal options
|
||||||
|
Verify(String, Option<Vec<String>>),
|
||||||
|
/// Expand command, takes optionnal options
|
||||||
|
Expand(String, Option<Vec<String>>),
|
||||||
|
/// Help command, takes optionnal options
|
||||||
|
Help(Option<String>),
|
||||||
|
/// Noop command
|
||||||
|
Noop,
|
||||||
|
/// Quit command
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Show for SmtpCommand {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
f.write(match *self {
|
||||||
|
Connect => "CONNECT".to_string(),
|
||||||
|
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_string(),
|
||||||
|
Reset => "RSET".to_string(),
|
||||||
|
Verify(ref address, None) =>
|
||||||
|
format!("VRFY {}", address.clone()),
|
||||||
|
Verify(ref address, Some(ref options)) =>
|
||||||
|
format!("VRFY {} {}", address.clone(), options.connect(" ")),
|
||||||
|
Expand(ref address, None) =>
|
||||||
|
format!("EXPN {}", address.clone()),
|
||||||
|
Expand(ref address, Some(ref options)) =>
|
||||||
|
format!("EXPN {} {}", address.clone(), options.connect(" ")),
|
||||||
|
Help(None) => "HELP".to_string(),
|
||||||
|
Help(Some(ref argument)) =>
|
||||||
|
format!("HELP {}", argument.clone()),
|
||||||
|
Noop => "NOOP".to_string(),
|
||||||
|
Quit => "QUIT".to_string(),
|
||||||
|
}.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use command;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_command_fmt() {
|
||||||
|
assert_eq!(format!("{}", command::Noop), "NOOP".to_string());
|
||||||
|
assert_eq!(format!("{}", command::ExtendedHello("this".to_string())), "EHLO this".to_string());
|
||||||
|
assert_eq!(format!("{}",
|
||||||
|
command::Mail("test".to_string(), Some(vec!("option".to_string())))), "MAIL FROM:<test> option".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/smtpcommon/common.rs
Normal file
112
src/smtpcommon/common.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||||
|
// file at the top-level directory of this distribution.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||||
|
// option. This file may not be copied, modified, or distributed
|
||||||
|
// except according to those terms.
|
||||||
|
|
||||||
|
//! Contains mixed-up tools for SMTP
|
||||||
|
//!
|
||||||
|
//! TODO : Clean up and split this file
|
||||||
|
|
||||||
|
use std::io::net::ip::Port;
|
||||||
|
use std::string::String;
|
||||||
|
|
||||||
|
/// Default SMTP port
|
||||||
|
pub static SMTP_PORT: Port = 25;
|
||||||
|
//pub static SMTPS_PORT: Port = 465;
|
||||||
|
//pub static SUBMISSION_PORT: Port = 587;
|
||||||
|
|
||||||
|
/// The word separator for SMTP transactions
|
||||||
|
pub static SP: &'static str = " ";
|
||||||
|
|
||||||
|
/// The line ending for SMTP transactions
|
||||||
|
pub static CRLF: &'static str = "\r\n";
|
||||||
|
|
||||||
|
/// Adds quotes to emails if needed
|
||||||
|
pub fn quote_email_address(address: String) -> String {
|
||||||
|
match address.len() {
|
||||||
|
0..1 => format!("<{:s}>", address),
|
||||||
|
_ => match (address.as_slice().slice_to(1), address.as_slice().slice_from(address.len() - 1)) {
|
||||||
|
("<", ">") => address,
|
||||||
|
_ => format!("<{:s}>", address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes quotes from emails if needed
|
||||||
|
pub fn unquote_email_address(address: String) -> String {
|
||||||
|
match address.len() {
|
||||||
|
0..1 => address,
|
||||||
|
_ => match (address.as_slice().slice_to(1), address.as_slice().slice_from(address.len() - 1)) {
|
||||||
|
("<", ">") => address.as_slice().slice(1, address.len() - 1).to_string(),
|
||||||
|
_ => address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the trailing line return at the end of a string
|
||||||
|
pub fn remove_trailing_crlf(string: String) -> String {
|
||||||
|
if string.len() > 1 && string.as_slice().slice_from(string.len() - 2) == CRLF {
|
||||||
|
string.as_slice().slice_to(string.len() - 2).to_string()
|
||||||
|
} else if string.len() > 0 && string.as_slice().slice_from(string.len() - 1) == "\r" {
|
||||||
|
string.as_slice().slice_to(string.len() - 1).to_string()
|
||||||
|
} else {
|
||||||
|
string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first word of a string, or the string if it contains no space
|
||||||
|
pub fn get_first_word(string: String) -> String {
|
||||||
|
string.as_slice().split_str(CRLF).next().unwrap().splitn(' ', 1).next().unwrap().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
#[test]
|
||||||
|
fn test_quote_email_address() {
|
||||||
|
assert_eq!(super::quote_email_address("address".to_string()), "<address>".to_string());
|
||||||
|
assert_eq!(super::quote_email_address("<address>".to_string()), "<address>".to_string());
|
||||||
|
assert_eq!(super::quote_email_address("a".to_string()), "<a>".to_string());
|
||||||
|
assert_eq!(super::quote_email_address("".to_string()), "<>".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unquote_email_address() {
|
||||||
|
assert_eq!(super::unquote_email_address("<address>".to_string()), "address".to_string());
|
||||||
|
assert_eq!(super::unquote_email_address("address".to_string()), "address".to_string());
|
||||||
|
assert_eq!(super::unquote_email_address("<address".to_string()), "<address".to_string());
|
||||||
|
assert_eq!(super::unquote_email_address("<>".to_string()), "".to_string());
|
||||||
|
assert_eq!(super::unquote_email_address("a".to_string()), "a".to_string());
|
||||||
|
assert_eq!(super::unquote_email_address("".to_string()), "".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_remove_trailing_crlf() {
|
||||||
|
assert_eq!(super::remove_trailing_crlf("word".to_string()), "word".to_string());
|
||||||
|
assert_eq!(super::remove_trailing_crlf("word\r\n".to_string()), "word".to_string());
|
||||||
|
assert_eq!(super::remove_trailing_crlf("word\r\n ".to_string()), "word\r\n ".to_string());
|
||||||
|
assert_eq!(super::remove_trailing_crlf("word\r".to_string()), "word".to_string());
|
||||||
|
assert_eq!(super::remove_trailing_crlf("\r\n".to_string()), "".to_string());
|
||||||
|
assert_eq!(super::remove_trailing_crlf("\r".to_string()), "".to_string());
|
||||||
|
assert_eq!(super::remove_trailing_crlf("a".to_string()), "a".to_string());
|
||||||
|
assert_eq!(super::remove_trailing_crlf("".to_string()), "".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_first_word() {
|
||||||
|
assert_eq!(super::get_first_word("first word".to_string()), "first".to_string());
|
||||||
|
assert_eq!(super::get_first_word("first word\r\ntest".to_string()), "first".to_string());
|
||||||
|
assert_eq!(super::get_first_word("first".to_string()), "first".to_string());
|
||||||
|
assert_eq!(super::get_first_word("".to_string()), "".to_string());
|
||||||
|
assert_eq!(super::get_first_word("\r\n".to_string()), "".to_string());
|
||||||
|
assert_eq!(super::get_first_word("a\r\n".to_string()), "a".to_string());
|
||||||
|
// Manage cases of empty line, spaces at the beginning, ...
|
||||||
|
//assert_eq!(super::get_first_word(" a".to_string()), "a".to_string());
|
||||||
|
//assert_eq!(super::get_first_word("\r\n a".to_string()), "a".to_string());
|
||||||
|
assert_eq!(super::get_first_word(" \r\n".to_string()), "".to_string());
|
||||||
|
assert_eq!(super::get_first_word("\r\n ".to_string()), "".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/smtpcommon/extension.rs
Normal file
103
src/smtpcommon/extension.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||||
|
// file at the top-level directory of this distribution.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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::from_str::FromStr;
|
||||||
|
use std::fmt::{Show, Formatter, Result};
|
||||||
|
|
||||||
|
/// Supported ESMTP keywords
|
||||||
|
#[deriving(PartialEq,Eq,Clone)]
|
||||||
|
pub enum SmtpExtension {
|
||||||
|
/// 8BITMIME keyword
|
||||||
|
///
|
||||||
|
/// RFC 6152 : https://tools.ietf.org/html/rfc6152
|
||||||
|
EightBitMime,
|
||||||
|
/// SMTPUTF8 keyword
|
||||||
|
///
|
||||||
|
/// RFC 6531 : https://tools.ietf.org/html/rfc6531
|
||||||
|
SmtpUtfEight,
|
||||||
|
/// SIZE keyword
|
||||||
|
///
|
||||||
|
/// RFC 1427 : https://tools.ietf.org/html/rfc1427
|
||||||
|
Size(uint)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Show for SmtpExtension {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
f.write(
|
||||||
|
match self {
|
||||||
|
&EightBitMime => "8BITMIME".to_string(),
|
||||||
|
&SmtpUtfEight => "SMTPUTF8".to_string(),
|
||||||
|
&Size(ref size) => format!("SIZE={}", size)
|
||||||
|
}.as_bytes()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for SmtpExtension {
|
||||||
|
fn from_str(s: &str) -> Option<SmtpExtension> {
|
||||||
|
let splitted : Vec<&str> = s.splitn(' ', 1).collect();
|
||||||
|
match splitted.len() {
|
||||||
|
1 => match *splitted.get(0) {
|
||||||
|
"8BITMIME" => Some(EightBitMime),
|
||||||
|
"SMTPUTF8" => Some(SmtpUtfEight),
|
||||||
|
_ => None
|
||||||
|
},
|
||||||
|
2 => match (*splitted.get(0), from_str::<uint>(*splitted.get(1))) {
|
||||||
|
("SIZE", Some(size)) => Some(Size(size)),
|
||||||
|
_ => None
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmtpExtension {
|
||||||
|
/// Checks if the ESMTP keyword is the same
|
||||||
|
pub fn same_extension_as(&self, other: SmtpExtension) -> bool {
|
||||||
|
if *self == other {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
match (*self, other) {
|
||||||
|
(Size(_), Size(_)) => true,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use extension;
|
||||||
|
use extension::SmtpExtension;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extension_same_extension_as() {
|
||||||
|
assert_eq!(extension::EightBitMime.same_extension_as(extension::EightBitMime), true);
|
||||||
|
assert_eq!(extension::Size(42).same_extension_as(extension::Size(42)), true);
|
||||||
|
assert_eq!(extension::Size(42).same_extension_as(extension::Size(43)), true);
|
||||||
|
assert_eq!(extension::Size(42).same_extension_as(extension::EightBitMime), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extension_fmt() {
|
||||||
|
assert_eq!(format!("{}", extension::EightBitMime), "8BITMIME".to_string());
|
||||||
|
assert_eq!(format!("{}", extension::Size(42)), "SIZE=42".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extension_from_str() {
|
||||||
|
assert_eq!(from_str::<SmtpExtension>("8BITMIME"), Some(extension::EightBitMime));
|
||||||
|
assert_eq!(from_str::<SmtpExtension>("SIZE 42"), Some(extension::Size(42)));
|
||||||
|
assert_eq!(from_str::<SmtpExtension>("SIZ 42"), None);
|
||||||
|
assert_eq!(from_str::<SmtpExtension>("SIZE 4a2"), None);
|
||||||
|
// TODO: accept trailing spaces ?
|
||||||
|
assert_eq!(from_str::<SmtpExtension>("SIZE 42 "), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,25 +31,24 @@
|
|||||||
//! extern crate smtp;
|
//! extern crate smtp;
|
||||||
//! use std::io::net::tcp::TcpStream;
|
//! use std::io::net::tcp::TcpStream;
|
||||||
//! use smtp::client::SmtpClient;
|
//! use smtp::client::SmtpClient;
|
||||||
//! use std::strbuf::StrBuf;
|
//! use std::string::String;
|
||||||
//!
|
//!
|
||||||
//! let mut email_client: SmtpClient<StrBuf, TcpStream> =
|
//! let mut email_client: SmtpClient<String, TcpStream> =
|
||||||
//! SmtpClient::new(StrBuf::from_str("localhost"), None, None);
|
//! SmtpClient::new(String::from_str("localhost"), None, None);
|
||||||
//! email_client.send_mail(
|
//! email_client.send_mail(
|
||||||
//! StrBuf::from_str("user@example.com"),
|
//! String::from_str("user@example.com"),
|
||||||
//! vec!(StrBuf::from_str("user@example.org")),
|
//! vec!(String::from_str("user@example.org")),
|
||||||
//! StrBuf::from_str("Test email")
|
//! String::from_str("Test email")
|
||||||
//! );
|
//! );
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
#![crate_id = "smtp#0.1-pre"]
|
|
||||||
#![crate_type = "rlib"]
|
#![crate_type = "rlib"]
|
||||||
#![crate_type = "dylib"]
|
#![crate_type = "dylib"]
|
||||||
|
|
||||||
#![desc = "Rust SMTP client"]
|
#![desc = "Rust SMTP library"]
|
||||||
#![comment = "Simple SMTP client"]
|
#![comment = "Simple and modern SMTP library"]
|
||||||
#![license = "MIT/ASL2"]
|
#![license = "MIT/ASL2"]
|
||||||
#![doc(html_root_url = "http://amousset.github.io/rust-smtp/smtp/")]
|
#![doc(html_root_url = "http://amousset.github.io/rust-smtp/smtpcommon/")]
|
||||||
|
|
||||||
#![feature(macro_rules)]
|
#![feature(macro_rules)]
|
||||||
#![feature(phase)]
|
#![feature(phase)]
|
||||||
@@ -59,10 +58,9 @@
|
|||||||
#![deny(non_uppercase_statics)]
|
#![deny(non_uppercase_statics)]
|
||||||
#![deny(unnecessary_typecast)]
|
#![deny(unnecessary_typecast)]
|
||||||
#![deny(unused_result)]
|
#![deny(unused_result)]
|
||||||
#![deny(deprecated_owned_vector)]
|
|
||||||
|
|
||||||
#![feature(phase)] #[phase(syntax, link)] extern crate log;
|
|
||||||
|
|
||||||
pub mod smtp;
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod client;
|
pub mod command;
|
||||||
|
pub mod extension;
|
||||||
|
pub mod response;
|
||||||
|
pub mod transaction;
|
||||||
143
src/smtpcommon/response.rs
Normal file
143
src/smtpcommon/response.rs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||||
|
// file at the top-level directory of this distribution.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||||
|
// option. This file may not be copied, modified, or distributed
|
||||||
|
// except according to those terms.
|
||||||
|
|
||||||
|
//! SMTP responses, contaiing a mandatory return code, and an optional text message
|
||||||
|
|
||||||
|
use std::from_str::FromStr;
|
||||||
|
use std::fmt::{Show, Formatter, Result};
|
||||||
|
use common::remove_trailing_crlf;
|
||||||
|
use std::result;
|
||||||
|
|
||||||
|
/// Contains an SMTP reply, with separed code and message
|
||||||
|
///
|
||||||
|
/// We do accept messages containing only a code, to comply with RFC5321
|
||||||
|
#[deriving(PartialEq,Eq,Clone)]
|
||||||
|
pub struct SmtpResponse {
|
||||||
|
/// Server response code
|
||||||
|
pub code: u16,
|
||||||
|
/// Server response string
|
||||||
|
pub message: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Show for SmtpResponse {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
|
f.write(
|
||||||
|
match self.clone().message {
|
||||||
|
Some(message) => format!("{} {}", self.code, message),
|
||||||
|
None => format!("{}", self.code)
|
||||||
|
}.as_bytes()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromStr ?
|
||||||
|
impl FromStr for SmtpResponse {
|
||||||
|
fn from_str(s: &str) -> Option<SmtpResponse> {
|
||||||
|
// 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::<u16>(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::<u16>(s.slice_to(3)),
|
||||||
|
vec!(" ", "-").contains(&s.slice(3,4)),
|
||||||
|
(remove_trailing_crlf(s.slice_from(4).to_string()))
|
||||||
|
) {
|
||||||
|
(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.
|
||||||
|
pub fn with_code(&self, expected_codes: Vec<u16>) -> result::Result<SmtpResponse,SmtpResponse> {
|
||||||
|
let response = self.clone();
|
||||||
|
if expected_codes.contains(&self.code) {
|
||||||
|
Ok(response)
|
||||||
|
} else {
|
||||||
|
Err(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use response::SmtpResponse;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_fmt() {
|
||||||
|
assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message".to_string())}), "200 message".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_from_str() {
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("200 response message"),
|
||||||
|
Some(SmtpResponse{
|
||||||
|
code: 200,
|
||||||
|
message: Some("response message".to_string())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("200-response message"),
|
||||||
|
Some(SmtpResponse{
|
||||||
|
code: 200,
|
||||||
|
message: Some("response message".to_string())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("200"),
|
||||||
|
Some(SmtpResponse{
|
||||||
|
code: 200,
|
||||||
|
message: None
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("200 "),
|
||||||
|
Some(SmtpResponse{
|
||||||
|
code: 200,
|
||||||
|
message: None
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("200-response\r\nmessage"),
|
||||||
|
Some(SmtpResponse{
|
||||||
|
code: 200,
|
||||||
|
message: Some("response\r\nmessage".to_string())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("2000response message"), None);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("20a response message"), None);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("20 "), None);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("20"), None);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>("2"), None);
|
||||||
|
assert_eq!(from_str::<SmtpResponse>(""), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_response_with_code() {
|
||||||
|
assert_eq!(SmtpResponse{code: 200, message: Some("message".to_string())}.with_code(vec!(200)),
|
||||||
|
Ok(SmtpResponse{code: 200, message: Some("message".to_string())}));
|
||||||
|
assert_eq!(SmtpResponse{code: 400, message: Some("message".to_string())}.with_code(vec!(200)),
|
||||||
|
Err(SmtpResponse{code: 400, message: Some("message".to_string())}));
|
||||||
|
assert_eq!(SmtpResponse{code: 200, message: Some("message".to_string())}.with_code(vec!(200, 300)),
|
||||||
|
Ok(SmtpResponse{code: 200, message: Some("message".to_string())}));
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/smtpcommon/transaction.rs
Normal file
121
src/smtpcommon/transaction.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||||
|
// file at the top-level directory of this distribution.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||||
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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;
|
||||||
|
use std::fmt::{Show, Formatter};
|
||||||
|
use command;
|
||||||
|
use command::SmtpCommand;
|
||||||
|
|
||||||
|
/// Contains the state of the current transaction
|
||||||
|
#[deriving(PartialEq,Eq,Clone)]
|
||||||
|
pub enum TransactionState {
|
||||||
|
/// No connection was established
|
||||||
|
Unconnected,
|
||||||
|
/// The connection was successful and the banner was received
|
||||||
|
Connected,
|
||||||
|
/// An HELO or EHLO was successful
|
||||||
|
HelloSent,
|
||||||
|
/// A MAIL command was successful send
|
||||||
|
MailSent,
|
||||||
|
/// At least one RCPT command was sucessful
|
||||||
|
RecipientSent,
|
||||||
|
/// A DATA command was successful
|
||||||
|
DataSent
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Show for TransactionState {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
f.write(
|
||||||
|
match *self {
|
||||||
|
Unconnected => "Unconnected",
|
||||||
|
Connected => "Connected",
|
||||||
|
HelloSent => "HelloSent",
|
||||||
|
MailSent => "MailSent",
|
||||||
|
RecipientSent => "RecipientSent",
|
||||||
|
DataSent => "DataSent"
|
||||||
|
}.as_bytes()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionState {
|
||||||
|
/// bla bla
|
||||||
|
pub fn is_command_possible(&self, command: SmtpCommand) -> bool {
|
||||||
|
match (*self, command) {
|
||||||
|
(Unconnected, command::Connect) => true,
|
||||||
|
(Unconnected, _) => false,
|
||||||
|
// Only the message content can be sent in this state
|
||||||
|
(DataSent, _) => false,
|
||||||
|
// Commands that can be issued everytime
|
||||||
|
(_, command::ExtendedHello(_)) => true,
|
||||||
|
(_, command::Hello(_)) => true,
|
||||||
|
(_, command::Reset) => true,
|
||||||
|
(_, command::Verify(_, _)) => true,
|
||||||
|
(_, command::Expand(_, _)) => true,
|
||||||
|
(_, command::Help(_)) => true,
|
||||||
|
(_, command::Noop) => true,
|
||||||
|
(_, command::Quit) => true,
|
||||||
|
// Commands that require a particular state
|
||||||
|
(HelloSent, command::Mail(_, _)) => true,
|
||||||
|
(MailSent, command::Recipient(_, _)) => true,
|
||||||
|
(RecipientSent, command::Recipient(_, _)) => true,
|
||||||
|
(RecipientSent, command::Data) => true,
|
||||||
|
// Everything else
|
||||||
|
(_, _) => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a method
|
||||||
|
pub fn next_state(&mut self, command: SmtpCommand) -> Option<TransactionState> {
|
||||||
|
match (*self, command) {
|
||||||
|
(Unconnected, command::Connect) => Some(Connected),
|
||||||
|
(Unconnected, _) => None,
|
||||||
|
(DataSent, _) => None,
|
||||||
|
// Commands that can be issued everytime
|
||||||
|
(_, command::ExtendedHello(_)) => Some(HelloSent),
|
||||||
|
(_, command::Hello(_)) => Some(HelloSent),
|
||||||
|
(Connected, command::Reset) => Some(Connected),
|
||||||
|
(_, command::Reset) => Some(HelloSent),
|
||||||
|
(state, command::Verify(_, _)) => Some(state),
|
||||||
|
(state, command::Expand(_, _)) => Some(state),
|
||||||
|
(state, command::Help(_)) => Some(state),
|
||||||
|
(state, command::Noop) => Some(state),
|
||||||
|
(_, command::Quit) => Some(Unconnected),
|
||||||
|
// Commands that require a particular state
|
||||||
|
(HelloSent, command::Mail(_, _)) => Some(MailSent),
|
||||||
|
(MailSent, command::Recipient(_, _)) => Some(RecipientSent),
|
||||||
|
(RecipientSent, command::Recipient(_, _)) => Some(RecipientSent),
|
||||||
|
(RecipientSent, command::Data) => Some(DataSent),
|
||||||
|
// Everything else
|
||||||
|
(_, _) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use command;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transaction_state_is_command_possible() {
|
||||||
|
assert!(!super::Unconnected.is_command_possible(command::Noop));
|
||||||
|
assert!(!super::DataSent.is_command_possible(command::Noop));
|
||||||
|
assert!(super::HelloSent.is_command_possible(command::Mail("".to_string(), None)));
|
||||||
|
assert!(!super::MailSent.is_command_possible(command::Mail("".to_string(), None)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_super_next_state() {
|
||||||
|
assert_eq!(super::MailSent.next_state(command::Noop), Some(super::MailSent));
|
||||||
|
assert_eq!(super::HelloSent.next_state(command::Mail("".to_string(), None)), Some(super::MailSent));
|
||||||
|
assert_eq!(super::MailSent.next_state(command::Mail("".to_string(), None)), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user