Files
lettre/src/transport/smtp/mod.rs
2015-10-25 15:29:40 +01:00

374 lines
12 KiB
Rust

//! Sends an email using the client
use std::string::String;
use std::net::{SocketAddr, ToSocketAddrs};
use openssl::ssl::{SslMethod, SslContext};
use transport::smtp::extension::{Extension, ServerInfo};
use transport::error::{EmailResult, Error};
use transport::smtp::client::Client;
use transport::smtp::authentication::Mecanism;
use transport::EmailTransport;
use email::SendableEmail;
pub mod extension;
pub mod authentication;
pub mod response;
pub mod client;
// Registrated port numbers:
// https://www.iana.
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
/// Default smtp port
pub static SMTP_PORT: u16 = 25;
/// Default submission port
pub static SUBMISSION_PORT: u16 = 587;
// Useful strings and characters
/// The word separator for SMTP transactions
pub static SP: &'static str = " ";
/// The line ending for SMTP transactions (carriage return + line feed)
pub static CRLF: &'static str = "\r\n";
/// Colon
pub static COLON: &'static str = ":";
/// The ending of message content
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
/// NUL unicode character
pub static NUL: &'static str = "\0";
/// TLS security level
#[derive(Debug)]
pub enum SecurityLevel {
/// Only send an email on encrypted connection
AlwaysEncrypt,
/// Use TLS when available
Opportunistic,
/// Never use TLS
NeverEncrypt,
}
/// Contains client configuration
pub struct SmtpTransportBuilder {
/// Maximum connection reuse
///
/// Zero means no limitation
connection_reuse_count_limit: u16,
/// Enable connection reuse
connection_reuse: bool,
/// Name sent during HELO or EHLO
hello_name: String,
/// Credentials
credentials: Option<(String, String)>,
/// Socket we are connecting to
server_addr: SocketAddr,
/// SSL contexyt to use
ssl_context: SslContext,
/// TLS security level
security_level: SecurityLevel,
/// Enable UTF8 mailboxes in enveloppe or headers
smtp_utf8: bool,
/// List of authentication mecanism, sorted by priority
authentication_mecanisms: Vec<Mecanism>,
}
/// Builder for the SMTP SmtpTransport
impl SmtpTransportBuilder {
/// Creates a new local SMTP client
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = try!(addr.to_socket_addrs());
match addresses.next() {
Some(addr) => Ok(SmtpTransportBuilder {
server_addr: addr,
ssl_context: SslContext::new(SslMethod::Tlsv1).unwrap(),
security_level: SecurityLevel::Opportunistic,
smtp_utf8: false,
credentials: None,
connection_reuse_count_limit: 100,
connection_reuse: false,
hello_name: "localhost".to_string(),
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
}),
None => Err(From::from("Could nor resolve hostname")),
}
}
/// Creates a new local SMTP client to port 25
pub fn localhost() -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(("localhost", SMTP_PORT))
}
/// Use STARTTLS with a specific context
pub fn ssl_context(mut self, ssl_context: SslContext) -> SmtpTransportBuilder {
self.ssl_context = ssl_context;
self
}
/// Require SSL/TLS using STARTTLS
pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder {
self.security_level = level;
self
}
/// Require SSL/TLS using STARTTLS
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder {
self.smtp_utf8 = enabled;
self
}
/// Set the name used during HELO or EHLO
pub fn hello_name(mut self, name: &str) -> SmtpTransportBuilder {
self.hello_name = name.to_string();
self
}
/// Enable connection reuse
pub fn connection_reuse(mut self, enable: bool) -> SmtpTransportBuilder {
self.connection_reuse = enable;
self
}
/// Set the maximum number of emails sent using one connection
pub fn connection_reuse_count_limit(mut self, limit: u16) -> SmtpTransportBuilder {
self.connection_reuse_count_limit = limit;
self
}
/// Set the client credentials
pub fn credentials(mut self, username: &str, password: &str) -> SmtpTransportBuilder {
self.credentials = Some((username.to_string(), password.to_string()));
self
}
/// Set the authentication mecanisms
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SmtpTransportBuilder {
self.authentication_mecanisms = mecanisms;
self
}
/// Build the SMTP client
///
/// It does not connects to the server, but only creates the `SmtpTransport`
pub fn build(self) -> SmtpTransport {
SmtpTransport::new(self)
}
}
/// Represents the state of a client
#[derive(Debug)]
struct State {
/// Panic state
pub panic: bool,
/// Connection reuse counter
pub connection_reuse_count: u16,
}
/// Structure that implements the high level SMTP client
pub struct SmtpTransport {
/// Information about the server
/// Value is None before HELO/EHLO
server_info: Option<ServerInfo>,
/// SmtpTransport variable states
state: State,
/// Information about the client
client_info: SmtpTransportBuilder,
/// Low level client
client: Client,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.reset();
}
return Err(From::from(err))
},
}
})
);
impl SmtpTransport {
/// Creates a new SMTP client
///
/// It does not connects to the server, but only creates the `SmtpTransport`
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
let client = Client::new();
SmtpTransport {
client: client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
/// Reset the client state
fn reset(&mut self) {
// Close the SMTP transaction if needed
self.close();
// Reset the client state
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
}
/// Gets the EHLO response and updates server information
pub fn get_ehlo(&mut self) -> EmailResult {
// Extended Hello
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
// Print server information
debug!("server {}", self.server_info.as_ref().unwrap());
Ok(ehlo_response)
}
}
impl EmailTransport for SmtpTransport {
/// Sends an email
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
// Extract email information
let message_id = email.message_id();
let from_address = email.from_address();
let to_addresses = email.to_addresses();
let message = email.message();
// Check if the connection is still available
if self.state.connection_reuse_count > 0 {
if !self.client.is_connected() {
self.reset();
}
}
// If there is a usable connection, test if the server answers and hello has
// been sent
if self.state.connection_reuse_count == 0 {
try!(self.client.connect(&self.client_info.server_addr));
// Log the connection
info!("connection established to {}", self.client_info.server_addr);
try!(self.get_ehlo());
match (&self.client_info.security_level,
self.server_info.as_ref().unwrap().supports_feature(&Extension::StartTls)) {
(&SecurityLevel::AlwaysEncrypt, false) =>
return Err(From::from("Could not encrypt connection, aborting")),
(&SecurityLevel::Opportunistic, false) => (),
(&SecurityLevel::NeverEncrypt, _) => (),
(_, true) => {
try_smtp!(self.client.starttls(), self);
try_smtp!(self.client.upgrade_tls_stream(&self.client_info.ssl_context),
self);
debug!("connection encrypted");
// Send EHLO again
try!(self.get_ehlo());
}
}
if self.client_info.credentials.is_some() {
let (username, password) = self.client_info.credentials.clone().unwrap();
let mut found = false;
for mecanism in self.client_info.authentication_mecanisms.clone() {
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
found = true;
try_smtp!(self.client.auth(mecanism, &username, &password), self);
break;
}
}
if !found {
info!("No supported authentication mecanisms available");
}
}
}
// Mail
let mail_options = match (self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::EightBitMime),
self.server_info
.as_ref()
.unwrap()
.supports_feature(&Extension::SmtpUtfEight)) {
(true, true) => Some("BODY=8BITMIME SMTPUTF8"),
(true, false) => Some("BODY=8BITMIME"),
(false, _) => None,
};
try_smtp!(self.client.mail(&from_address, mail_options), self);
// Log the mail command
info!("{}: from=<{}>", message_id, from_address);
// Recipient
for to_address in to_addresses.iter() {
try_smtp!(self.client.rcpt(&to_address), self);
// Log the rcpt command
info!("{}: to=<{}>", message_id, to_address);
}
// Data
try_smtp!(self.client.data(), self);
// Message content
let result = self.client.message(&message);
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 ({})",
message_id,
self.state.connection_reuse_count,
message.len(),
result.as_ref()
.ok()
.unwrap()
.message()
.iter()
.next()
.unwrap_or(&"no response".to_string()));
}
// Test if we can reuse the existing connection
if (!self.client_info.connection_reuse) ||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
self.reset();
}
result
}
/// Closes the inner connection
fn close(&mut self) {
self.client.close();
}
}