Reorganize the SMTP client
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#[crate_id = "client"];
|
||||
|
||||
extern mod smtp;
|
||||
extern crate smtp;
|
||||
use std::io::net::tcp::TcpStream;
|
||||
use smtp::client::SmtpClient;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ email_client.send_mail("user@example.org", [&"user@localhost"], "Message content
|
||||
|
||||
# TODO
|
||||
|
||||
Support ESMTP : Parse server answer, and manage mail and rcpt options.
|
||||
Support ESMTP : Parse server answer, and manage mail and rcpt options.
|
||||
|
||||
* Client options: `mail_options` and `rcpt_options` lists
|
||||
|
||||
@@ -25,14 +25,25 @@ Support SSL/TLS
|
||||
|
||||
use std::fmt;
|
||||
use std::str::from_utf8;
|
||||
use std::result::Result;
|
||||
use std::io::{IoResult, IoError};
|
||||
use std::io::net::ip::{SocketAddr, Port};
|
||||
use std::io::net::tcp::TcpStream;
|
||||
use std::io::net::addrinfo::get_host_addresses;
|
||||
use common::{SMTP_PORT, CRLF};
|
||||
use commands;
|
||||
use commands::{Command, SmtpCommand};
|
||||
|
||||
// // Define smtp_fail! and smtp_success!
|
||||
// macro_rules! smtp_fail(
|
||||
// ($command:expr $code:ident $message:expr) => (
|
||||
// fail!("{} failed: {:u} {:s}", $command, $code, $message);
|
||||
// );
|
||||
// )
|
||||
|
||||
|
||||
/// Contains an SMTP reply, with separed code and message
|
||||
#[deriving(Eq,Clone)]
|
||||
pub struct SmtpResponse {
|
||||
/// Server respinse code code
|
||||
code: uint,
|
||||
@@ -55,14 +66,14 @@ impl fmt::Show for SmtpResponse {
|
||||
}
|
||||
|
||||
impl SmtpResponse {
|
||||
/// Check the repsonse code and fail if there is an error
|
||||
fn check_response(&self, expected_codes: &[uint]) {
|
||||
/// Check the response code
|
||||
fn with_code(&self, expected_codes: &[uint]) -> Result<SmtpResponse,SmtpResponse> {
|
||||
for &code in expected_codes.iter() {
|
||||
if code == self.code {
|
||||
return;
|
||||
return Ok(SmtpResponse{code: self.code, message: self.message.clone()});
|
||||
}
|
||||
}
|
||||
fail!("Failed with {}", self.to_str());
|
||||
return Err(SmtpResponse{code: self.code, message: self.message.clone()});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,14 +86,35 @@ pub struct SmtpClient<S> {
|
||||
/// Port we are connecting on
|
||||
port: Port,
|
||||
/// Our hostname for HELO/EHLO commands
|
||||
my_hostname: ~str
|
||||
my_hostname: ~str,
|
||||
/// Does the server supports ESMTP
|
||||
does_esmtp: Option<bool>,
|
||||
/// ESMTP features supported by the server
|
||||
esmtp_features: Option<~[~str]>
|
||||
}
|
||||
|
||||
impl<S: Reader + Writer + Clone> SmtpClient<S> {
|
||||
impl<S> SmtpClient<S> {
|
||||
/// Create a new SMTP client
|
||||
pub fn new(host: &str, port: Option<Port>, my_hostname: Option<&str>) -> SmtpClient<S> {
|
||||
SmtpClient{
|
||||
stream: None,
|
||||
host: host.to_owned(),
|
||||
port: port.unwrap_or(SMTP_PORT),
|
||||
my_hostname: my_hostname.unwrap_or("localhost").to_owned(),
|
||||
does_esmtp: None,
|
||||
esmtp_features: None
|
||||
}
|
||||
}
|
||||
|
||||
// fn parse_ehello_or_hello_response(response: &str) {
|
||||
// // split
|
||||
// }
|
||||
}
|
||||
|
||||
impl SmtpClient<TcpStream> {
|
||||
/// Send an SMTP command
|
||||
pub fn send_command(&mut self, command: commands::Command, option: Option<~str>) -> SmtpResponse {
|
||||
self.send_and_get_response(commands::SmtpCommand::new(command, option).get_formatted_command())
|
||||
pub fn send_command(&mut self, command: Command, option: Option<~str>) -> SmtpResponse {
|
||||
self.send_and_get_response(SmtpCommand::new(command, option).to_str())
|
||||
}
|
||||
|
||||
/// Send an email
|
||||
@@ -92,7 +124,8 @@ impl<S: Reader + Writer + Clone> SmtpClient<S> {
|
||||
|
||||
/// Send a complete message or a command to the server and get the response
|
||||
fn send_and_get_response(&mut self, string: ~str) -> SmtpResponse {
|
||||
match (&mut self.stream.clone().unwrap() as &mut Writer).write_str(format!("{:s}{:s}", string, CRLF)) {
|
||||
match (&mut self.stream.clone().unwrap() as &mut Writer)
|
||||
.write_str(format!("{:s}{:s}", string, CRLF)) {
|
||||
Err(..) => fail!("Could not write to stream"),
|
||||
Ok(..) => debug!("Write success")
|
||||
}
|
||||
@@ -105,7 +138,7 @@ impl<S: Reader + Writer + Clone> SmtpClient<S> {
|
||||
|
||||
/// Get the SMTP response
|
||||
fn get_reply(&mut self) -> Option<SmtpResponse> {
|
||||
let response = match self.stream.clone().unwrap().read_to_str() {
|
||||
let response = match self.read_to_str() {
|
||||
Err(..) => fail!("No answer"),
|
||||
Ok(string) => string
|
||||
};
|
||||
@@ -120,59 +153,93 @@ impl<S: Reader + Writer + Clone> SmtpClient<S> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new SMTP client
|
||||
pub fn new(host: &str, port: Option<Port>, my_hostname: Option<&str>) -> SmtpClient<S> {
|
||||
SmtpClient{
|
||||
stream: None,
|
||||
host: host.to_owned(),
|
||||
port: port.unwrap_or(SMTP_PORT),
|
||||
my_hostname: my_hostname.unwrap_or("localhost").to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpClient<TcpStream> {
|
||||
|
||||
/// 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().check_response([220]);
|
||||
self.send_command(commands::Hello, Some(my_hostname)).check_response([250]);
|
||||
self.send_command(commands::Mail, Some(from_addr.to_owned())).check_response([250]);
|
||||
for &to_addr in to_addrs.iter() {
|
||||
self.send_command(commands::Recipient, Some(to_addr.to_owned())).check_response([250]);
|
||||
}
|
||||
self.send_command(commands::Data, None).check_response([354]);
|
||||
self.send_message(message.to_owned()).check_response([250]);
|
||||
self.send_command(commands::Quit, None).check_response([221]);
|
||||
}
|
||||
|
||||
/// Connect to the configured server
|
||||
pub fn connect(&mut self) -> SmtpResponse {
|
||||
|
||||
if !self.stream.is_none() {
|
||||
fail!("The connection is already established");
|
||||
}
|
||||
|
||||
let ip = match get_host_addresses(self.host.clone()) {
|
||||
Ok(ip_vector) => ip_vector[0],
|
||||
Err(..) => fail!("Cannot resolve {}", self.host)
|
||||
};
|
||||
|
||||
self.stream = match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) {
|
||||
Err(..) => fail!("Cannot connect to {}:{}", self.host, self.port),
|
||||
Ok(stream) => Some(stream)
|
||||
};
|
||||
|
||||
match self.get_reply() {
|
||||
None => fail!("No banner on {}", self.host),
|
||||
Some(response) => response
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a QUIT command and end the program
|
||||
fn smtp_fail(&mut self, command: ~str, response: SmtpResponse) {
|
||||
self.send_command(commands::Quit, None);
|
||||
fail!("{} failed: {:u} {:s}", command, response.code, response.message);
|
||||
}
|
||||
|
||||
/// Send an email
|
||||
pub fn send_mail(&mut self, from_addr: &str, to_addrs: &[&str], message: &str) {
|
||||
let my_hostname = self.my_hostname.clone();
|
||||
|
||||
// Connect
|
||||
match self.connect().with_code([220]) {
|
||||
Ok(response) => info!("{:u} {:s}", response.code, response.message),
|
||||
Err(response) => self.smtp_fail(~"CONNECT", response)
|
||||
}
|
||||
|
||||
// Ehello or Hello
|
||||
match self.send_command(commands::Ehello, Some(my_hostname.clone())).with_code([250, 500]) {
|
||||
Ok(SmtpResponse{code: 250, message: message}) => {
|
||||
self.does_esmtp = Some(true);
|
||||
info!("{:u} {:s}", 250u, message);
|
||||
},
|
||||
Ok(SmtpResponse{code: code, message: message}) => {
|
||||
self.does_esmtp = Some(false);
|
||||
info!("{:u} {:s}", code, message);
|
||||
match self.send_command(commands::Ehello, Some(my_hostname.clone())).with_code([250]) {
|
||||
Ok(response) => info!("{:u} {:s}", response.code, response.message),
|
||||
Err(response) => self.smtp_fail(~"HELO", response)
|
||||
}
|
||||
},
|
||||
Err(response) => self.smtp_fail(~"EHLO", response)
|
||||
}
|
||||
|
||||
// Mail
|
||||
match self.send_command(commands::Mail, Some(from_addr.to_owned())).with_code([250]) {
|
||||
Ok(response) => info!("{:u} {:s}", response.code, response.message),
|
||||
Err(response) => self.smtp_fail(~"MAIL", response)
|
||||
}
|
||||
|
||||
// Recipient
|
||||
for &to_addr in to_addrs.iter() {
|
||||
match self.send_command(commands::Recipient, Some(to_addr.to_owned())).with_code([250]) {
|
||||
Ok(response) => info!("{:u} {:s}", response.code, response.message),
|
||||
Err(response) => self.smtp_fail(~"RCPT", response)
|
||||
}
|
||||
}
|
||||
|
||||
// Data
|
||||
match self.send_command(commands::Data, None).with_code([354]) {
|
||||
Ok(response) => info!("{:u} {:s}", response.code, response.message),
|
||||
Err(response) => self.smtp_fail(~"DATA", response)
|
||||
}
|
||||
|
||||
// Message content
|
||||
match self.send_message(message.to_owned()).with_code([250]) {
|
||||
Ok(response) => info!("{:u} {:s}", response.code, response.message),
|
||||
Err(response) => self.smtp_fail(~"MESSAGE", response)
|
||||
}
|
||||
|
||||
// Quit
|
||||
match self.send_command(commands::Quit, None).with_code([221]) {
|
||||
Ok(response) => info!("{:u} {:s}", response.code, response.message),
|
||||
Err(response) => self.smtp_fail(~"DATA", response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Reader for SmtpClient<TcpStream> {
|
||||
|
||||
/// Read a string from the client socket
|
||||
fn read(&mut self, buf: &mut [u8]) -> IoResult<uint> {
|
||||
self.stream.clone().unwrap().read(buf)
|
||||
@@ -193,7 +260,6 @@ impl Reader for SmtpClient<TcpStream> {
|
||||
}
|
||||
|
||||
impl Writer for SmtpClient<TcpStream> {
|
||||
|
||||
/// Send a string on the client socket
|
||||
fn write(&mut self, buf: &[u8]) -> IoResult<()> {
|
||||
self.stream.clone().unwrap().write(buf)
|
||||
|
||||
@@ -27,21 +27,35 @@ use std::io;
|
||||
/// List of SMTP commands
|
||||
#[deriving(Eq,Clone)]
|
||||
pub enum Command {
|
||||
/// Hello command
|
||||
Hello,
|
||||
/// Ehello command
|
||||
Ehello,
|
||||
/// Mail command
|
||||
Mail,
|
||||
/// Recipient command
|
||||
Recipient,
|
||||
/// Data command
|
||||
Data,
|
||||
/// Reset command
|
||||
Reset,
|
||||
/// SendMail command
|
||||
SendMail,
|
||||
/// SendOrMail command
|
||||
SendOrMail,
|
||||
/// SendAndMail command
|
||||
SendAndMail,
|
||||
/// Verify command
|
||||
Verify,
|
||||
/// Expand command
|
||||
Expand,
|
||||
/// Help command
|
||||
Help,
|
||||
/// Noop command
|
||||
Noop,
|
||||
/// Quit command
|
||||
Quit,
|
||||
/// Deprecated in RFC 5321
|
||||
/// Turn command, deprecated in RFC 5321
|
||||
Turn,
|
||||
}
|
||||
|
||||
@@ -164,7 +178,9 @@ impl fmt::Show for Command {
|
||||
|
||||
/// Structure for a complete SMTP command, containing an optionnal string argument.
|
||||
pub struct SmtpCommand {
|
||||
/// The SMTP command (e.g. MAIL, QUIT, ...)
|
||||
command: Command,
|
||||
/// An optionnal argument to the command
|
||||
argument: Option<~str>
|
||||
}
|
||||
|
||||
@@ -177,20 +193,31 @@ impl SmtpCommand {
|
||||
_ => SmtpCommand {command: command, argument: argument}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStr for SmtpCommand {
|
||||
/// Return the formatted command, ready to be used in an SMTP session.
|
||||
pub fn get_formatted_command(&self) -> ~str {
|
||||
fn to_str(&self) -> ~str {
|
||||
match (self.command.takes_argument(), self.command.needs_argument(), self.argument.clone()) {
|
||||
(true, _, Some(argument)) => format!("{} {}", self.command, argument),
|
||||
(_, false, None) => format!("{}", self.command),
|
||||
_ => fail!("Wrong SMTP syntax")
|
||||
(true, _, Some(argument)) => format!("{} {}", self.command, argument),
|
||||
(_, false, None) => format!("{}", self.command),
|
||||
_ => fail!("Wrong SMTP syntax")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Show for SmtpCommand {
|
||||
/// Return the formatted command, ready to be used in an SMTP session.
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), io::IoError> {
|
||||
f.buf.write(
|
||||
self.to_str().as_bytes()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SmtpCommand;
|
||||
use super::{Command, SmtpCommand};
|
||||
|
||||
#[test]
|
||||
fn test_command_parameters() {
|
||||
@@ -199,13 +226,28 @@ mod test {
|
||||
assert!((super::Hello).needs_argument() == true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_str() {
|
||||
assert!(super::Turn.to_str() == ~"TURN");
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_from_str() {
|
||||
// assert!(from_str == ~"TURN");
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_fmt() {
|
||||
assert!(format!("{}", super::Turn) == ~"TURN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_simple_command() {
|
||||
assert!(SmtpCommand::new(super::Turn, None).get_formatted_command() == ~"TURN");
|
||||
assert!(SmtpCommand::new(super::Turn, None).to_str() == ~"TURN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_argument_command() {
|
||||
assert!(SmtpCommand::new(super::Ehello, Some(~"example.example")).get_formatted_command() == ~"EHLO example.example");
|
||||
assert!(SmtpCommand::new(super::Ehello, Some(~"example.example")).to_str() == ~"EHLO example.example");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ pub fn unquote_email_address(addr: &str) -> ~str {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
#[test]
|
||||
fn test_quote_email_address() {
|
||||
assert!(super::quote_email_address("plop") == ~"<plop>");
|
||||
@@ -44,4 +43,4 @@ mod test {
|
||||
assert!(super::unquote_email_address("<plop>") == ~"plop");
|
||||
assert!(super::unquote_email_address("plop") == ~"plop");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
/*!
|
||||
* SMTP library
|
||||
*
|
||||
* For now, contains only a basic and uncomplete SMTP client and some common general functions.
|
||||
*/
|
||||
|
||||
#[crate_id = "smtp#0.1-pre"];
|
||||
|
||||
#[comment = "Rust SMTP client"];
|
||||
#[license = "MIT/ASL2"];
|
||||
#[license = "ASL2"];
|
||||
#[crate_type = "lib"];
|
||||
|
||||
//#[crate_type = "dylib"];
|
||||
//#[crate_type = "rlib"];
|
||||
|
||||
#[deny(non_camel_case_types)];
|
||||
//#[deny(missing_doc)];
|
||||
#[deny(missing_doc)];
|
||||
|
||||
pub mod commands;
|
||||
pub mod common;
|
||||
|
||||
Reference in New Issue
Block a user