Files
lettre/src/client/mod.rs
2015-02-28 19:39:47 +01:00

446 lines
14 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::ascii::AsciiExt;
use std::string::String;
use std::error::FromError;
use std::default::Default;
use std::old_io::net::tcp::TcpStream;
use std::old_io::net::ip::{SocketAddr, ToSocketAddr};
use uuid::Uuid;
use tools::get_first_word;
use common::{CRLF, MESSAGE_ENDING, SMTP_PORT};
use response::Response;
use extension::Extension;
use command::Command;
use transaction::TransactionState;
use error::{SmtpResult, ErrorKind};
use client::connecter::Connecter;
use client::server_info::ServerInfo;
use client::stream::ClientStream;
use sendable_email::SendableEmail;
pub mod server_info;
pub mod connecter;
pub mod stream;
/// Represents the configuration of a client
#[derive(Debug)]
pub struct Configuration {
/// Maximum connection reuse
///
/// Zero means no limitation
pub connection_reuse_count_limit: u16,
/// Enable connection reuse
pub enable_connection_reuse: bool,
/// Maximum line length
pub line_length_limit: u16,
/// Name sent during HELO or EHLO
pub hello_name: String,
}
/// Represents the state of a client
#[derive(Debug)]
pub struct State {
/// Transaction state, to check the sequence of commands
pub transaction_state: TransactionState,
/// Panic state
pub panic: bool,
/// Connection reuse counter
pub connection_reuse_count: u16,
/// Current message id
pub current_message: Option<Uuid>,
}
/// Structure that implements the SMTP client
pub struct Client<S = TcpStream> {
/// TCP stream between client and server
/// Value is None before connection
stream: Option<S>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
/// Client variable states
state: State,
/// Configuration of the client
configuration: Configuration,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => close_and_return_err!(err, $client),
}
})
);
macro_rules! close_and_return_err (
($err: expr, $client: ident) => ({
if !$client.state.panic {
$client.state.panic = true;
$client.close();
}
return Err(FromError::from_error($err))
})
);
macro_rules! check_command_sequence (
($command: ident $client: ident) => ({
if !$client.state.transaction_state.is_allowed(&$command) {
close_and_return_err!(
Response{code: 503, message: Some("Bad sequence of commands".to_string())}, $client
);
}
})
);
impl<S = TcpStream> Client<S> {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only create the `Client`
pub fn new<A: ToSocketAddr>(addr: A) -> Client<S> {
Client{
stream: None,
server_addr: addr.to_socket_addr().unwrap(),
server_info: None,
configuration: Configuration {
connection_reuse_count_limit: 100,
enable_connection_reuse: false,
line_length_limit: 998,
hello_name: "localhost".to_string(),
},
state: State {
transaction_state: Default::default(),
panic: false,
connection_reuse_count: 0,
current_message: None,
},
}
}
/// Creates a new local SMTP client to port 25
///
/// It does not connects to the server, but only create the `Client`
pub fn localhost() -> Client<S> {
Client::new(("localhost", SMTP_PORT))
}
/// Set the name used during HELO or EHLO
pub fn set_hello_name(&mut self, name: &str) {
self.configuration.hello_name = name.to_string()
}
/// Set the maximum number of emails sent using one connection
pub fn set_enable_connection_reuse(&mut self, enable: bool) {
self.configuration.enable_connection_reuse = enable
}
/// Set the maximum number of emails sent using one connection
pub fn set_connection_reuse_count_limit(&mut self, count: u16) {
self.configuration.connection_reuse_count_limit = count
}
}
impl<S: Connecter + ClientStream + Clone = TcpStream> Client<S> {
/// Closes the SMTP transaction if possible
pub fn close(&mut self) {
let _ = self.quit();
}
/// Reset the client state
pub fn reset(&mut self) {
// Close the SMTP transaction if needed
self.close();
// Reset the client state
self.stream = None;
self.state.transaction_state = Default::default();
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
self.state.current_message = None;
}
/// 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.noop().is_err() {
self.reset();
}
}
// Connect to the server if needed
if self.stream.is_none() || self.server_info.is_none() {
try!(self.connect());
// Extended Hello or Hello if needed
if let Err(error) = self.ehlo() {
match error.kind {
ErrorKind::PermanentError(Response{code: 550, message: _}) => {
try_smtp!(self.helo(), self);
},
_ => {
try_smtp!(Err(error), self)
},
};
}
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
}
self.state.current_message = Some(Uuid::new_v4());
email.set_message_id(format!("<{}@{}>", self.state.current_message.as_ref().unwrap(),
self.configuration.hello_name.clone()));
let from_address = email.from_address();
let to_addresses = email.to_addresses();
let message = email.message();
// Mail
try_smtp!(self.mail(from_address.as_slice()), self);
// Recipient
// TODO Return rejected addresses
// TODO Limit the number of recipients
for to_address in to_addresses.iter() {
try_smtp!(self.rcpt(to_address.as_slice()), self);
}
// Data
try_smtp!(self.data(), self);
// Message content
self.message(message.as_slice())
}
/// Connects to the configured server
pub fn connect(&mut self) -> SmtpResult {
let command = Command::Connect;
check_command_sequence!(command self);
// Connect should not be called when the client is already connected
if self.stream.is_some() {
close_and_return_err!("The connection is already established", self);
}
// Try to connect
self.stream = Some(try!(Connecter::connect(self.server_addr)));
// Log the connection
info!("connection established to {}",
self.stream.as_mut().unwrap().peer_name().unwrap());
let result = try!(self.stream.as_mut().unwrap().get_reply());
let checked_result = try!(command.test_success(result));
self.state.transaction_state = self.state.transaction_state.next_state(&command).unwrap();
Ok(checked_result)
}
/// Sends an SMTP command
fn command(&mut self, command: Command) -> SmtpResult {
// for now we do not support SMTPUTF8
if !command.is_ascii() {
close_and_return_err!("Non-ASCII string", self);
}
self.send_server(command, None)
}
/// Sends the email content
fn send_message(&mut self, message: &str) -> SmtpResult {
self.send_server(Command::Message, Some(message))
}
/// Sends content to the server, after checking the command sequence, and then
/// updates the transaction state
///
/// * If `message` is `None`, the given command will be formatted and sent to the server
/// * If `message` is `Some(str)`, the `str` string will be sent to the server
fn send_server(&mut self, command: Command, message: Option<&str>) -> SmtpResult {
check_command_sequence!(command self);
let result = try!(match message {
Some(message) => self.stream.as_mut().unwrap().send_and_get_response(
message, MESSAGE_ENDING
),
None => self.stream.as_mut().unwrap().send_and_get_response(
format! ("{}", command) .as_slice(), CRLF
),
});
let checked_result = try!(command.test_success(result));
self.state.transaction_state = self.state.transaction_state.next_state(&command).unwrap();
Ok(checked_result)
}
/// Checks if the server is connected using the NOOP SMTP command
pub fn is_connected(&mut self) -> bool {
self.noop().is_ok()
}
/// Send a HELO command and fills `server_info`
pub fn helo(&mut self) -> SmtpResult {
let hostname = self.configuration.hello_name.clone();
let result = try!(self.command(Command::Hello(hostname)));
self.server_info = Some(
ServerInfo{
name: get_first_word(result.message.as_ref().unwrap().as_slice()).to_string(),
esmtp_features: vec!(),
}
);
Ok(result)
}
/// Sends a EHLO command and fills `server_info`
pub fn ehlo(&mut self) -> SmtpResult {
let hostname = self.configuration.hello_name.clone();
let result = try!(self.command(Command::ExtendedHello(hostname)));
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()
),
}
);
Ok(result)
}
/// Sends a MAIL command
pub fn mail(&mut self, from_address: &str) -> SmtpResult {
// Generate an ID for the logs if None was provided
if self.state.current_message.is_none() {
self.state.current_message = Some(Uuid::new_v4());
}
let server_info = match self.server_info.clone() {
Some(info) => info,
None => close_and_return_err!(Response{
code: 503,
message: Some("Bad sequence of commands".to_string()),
}, self),
};
// Checks message encoding according to the server's capability
let options = match server_info.supports_feature(Extension::EightBitMime) {
Some(extension) => Some(vec![extension.client_mail_option().unwrap().to_string()]),
None => None,
};
let result = self.command(
Command::Mail(from_address.to_string(), options)
);
if result.is_ok() {
// Log the mail command
info!("{}: from=<{}>", self.state.current_message.as_ref().unwrap(), from_address);
}
result
}
/// Sends a RCPT command
pub fn rcpt(&mut self, to_address: &str) -> SmtpResult {
let result = self.command(
Command::Recipient(to_address.to_string(), None)
);
if result.is_ok() {
// Log the rcpt command
info!("{}: to=<{}>", self.state.current_message.as_ref().unwrap(), to_address);
}
result
}
/// Sends a DATA command
pub fn data(&mut self) -> SmtpResult {
self.command(Command::Data)
}
/// Sends the message content
pub fn message(&mut self, message_content: &str) -> SmtpResult {
let server_info = match self.server_info.clone() {
Some(info) => info,
None => close_and_return_err!(Response{
code: 503,
message: Some("Bad sequence of commands".to_string()),
}, self)
};
// Check message encoding
if !server_info.supports_feature(Extension::EightBitMime).is_some() {
if !message_content.as_bytes().is_ascii() {
close_and_return_err!("Server does not accepts UTF-8 strings", self);
}
}
let result = self.send_message(message_content);
if result.is_ok() {
// Increment the connection reuse counter
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
// Log the message
info!("{}: conn_use={}, size={}, status=sent ({})", self.state.current_message.as_ref().unwrap(),
self.state.connection_reuse_count, message_content.len(), result.as_ref().ok().unwrap());
}
self.state.current_message = None;
// Test if we can reuse the existing connection
if (!self.configuration.enable_connection_reuse) ||
(self.state.connection_reuse_count == self.configuration.connection_reuse_count_limit) {
self.reset();
}
result
}
/// Sends a QUIT command
pub fn quit(&mut self) -> SmtpResult {
self.command(Command::Quit)
}
/// Sends a RSET command
pub fn rset(&mut self) -> SmtpResult {
self.command(Command::Reset)
}
/// Sends a NOOP command
pub fn noop(&mut self) -> SmtpResult {
self.command(Command::Noop)
}
/// Sends a VRFY command
pub fn vrfy(&mut self, to_address: &str) -> SmtpResult {
self.command(Command::Verify(to_address.to_string()))
}
/// Sends a EXPN command
pub fn expn(&mut self, list: &str) -> SmtpResult {
self.command(Command::Expand(list.to_string()))
}
}