Update to new io and improve reply handling

This commit is contained in:
Alexis Mousset
2015-03-13 01:04:38 +01:00
parent 3a86f09475
commit 24e6eeb9d2
10 changed files with 376 additions and 384 deletions

View File

@@ -7,7 +7,7 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
#![feature(core, old_io, rustc_private, collections)]
#![feature(core, old_io, net, rustc_private, collections)]
#[macro_use]
extern crate log;
@@ -16,17 +16,19 @@ extern crate smtp;
extern crate getopts;
use std::old_io::stdin;
use std::old_io::net::ip::Port;
use std::string::String;
use std::env;
use getopts::{optopt, optflag, getopts, OptGroup, usage};
use std::net::TcpStream;
use smtp::client::ClientBuilder;
use smtp::client::{Client, ClientBuilder};
use smtp::error::SmtpResult;
use smtp::mailer::EmailBuilder;
fn sendmail(source_address: String, recipient_addresses: Vec<String>, message: String, subject: String,
server: String, port: Port, my_hostname: String, number: u16) -> SmtpResult {
server: String, port: u16, my_hostname: String, number: u16) -> SmtpResult {
let mut email_builder = EmailBuilder::new();
for destination in recipient_addresses.iter() {
@@ -37,7 +39,7 @@ fn sendmail(source_address: String, recipient_addresses: Vec<String>, message: S
.subject(subject.as_slice())
.build();
let mut client = ClientBuilder::new((server.as_slice(), port)).hello_name(my_hostname.as_slice())
let mut client: Client<TcpStream> = ClientBuilder::new((server.as_slice(), port)).hello_name(my_hostname.as_slice())
.enable_connection_reuse(true).build();
for _ in range(1, number) {
@@ -135,7 +137,7 @@ fn main() {
},
// port
match matches.opt_str("p") {
Some(port) => port.as_slice().parse::<Port>().unwrap(),
Some(port) => port.as_slice().parse::<u16>().unwrap(),
None => 25,
},
// my hostname

View File

@@ -7,28 +7,20 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// Taken fron rust-http
//! TODO
use std::old_io::IoResult;
use std::old_io::net::ip::SocketAddr;
use std::old_io::net::tcp::TcpStream;
use std::io;
use std::net::SocketAddr;
use std::net::TcpStream;
/// A trait for the concept of opening a stream connected to a IP socket address.
pub trait Connecter {
/// TODO
fn connect(addr: SocketAddr) -> IoResult<Self>;
/// TODO
fn peer_name(&mut self) -> IoResult<SocketAddr>;
fn connect(addr: &SocketAddr) -> io::Result<Self>;
}
impl Connecter for TcpStream {
fn connect(addr: SocketAddr) -> IoResult<TcpStream> {
fn connect(addr: &SocketAddr) -> io::Result<TcpStream> {
TcpStream::connect(addr)
}
fn peer_name(&mut self) -> IoResult<SocketAddr> {
self.peer_name()
}
}

View File

@@ -9,11 +9,11 @@
//! SMTP client
use std::slice::Iter;
use std::string::String;
use std::error::FromError;
use std::old_io::net::tcp::TcpStream;
use std::old_io::net::ip::{SocketAddr, ToSocketAddr};
use std::net::TcpStream;
use std::net::{SocketAddr, ToSocketAddrs};
use std::io::{BufRead, BufStream, Read, Write};
use uuid::Uuid;
use serialize::base64::{self, ToBase64, FromBase64};
@@ -23,19 +23,17 @@ use crypto::md5::Md5;
use crypto::mac::Mac;
use SMTP_PORT;
use tools::get_first_word;
use tools::{NUL, CRLF, MESSAGE_ENDING};
use response::Response;
use tools::{escape_dot, escape_crlf};
use response::{Response, Severity, Category};
use extension::Extension;
use error::{SmtpResult, ErrorKind};
use error::{SmtpResult, SmtpError};
use sendable_email::SendableEmail;
use client::connecter::Connecter;
use client::server_info::ServerInfo;
use client::stream::ClientStream;
mod server_info;
mod connecter;
mod stream;
/// Contains client configuration
pub struct ClientBuilder {
@@ -56,9 +54,9 @@ pub struct ClientBuilder {
/// Builder for the SMTP Client
impl ClientBuilder {
/// Creates a new local SMTP client
pub fn new<A: ToSocketAddr>(addr: A) -> ClientBuilder {
pub fn new<A: ToSocketAddrs>(addr: A) -> ClientBuilder {
ClientBuilder {
server_addr: addr.to_socket_addr().ok().expect("could not parse server address"),
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
credentials: None,
connection_reuse_count_limit: 100,
enable_connection_reuse: false,
@@ -98,7 +96,7 @@ impl ClientBuilder {
/// Build the SMTP client
///
/// It does not connects to the server, but only creates the `Client`
pub fn build(self) -> Client {
pub fn build<S: Connecter + Read + Write>(self) -> Client<S> {
Client::new(self)
}
}
@@ -116,7 +114,7 @@ struct State {
pub struct Client<S = TcpStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<S>,
stream: Option<BufStream<S>>,
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
@@ -145,16 +143,14 @@ macro_rules! close_and_return_err (
})
);
macro_rules! with_code (
($result: ident, $codes: expr) => ({
macro_rules! check_response (
($result: ident) => ({
match $result {
Ok(response) => {
for code in $codes {
if *code == response.code {
return Ok(response);
}
match response.is_positive() {
true => Ok(response),
false => Err(FromError::from_error(response)),
}
Err(FromError::from_error(response))
},
Err(_) => $result,
}
@@ -178,7 +174,7 @@ impl<S = TcpStream> Client<S> {
}
}
impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
impl<S: Connecter + Write + Read = TcpStream> Client<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.quit();
@@ -198,7 +194,6 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// Sends an email
pub fn send<T: SendableEmail>(&mut self, mut email: T) -> SmtpResult {
// If there is a usable connection, test if the server answers and hello has been sent
if self.state.connection_reuse_count > 0 {
if !self.is_connected() {
@@ -211,13 +206,12 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
try!(self.connect());
// Log the connection
info!("connection established to {}",
self.stream.as_mut().unwrap().peer_name().unwrap());
info!("connection established to {}", self.client_info.server_addr);
// Extended Hello or Hello if needed
if let Err(error) = self.ehlo() {
match error.kind {
ErrorKind::PermanentError(Response{code: 550, message: _}) => {
match error {
SmtpError::PermanentError(ref response) if response.has_code(550) => {
try_smtp!(self.helo(), self);
},
_ => {
@@ -281,7 +275,11 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
// Log the message
info!("{}: conn_use={}, size={}, status=sent ({})", current_message,
self.state.connection_reuse_count, message.len(), result.as_ref().ok().unwrap());
self.state.connection_reuse_count, message.len(), match result.as_ref().ok().unwrap().message().as_slice() {
[ref line, ..] => line.as_slice(),
[] => "no response",
}
);
}
// Test if we can reuse the existing connection
@@ -301,35 +299,28 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
}
// Try to connect
self.stream = Some(try!(Connecter::connect(self.client_info.server_addr)));
self.stream = Some(BufStream::new(try!(Connecter::connect(&self.client_info.server_addr))));
let result = self.stream.as_mut().unwrap().get_reply();
with_code!(result, [220].iter())
}
/// Sends content to the server
fn send_server(&mut self, content: &str, end: &str, expected_codes: Iter<u16>) -> SmtpResult {
let result = self.stream.as_mut().unwrap().send_and_get_response(content, end);
with_code!(result, expected_codes)
self.get_reply()
}
/// Checks if the server is connected using the NOOP SMTP command
fn is_connected(&mut self) -> bool {
pub fn is_connected(&mut self) -> bool {
self.noop().is_ok()
}
/// Sends an SMTP command
fn command(&mut self, command: &str, expected_codes: Iter<u16>) -> SmtpResult {
self.send_server(command, CRLF, expected_codes)
pub fn command(&mut self, command: &str) -> SmtpResult {
self.send_server(command, CRLF)
}
/// Send a HELO command and fills `server_info`
fn helo(&mut self) -> SmtpResult {
pub fn helo(&mut self) -> SmtpResult {
let hostname = self.client_info.hello_name.clone();
let result = try!(self.command(format!("HELO {}", hostname).as_slice(), [250].iter()));
let result = try!(self.command(format!("HELO {}", hostname).as_slice()));
self.server_info = Some(
ServerInfo{
name: get_first_word(result.message.as_ref().unwrap().as_slice()).to_string(),
name: result.first_word().expect("Server announced no hostname"),
esmtp_features: vec![],
}
);
@@ -337,60 +328,81 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
}
/// Sends a EHLO command and fills `server_info`
fn ehlo(&mut self) -> SmtpResult {
pub fn ehlo(&mut self) -> SmtpResult {
let hostname = self.client_info.hello_name.clone();
let result = try!(self.command(format!("EHLO {}", hostname).as_slice(), [250].iter()));
let result = try!(self.command(format!("EHLO {}", hostname).as_slice()));
self.server_info = Some(
ServerInfo{
name: get_first_word(result.message.as_ref().unwrap().as_slice()).to_string(),
esmtp_features: Extension::parse_esmtp_response(
result.message.as_ref().unwrap().as_slice()
),
name: result.first_word().expect("Server announced no hostname"),
esmtp_features: Extension::parse_esmtp_response(&result),
}
);
Ok(result)
}
/// Sends a MAIL command
fn mail(&mut self, address: &str) -> SmtpResult {
pub fn mail(&mut self, address: &str) -> SmtpResult {
// Checks message encoding according to the server's capability
let options = match self.server_info.as_ref().unwrap().supports_feature(Extension::EightBitMime) {
true => "BODY=8BITMIME",
false => "",
};
self.command(format!("MAIL FROM:<{}> {}", address, options).as_slice(), [250].iter())
self.command(format!("MAIL FROM:<{}> {}", address, options).as_slice())
}
/// Sends a RCPT command
fn rcpt(&mut self, address: &str) -> SmtpResult {
self.command(format!("RCPT TO:<{}>", address).as_slice(), [250, 251].iter())
pub fn rcpt(&mut self, address: &str) -> SmtpResult {
self.command(format!("RCPT TO:<{}>", address).as_slice())
}
/// Sends a DATA command
fn data(&mut self) -> SmtpResult {
self.command("DATA", [354].iter())
pub fn data(&mut self) -> SmtpResult {
self.command("DATA")
}
/// Sends a QUIT command
fn quit(&mut self) -> SmtpResult {
self.command("QUIT", [221].iter())
pub fn quit(&mut self) -> SmtpResult {
self.command("QUIT")
}
/// Sends a NOOP command
fn noop(&mut self) -> SmtpResult {
self.command("NOOP", [250].iter())
pub fn noop(&mut self) -> SmtpResult {
self.command("NOOP")
}
/// Sends a HELP command
pub fn help(&mut self, argument: Option<&str>) -> SmtpResult {
match argument {
Some(ref argument) => self.command(format!("HELP {}", argument).as_slice()),
None => self.command("HELP"),
}
}
/// Sends a VRFY command
pub fn vrfy(&mut self, address: &str) -> SmtpResult {
self.command(format!("VRFY {}", address).as_slice())
}
/// Sends a EXPN command
pub fn expn(&mut self, address: &str) -> SmtpResult {
self.command(format!("EXPN {}", address).as_slice())
}
/// Sends a RSET command
pub fn rset(&mut self) -> SmtpResult {
self.command("RSET")
}
/// Sends an AUTH command with PLAIN mecanism
fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
let auth_string = format!("{}{}{}{}", NUL, username, NUL, password);
self.command(format!("AUTH PLAIN {}", auth_string.as_bytes().to_base64(base64::STANDARD)).as_slice(), [235].iter())
self.command(format!("AUTH PLAIN {}", auth_string.as_bytes().to_base64(base64::STANDARD)).as_slice())
}
/// Sends an AUTH command with CRAM-MD5 mecanism
fn auth_cram_md5(&mut self, username: &str, password: &str) -> SmtpResult {
let encoded_challenge = try_smtp!(self.command("AUTH CRAM-MD5", [334].iter()), self).message.unwrap();
pub fn auth_cram_md5(&mut self, username: &str, password: &str) -> SmtpResult {
let encoded_challenge = try_smtp!(self.command("AUTH CRAM-MD5"), self).first_word().expect("No challenge");
// TODO manage errors
let challenge = encoded_challenge.from_base64().unwrap();
@@ -399,11 +411,58 @@ impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
let auth_string = format!("{} {}", username, hmac.result().code().to_hex());
self.command(format!("AUTH CRAM-MD5 {}", auth_string.as_bytes().to_base64(base64::STANDARD)).as_slice(), [235].iter())
self.command(format!("AUTH CRAM-MD5 {}", auth_string.as_bytes().to_base64(base64::STANDARD)).as_slice())
}
/// Sends the message content and close
fn message(&mut self, message_content: &str) -> SmtpResult {
self.send_server(message_content, MESSAGE_ENDING, [250].iter())
pub fn message(&mut self, message_content: &str) -> SmtpResult {
self.send_server(escape_dot(message_content).as_slice(), MESSAGE_ENDING)
}
/// Sends a string to the server and gets the response
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
try!(self.stream.as_mut().unwrap().flush());
debug!("Wrote: {}", escape_crlf(string));
self.get_reply()
}
/// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult {
let mut line = String::new();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
// If the string is too short to be a response code
if line.len() < 3 {
return Err(FromError::from_error("Could not parse reply code, line too short"));
}
let (severity, category, detail) = match (line[0..1].parse::<Severity>(), line[1..2].parse::<Category>(), line[2..3].parse::<u8>()) {
(Ok(severity), Ok(category), Ok(detail)) => (severity, category, detail),
_ => return Err(FromError::from_error("Could not parse reply code")),
};
let mut message = Vec::new();
// 3 chars for code + space + CRLF
while line.len() > 6 {
let end = line.len() - 2;
message.push(line[4..end].to_string());
if line.as_bytes()[3] == '-' as u8 {
line.clear();
try!(self.stream.as_mut().unwrap().read_line(&mut line));
} else {
line.clear();
}
}
let response = Response::new(severity, category, detail, message);
match response.is_positive() {
true => Ok(response),
false => Err(FromError::from_error(response)),
}
}
}

View File

@@ -1,80 +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.
//! TODO
use std::old_io::net::tcp::TcpStream;
use std::old_io::IoResult;
use std::str::from_utf8;
use std::vec::Vec;
use std::error::FromError;
use error::SmtpResult;
use response::Response;
use tools::{escape_dot, escape_crlf};
static BUFFER_SIZE: usize = 1024;
/// TODO
pub trait ClientStream {
/// TODO
fn send_and_get_response(&mut self, string: &str, end: &str) -> SmtpResult;
/// TODO
fn get_reply(&mut self) -> SmtpResult;
/// TODO
fn read_into_string(&mut self) -> IoResult<String>;
}
impl ClientStream for TcpStream {
/// Sends a string to the server and gets the response
fn send_and_get_response(&mut self, string: &str, end: &str) -> SmtpResult {
try!(self.write_str(format!("{}{}", escape_dot(string), end).as_slice()));
debug!("Wrote: {}", escape_crlf(escape_dot(string).as_slice()));
self.get_reply()
}
/// Reads on the stream into a string
fn read_into_string(&mut self) -> IoResult<String> {
let mut more = true;
let mut result = String::new();
// TODO: Set appropriate timeouts
self.set_timeout(Some(1000));
while more {
let mut buf: Vec<u8> = Vec::with_capacity(BUFFER_SIZE);
let response = match self.push(BUFFER_SIZE, &mut buf) {
Ok(bytes_read) => {
more = bytes_read == BUFFER_SIZE;
if bytes_read > 0 {
from_utf8(&buf[..bytes_read]).unwrap()
} else {
""
}
},
// TODO: Manage error kinds
Err(..) => {more = false; ""},
};
result.push_str(response);
}
debug!("Read: {}", escape_crlf(result.as_slice()));
return Ok(result);
}
/// Gets the SMTP response
fn get_reply(&mut self) -> SmtpResult {
let response = try!(self.read_into_string());
match response.as_slice().parse::<Response>() {
Ok(response) => Ok(response),
Err(_) => Err(FromError::from_error("Could not parse response"))
}
}
}

View File

@@ -10,17 +10,17 @@
//! Error and result type for SMTP clients
use std::error::Error;
use std::old_io::IoError;
use std::io;
use std::error::FromError;
use std::fmt::{Display, Formatter};
use std::fmt::Error as FmtError;
use std::fmt;
use response::Response;
use self::ErrorKind::{TransientError, PermanentError, UnknownError, InternalIoError};
use response::{Severity, Response};
use self::SmtpError::*;
/// An enum of all error kinds.
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum ErrorKind {
pub enum SmtpError {
/// Transient error, 4xx reply code
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
@@ -29,98 +29,55 @@ pub enum ErrorKind {
///
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
PermanentError(Response),
/// Unknown error
UnknownError(String),
/// TODO
ClientError(String),
/// IO error
InternalIoError(IoError),
IoError(io::Error),
}
/// smtp error type
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct SmtpError {
/// Error kind
pub kind: ErrorKind,
/// Error description
pub desc: &'static str,
/// Error cause
pub detail: Option<String>,
impl Display for SmtpError {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
fmt.write_str(self.description())
}
}
impl FromError<IoError> for SmtpError {
fn from_error(err: IoError) -> SmtpError {
SmtpError {
kind: InternalIoError(err),
desc: "An internal IO error ocurred.",
detail: None,
impl Error for SmtpError {
fn description(&self) -> &str {
match *self {
TransientError(_) => "a transient error occured during the SMTP transaction",
PermanentError(_) => "a permanent error occured during the SMTP transaction",
ClientError(_) => "an unknown error occured",
IoError(_) => "an I/O error occured",
}
}
fn cause(&self) -> Option<&Error> {
match *self {
IoError(ref err) => Some(&*err as &Error),
_ => None,
}
}
}
impl FromError<(ErrorKind, &'static str)> for SmtpError {
fn from_error((kind, desc): (ErrorKind, &'static str)) -> SmtpError {
SmtpError {
kind: kind,
desc: desc,
detail: None,
}
impl FromError<io::Error> for SmtpError {
fn from_error(err: io::Error) -> SmtpError {
IoError(err)
}
}
impl FromError<Response> for SmtpError {
fn from_error(response: Response) -> SmtpError {
let kind = match response.code/100 {
4 => TransientError(response),
5 => PermanentError(response),
_ => UnknownError(format! ("{:?}", response)),
};
let desc = match kind {
TransientError(_) => "a transient error occured during the SMTP transaction",
PermanentError(_) => "a permanent error occured during the SMTP transaction",
UnknownError(_) => "an unknown error occured during the SMTP transaction",
InternalIoError(_) => "an I/O error occurred",
};
SmtpError {
kind: kind,
desc: desc,
detail: None,
match response.severity() {
Severity::TransientNegativeCompletion => TransientError(response),
Severity::PermanentNegativeCompletion => PermanentError(response),
_ => ClientError("Unknown error code".to_string())
}
}
}
impl FromError<&'static str> for SmtpError {
fn from_error(string: &'static str) -> SmtpError {
SmtpError {
kind: UnknownError(string.to_string()),
desc: "an unknown error occured during the SMTP transaction",
detail: None,
}
}
}
impl Display for SmtpError {
fn fmt (&self, fmt: &mut Formatter) -> Result<(), FmtError> {
match self.kind {
TransientError(ref response) => write! (fmt, "{:?}", response),
PermanentError(ref response) => write! (fmt, "{:?}", response),
UnknownError(ref string) => write! (fmt, "{}", string),
InternalIoError(ref err) => write! (fmt, "{:?}", err.detail),
}
}
}
impl Error for SmtpError {
fn description(&self) -> &str {
match self.kind {
InternalIoError(ref err) => err.desc,
_ => self.desc,
}
}
fn cause(&self) -> Option<&Error> {
match self.kind {
InternalIoError(ref err) => Some(&*err as &Error),
_ => None,
}
ClientError(string.to_string())
}
}

View File

@@ -12,9 +12,8 @@
use std::str::FromStr;
use std::result::Result;
use tools::CRLF;
use response::Response;
use self::Extension::{PlainAuthentication, CramMd5Authentication, EightBitMime, SmtpUtfEight, StartTls};
use self::Extension::*;
/// Supported ESMTP keywords
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
@@ -64,15 +63,15 @@ impl Extension {
}
/// Parses supported ESMTP features
pub fn parse_esmtp_response(message: &str) -> Vec<Extension> {
pub fn parse_esmtp_response(response: &Response) -> Vec<Extension> {
let mut esmtp_features: Vec<Extension> = Vec::new();
for line in message.split(CRLF) {
if let Ok(Response{code: 250, message}) = line.parse::<Response>() {
if let Ok(keywords) = Extension::from_str(message.unwrap().as_slice()) {
esmtp_features.push_all(&keywords);
};
}
for line in response.message() {
if let Ok(keywords) = Extension::from_str(line.as_slice()) {
esmtp_features.push_all(&keywords);
};
}
esmtp_features
}
}
@@ -90,17 +89,17 @@ mod test {
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
}
#[test]
fn test_parse_esmtp_response() {
assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 SIZE 42"),
vec![Extension::EightBitMime]);
assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 AUTH PLAIN CRAM-MD5\r\n250 UNKNON 42"),
vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
assert_eq!(Extension::parse_esmtp_response("me\r\n250-9BITMIME\r\n250 SIZE a"),
vec![]);
assert_eq!(Extension::parse_esmtp_response("me\r\n250-SIZE 42\r\n250 SIZE 43"),
vec![]);
assert_eq!(Extension::parse_esmtp_response(""),
vec![]);
}
// #[test]
// fn test_parse_esmtp_response() {
// assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 SIZE 42"),
// vec![Extension::EightBitMime]);
// assert_eq!(Extension::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 AUTH PLAIN CRAM-MD5\r\n250 UNKNON 42"),
// vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
// assert_eq!(Extension::parse_esmtp_response("me\r\n250-9BITMIME\r\n250 SIZE a"),
// vec![]);
// assert_eq!(Extension::parse_esmtp_response("me\r\n250-SIZE 42\r\n250 SIZE 43"),
// vec![]);
// assert_eq!(Extension::parse_esmtp_response(""),
// vec![]);
// }
}

View File

@@ -30,8 +30,9 @@
//! This is the most basic example of usage:
//!
//! ```rust,no_run
//! use smtp::client::ClientBuilder;
//! use smtp::client::{Client, ClientBuilder};
//! use smtp::mailer::EmailBuilder;
//! use std::net::TcpStream;
//!
//! // Create an email
//! let email = EmailBuilder::new()
@@ -44,7 +45,7 @@
//! .build();
//!
//! // Open a local connection on port 25
//! let mut client = ClientBuilder::localhost().build();
//! let mut client: Client<TcpStream> = ClientBuilder::localhost().build();
//! // Send the email
//! let result = client.send(email);
//!
@@ -54,8 +55,9 @@
//! ### Complete example
//!
//! ```rust,no_run
//! use smtp::client::ClientBuilder;
//! use smtp::client::{Client, ClientBuilder};
//! use smtp::mailer::EmailBuilder;
//! use std::net::TcpStream;
//!
//! let mut builder = EmailBuilder::new();
//! builder = builder.to(("user@example.org", "Alias name"));
@@ -71,7 +73,7 @@
//! let email = builder.build();
//!
//! // Connect to a remote server on a custom port
//! let mut client = ClientBuilder::new(("server.tld", 10025))
//! let mut client: Client<TcpStream> = ClientBuilder::new(("server.tld", 10025))
//! // Set the name sent during EHLO/HELO, default is `localhost`
//! .hello_name("my.hostname.tld")
//! // Add credentials for authentication
@@ -93,8 +95,9 @@
//! If you just want to send an email without using `Email` to provide headers:
//!
//! ```rust,no_run
//! use smtp::client::ClientBuilder;
//! use smtp::client::{Client, ClientBuilder};
//! use smtp::sendable_email::SimpleSendableEmail;
//! use std::net::TcpStream;
//!
//! // Create a minimal email
//! let email = SimpleSendableEmail::new(
@@ -103,12 +106,12 @@
//! "Hello world !"
//! );
//!
//! let mut client = ClientBuilder::localhost().build();
//! let mut client: Client<TcpStream> = ClientBuilder::localhost().build();
//! let result = client.send(email);
//! assert!(result.is_ok());
//! ```
#![feature(plugin, core, old_io, io, collections)]
#![feature(plugin, core, io, collections, net, str_words)]
#![deny(missing_docs)]
#[macro_use] extern crate log;
@@ -125,16 +128,14 @@ pub mod error;
pub mod sendable_email;
pub mod mailer;
use std::old_io::net::ip::Port;
// Registrated port numbers:
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port
pub static SMTP_PORT: Port = 25;
pub static SMTP_PORT: u16 = 25;
/// Default smtps port
pub static SMTPS_PORT: Port = 465;
pub static SMTPS_PORT: u16 = 465;
/// Default submission port
pub static SUBMISSION_PORT: Port = 587;
pub static SUBMISSION_PORT: u16 = 587;

View File

@@ -13,109 +13,189 @@ use std::str::FromStr;
use std::fmt::{Display, Formatter, Result};
use std::result::Result as RResult;
use tools::remove_trailing_crlf;
use self::Severity::*;
use self::Category::*;
/// First digit indicates severity
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Severity {
/// 2yx
PositiveCompletion,
/// 3yz
PositiveIntermediate,
/// 4yz
TransientNegativeCompletion,
/// 5yz
PermanentNegativeCompletion,
}
impl FromStr for Severity {
type Err = &'static str;
fn from_str(s: &str) -> RResult<Severity, &'static str> {
match s {
"2" => Ok(PositiveCompletion),
"3" => Ok(PositiveIntermediate),
"4" => Ok(TransientNegativeCompletion),
"5" => Ok(PermanentNegativeCompletion),
_ => Err("First digit must be between 2 and 5"),
}
}
}
impl Display for Severity {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
match *self {
PositiveCompletion => 2,
PositiveIntermediate => 3,
TransientNegativeCompletion => 4,
PermanentNegativeCompletion => 5,
}
)
}
}
/// Second digit
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
pub enum Category {
/// x0z
Syntax,
/// x1z
Information,
/// x2z
Connections,
/// x3z
Unspecified3,
/// x4z
Unspecified4,
/// x5z
MailSystem,
}
impl FromStr for Category {
type Err = &'static str;
fn from_str(s: &str) -> RResult<Category, &'static str> {
match s {
"0" => Ok(Syntax),
"1" => Ok(Information),
"2" => Ok(Connections),
"3" => Ok(Unspecified3),
"4" => Ok(Unspecified4),
"5" => Ok(MailSystem),
_ => Err("Second digit must be between 0 and 5"),
}
}
}
impl Display for Category {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}",
match *self {
Syntax => 0,
Information => 1,
Connections => 2,
Unspecified3 => 3,
Unspecified4 => 4,
MailSystem => 5,
}
)
}
}
/// Contains an SMTP reply, with separed code and message
///
/// The text message is optional, only the code is mandatory
#[derive(PartialEq,Eq,Clone,Debug)]
pub struct Response {
/// Server response code
pub code: u16,
/// First digit of the response code
severity: Severity,
/// Second digit of the response code
category: Category,
/// Third digit
detail: u8,
/// Server response string (optional)
pub message: Option<String>
/// Handle multiline responses
message: Vec<String>
}
impl Display for Response {
fn fmt(&self, f: &mut Formatter) -> Result {
write! (f, "{}",
match self.clone().message {
Some(message) => format!("{} {}", self.code, message),
None => format!("{}", self.code),
}
let code = self.code();
for line in self.message[..-1].iter() {
let _ = write!(f, "{}-{}",
code,
line
);
}
write!(f, "{} {}",
code,
self.message[-1]
)
}
}
impl FromStr for Response {
type Err = &'static str;
fn from_str(s: &str) -> RResult<Response, &'static str> {
// If the string is too short to be a response code
if s.len() < 3 {
Err("len < 3")
// If we have only a code, with or without a trailing space
} else if s.len() == 3 || (s.len() == 4 && &s[3..4] == " ") {
match s[..3].parse::<u16>() {
Ok(code) => Ok(Response{
code: code,
message: None
}),
Err(_) => Err("Can't parse the code"),
}
// If we have a code and a message
} else {
match (
s[..3].parse::<u16>(),
vec![" ", "-"].contains(&&s[3..4]),
(remove_trailing_crlf(&s[4..]))
) {
(Ok(code), true, message) => Ok(Response{
code: code,
message: Some(message.to_string())
}),
_ => Err("Error parsing a code with a message"),
}
impl Response {
/// Creates a new `Response`
pub fn new(severity: Severity, category: Category, detail: u8, message: Vec<String>) -> Response {
Response {
severity: severity,
category: category,
detail: detail,
message: message
}
}
/// Tells if the response is positive
pub fn is_positive(&self) -> bool {
match self.severity {
PositiveCompletion => true,
PositiveIntermediate => true,
_ => false,
}
}
/// Returns the message
pub fn message(&self) -> Vec<String> {
self.message.clone()
}
/// Returns the severity (i.e. 1st digit)
pub fn severity(&self) -> Severity {
self.severity
}
/// Returns the category (i.e. 2nd digit)
pub fn category(&self) -> Category {
self.category
}
/// Returns the detail (i.e. 3rd digit)
pub fn detail(&self) -> u8 {
self.detail
}
/// Returns the reply code
fn code(&self) -> String {
format!("{}{}{}", self.severity, self.category, self.detail)
}
/// Checls code equality
pub fn has_code(&self, code: u16) -> bool {
self.code() == format!("{}", code)
}
/// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<String> {
match self.message.is_empty() {
true => None,
false => Some(self.message[0].words().next().unwrap().to_string())
}
}
}
#[cfg(test)]
mod test {
use super::Response;
#[test]
fn test_fmt() {
assert_eq!(format!("{}", Response{code: 200, message: Some("message".to_string())}),
"200 message".to_string());
}
#[test]
fn test_from_str() {
assert_eq!("200 response message".parse::<Response>(),
Ok(Response{
code: 200,
message: Some("response message".to_string())
})
);
assert_eq!("200-response message".parse::<Response>(),
Ok(Response{
code: 200,
message: Some("response message".to_string())
})
);
assert_eq!("200".parse::<Response>(),
Ok(Response{
code: 200,
message: None
})
);
assert_eq!("200 ".parse::<Response>(),
Ok(Response{
code: 200,
message: None
})
);
assert_eq!("200-response\r\nmessage".parse::<Response>(),
Ok(Response{
code: 200,
message: Some("response\r\nmessage".to_string())
})
);
assert!("2000response message".parse::<Response>().is_err());
assert!("20a response message".parse::<Response>().is_err());
assert!("20 ".parse::<Response>().is_err());
assert!("20".parse::<Response>().is_err());
assert!("2".parse::<Response>().is_err());
assert!("".parse::<Response>().is_err());
}
// TODO
}

View File

@@ -32,22 +32,16 @@ pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub static NUL: &'static str = "\0";
/// Removes the trailing line return at the end of a string
#[inline]
pub fn remove_trailing_crlf(string: &str) -> &str {
if string.ends_with(CRLF) {
&string[.. string.len() - 2]
} else if string.ends_with(CR) {
&string[.. string.len() - 1]
} else {
string
}
}
/// Returns the first word of a string, or the string if it contains no space
#[inline]
pub fn get_first_word(string: &str) -> &str {
string.split(CRLF).next().unwrap().splitn(1, ' ').next().unwrap()
match string.lines_any().next() {
Some(line) => match line.words().next() {
Some(word) => word,
None => "",
},
None => "",
}
}
/// Returns the string replacing all the CRLF with "\<CRLF\>"
@@ -71,19 +65,7 @@ pub fn escape_dot(string: &str) -> String {
#[cfg(test)]
mod test {
use super::{remove_trailing_crlf, get_first_word, escape_crlf, escape_dot};
#[test]
fn test_remove_trailing_crlf() {
assert_eq!(remove_trailing_crlf("word"), "word");
assert_eq!(remove_trailing_crlf("word\r\n"), "word");
assert_eq!(remove_trailing_crlf("word\r\n "), "word\r\n ");
assert_eq!(remove_trailing_crlf("word\r"), "word");
assert_eq!(remove_trailing_crlf("\r\n"), "");
assert_eq!(remove_trailing_crlf("\r"), "");
assert_eq!(remove_trailing_crlf("a"), "a");
assert_eq!(remove_trailing_crlf(""), "");
}
use super::*;
#[test]
fn test_get_first_word() {