use native_tls::TlsConnector;
use smtp::authentication::{
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
};
use smtp::client::net::ClientTlsParameters;
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
use smtp::client::InnerClient;
use smtp::commands::*;
use smtp::error::{Error, SmtpResult};
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
use std::net::{SocketAddr, ToSocketAddrs};
use std::time::Duration;
use {SendableEmail, Transport};
pub mod authentication;
pub mod client;
pub mod commands;
pub mod error;
pub mod extension;
#[cfg(feature = "connection-pool")]
pub mod r2d2;
pub mod response;
pub mod util;
pub const SMTP_PORT: u16 = 25;
pub const SUBMISSION_PORT: u16 = 587;
pub const SUBMISSIONS_PORT: u16 = 465;
#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub enum ClientSecurity {
None,
Opportunistic(ClientTlsParameters),
Required(ClientTlsParameters),
Wrapper(ClientTlsParameters),
}
#[derive(Clone, Debug, Copy)]
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
pub enum ConnectionReuseParameters {
ReuseUnlimited,
ReuseLimited(u16),
NoReuse,
}
#[allow(missing_debug_implementations)]
#[derive(Clone)]
pub struct SmtpClient {
connection_reuse: ConnectionReuseParameters,
hello_name: ClientId,
credentials: Option<Credentials>,
server_addr: SocketAddr,
security: ClientSecurity,
smtp_utf8: bool,
authentication_mechanism: Option<Mechanism>,
timeout: Option<Duration>,
}
impl SmtpClient {
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
let mut addresses = addr.to_socket_addrs()?;
match addresses.next() {
Some(addr) => Ok(SmtpClient {
server_addr: addr,
security,
smtp_utf8: false,
credentials: None,
connection_reuse: ConnectionReuseParameters::NoReuse,
hello_name: ClientId::hostname(),
authentication_mechanism: None,
timeout: Some(Duration::new(60, 0)),
}),
None => Err(Error::Resolution),
}
}
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpClient::new(
(domain, SUBMISSIONS_PORT),
ClientSecurity::Wrapper(tls_parameters),
)
}
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
}
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
self.smtp_utf8 = enabled;
self
}
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
self.hello_name = name;
self
}
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
self.connection_reuse = parameters;
self
}
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
self.credentials = Some(credentials.into());
self
}
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
self.authentication_mechanism = Some(mechanism);
self
}
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
self.timeout = timeout;
self
}
pub fn transport(self) -> SmtpTransport {
SmtpTransport::new(self)
}
}
#[derive(Debug)]
struct State {
pub panic: bool,
pub connection_reuse_count: u16,
}
#[allow(missing_debug_implementations)]
pub struct SmtpTransport {
server_info: Option<ServerInfo>,
state: State,
client_info: SmtpClient,
client: InnerClient,
}
macro_rules! try_smtp (
($err: expr, $client: ident) => ({
match $err {
Ok(val) => val,
Err(err) => {
if !$client.state.panic {
$client.state.panic = true;
$client.close();
}
return Err(From::from(err))
},
}
})
);
impl<'a> SmtpTransport {
pub fn new(builder: SmtpClient) -> SmtpTransport {
let client = InnerClient::new();
SmtpTransport {
client,
server_info: None,
client_info: builder,
state: State {
panic: false,
connection_reuse_count: 0,
},
}
}
fn connect(&mut self) -> Result<(), Error> {
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
self.close();
}
if self.state.connection_reuse_count > 0 {
info!(
"connection already established to {}",
self.client_info.server_addr
);
return Ok(());
}
self.client.connect(
&self.client_info.server_addr,
match self.client_info.security {
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
_ => None,
},
)?;
self.client.set_timeout(self.client_info.timeout)?;
info!("connection established to {}", self.client_info.server_addr);
self.ehlo()?;
match (
&self.client_info.security.clone(),
self.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::StartTls),
) {
(&ClientSecurity::Required(_), false) => {
return Err(From::from("Could not encrypt connection, aborting"));
}
(&ClientSecurity::Opportunistic(_), false) => (),
(&ClientSecurity::None, _) => (),
(&ClientSecurity::Wrapper(_), _) => (),
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
| (&ClientSecurity::Required(ref tls_parameters), true) => {
try_smtp!(self.client.command(StarttlsCommand), self);
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
debug!("connection encrypted");
self.ehlo()?;
}
}
if self.client_info.credentials.is_some() {
let mut found = false;
let accepted_mechanisms = match self.client_info.authentication_mechanism {
Some(mechanism) => vec![mechanism],
None => {
if self.client.is_encrypted() {
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
} else {
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
}
}
};
for mechanism in accepted_mechanisms {
if self
.server_info
.as_ref()
.unwrap()
.supports_auth_mechanism(mechanism)
{
found = true;
try_smtp!(
self.client
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
self
);
break;
}
}
if !found {
info!("No supported authentication mechanisms available");
}
}
Ok(())
}
fn ehlo(&mut self) -> SmtpResult {
let ehlo_response = try_smtp!(
self.client.command(EhloCommand::new(ClientId::new(
self.client_info.hello_name.to_string()
),)),
self
);
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
debug!("server {}", self.server_info.as_ref().unwrap());
Ok(ehlo_response)
}
pub fn close(&mut self) {
self.client.close();
self.server_info = None;
self.state.panic = false;
self.state.connection_reuse_count = 0;
}
}
impl<'a> Transport<'a> for SmtpTransport {
type Result = SmtpResult;
#[cfg_attr(
feature = "cargo-clippy",
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
)]
fn send(&mut self, email: SendableEmail) -> SmtpResult {
let message_id = email.message_id().to_string();
if !self.client.is_connected() {
self.connect()?;
}
let mut mail_options = vec![];
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::EightBitMime)
{
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
}
if self
.server_info
.as_ref()
.unwrap()
.supports_feature(Extension::SmtpUtfEight)
&& self.client_info.smtp_utf8
{
mail_options.push(MailParameter::SmtpUtfEight);
}
try_smtp!(
self.client.command(MailCommand::new(
email.envelope().from().cloned(),
mail_options,
)),
self
);
info!(
"{}: from=<{}>",
message_id,
match email.envelope().from() {
Some(address) => address.to_string(),
None => "".to_string(),
}
);
for to_address in email.envelope().to() {
try_smtp!(
self.client
.command(RcptCommand::new(to_address.clone(), vec![])),
self
);
info!("{}: to=<{}>", message_id, to_address);
}
try_smtp!(self.client.command(DataCommand), self);
let result = self.client.message(Box::new(email.message()));
if result.is_ok() {
self.state.connection_reuse_count += 1;
info!(
"{}: conn_use={}, status=sent ({})",
message_id,
self.state.connection_reuse_count,
result
.as_ref()
.ok()
.unwrap()
.message
.iter()
.next()
.unwrap_or(&"no response".to_string())
);
}
match self.client_info.connection_reuse {
ConnectionReuseParameters::ReuseLimited(limit)
if self.state.connection_reuse_count >= limit =>
{
self.close()
}
ConnectionReuseParameters::NoReuse => self.close(),
_ => (),
}
result
}
}