Files
lettre/src/smtp/client.rs
2014-05-11 17:11:07 +02:00

666 lines
22 KiB
Rust

// 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::from_str::FromStr;
use std::str::from_utf8;
use std::result::Result;
use std::strbuf::StrBuf;
use std::io::{IoResult, Reader, Writer};
use std::io::net::ip::{SocketAddr, Port};
use std::io::net::tcp::TcpStream;
use std::io::net::addrinfo::get_host_addresses;
use common::{CRLF, get_first_word, unquote_email_address, remove_trailing_crlf};
use commands;
use commands::{SMTP_PORT, SmtpCommand, EsmtpParameter};
/// 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: uint,
/// Server response string
pub message: Option<T>
}
impl<T: Show + Clone> Show for SmtpResponse<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.buf.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::<uint>(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::<uint>(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.
fn with_code(&self, expected_codes: Vec<uint>) -> Result<SmtpResponse<T>,SmtpResponse<T>> {
let response = self.clone();
if expected_codes.contains(&self.code) {
Ok(response)
} else {
Err(response)
}
}
}
/// Information about an SMTP server
#[deriving(Clone)]
struct SmtpServerInfo<T> {
/// Server name
name: T,
/// ESMTP features supported by the server
esmtp_features: Option<Vec<EsmtpParameter>>
}
impl<T: Show> Show for SmtpServerInfo<T>{
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.buf.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<EsmtpParameter>> {
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::<EsmtpParameter>(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: EsmtpParameter) -> Result<EsmtpParameter, ()> {
match self.esmtp_features.clone() {
Some(esmtp_features) => {
for feature in esmtp_features.iter() {
if keyword.same_keyword_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
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, permits to check order againt RFCs
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) -> Result<SmtpResponse<StrBuf>, SmtpResponse<StrBuf>> {
if !self.stream.is_none() {
fail!("The connection is already established");
}
let ip = match get_host_addresses(self.host.clone().into_owned()) {
Ok(ip_vector) => *ip_vector.get(0), // TODO : select a random ip
Err(..) => fail!("Cannot resolve {:s}", self.host)
};
self.stream = match TcpStream::connect(SocketAddr{ip: ip, port: self.port}) {
Ok(stream) => Some(stream),
Err(..) => fail!("Cannot connect to {:s}:{:u}", self.host, self.port)
};
// Log the connection
info!("Connection established to {}[{}]:{}", self.my_hostname.clone(), ip, self.port);
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(_) => {},
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(commands::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) {
drop(self.stream.clone().unwrap());
}
/// 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(commands::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(commands::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(commands::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(commands::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(commands::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(commands::Quit).with_code(vec!(221)) {
Ok(response) => {
self.close();
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(commands::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(commands::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(commands::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::{SmtpResponse, SmtpServerInfo};
use commands;
#[test]
fn test_smtp_response_fmt() {
assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message")}), "200 message".to_owned());
}
#[test]
fn test_smtp_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_smtp_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_smtp_server_info_fmt() {
assert_eq!(format!("{}", SmtpServerInfo{
name: "name",
esmtp_features: Some(vec!(commands::EightBitMime))
}), "name with [8BITMIME]".to_owned());
assert_eq!(format!("{}", SmtpServerInfo{
name: "name",
esmtp_features: Some(vec!(commands::EightBitMime, commands::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!(commands::EightBitMime, commands::Size(42))));
assert_eq!(SmtpServerInfo::parse_esmtp_response("me\r\n250-8BITMIME\r\n250 UNKNON 42"),
Some(vec!(commands::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!(commands::Size(42), commands::Size(43))));
}
#[test]
fn test_smtp_server_info_supports_feature() {
assert_eq!(SmtpServerInfo{
name: "name",
esmtp_features: Some(vec!(commands::EightBitMime))
}.supports_feature(commands::EightBitMime), Ok(commands::EightBitMime));
assert_eq!(SmtpServerInfo{
name: "name",
esmtp_features: Some(vec!(commands::Size(42), commands::EightBitMime))
}.supports_feature(commands::EightBitMime), Ok(commands::EightBitMime));
assert_eq!(SmtpServerInfo{
name: "name",
esmtp_features: Some(vec!(commands::Size(42), commands::EightBitMime))
}.supports_feature(commands::Size(0)), Ok(commands::Size(42)));
assert!(SmtpServerInfo{
name: "name",
esmtp_features: Some(vec!(commands::EightBitMime))
}.supports_feature(commands::Size(42)).is_err());
}
}