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,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"))
}
}
}