use std::fmt::Debug;
use std::io as std_io;
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
use futures::future::{self, Either, Future};
use crate::{
common::{ClientId, DefaultTlsSetup, SetupTls, TlsConfig},
connection::{Cmd, Connection},
data_types::Domain,
error::{ConnectingFailed, LogicError},
future_ext::ResultWithContextExt,
io::{Io, SmtpResult},
command::Noop,
};
pub type ConnectingFuture =
Box<dyn Future<Item = Connection, Error = ConnectingFailed> + Send + 'static>;
pub const DEFAULT_SMTP_MSA_PORT: u16 = 587;
pub const DEFAULT_SMTP_MX_PORT: u16 = 25;
fn cmd_future2connecting_future<LE: 'static, E>(
res: Result<(Connection, SmtpResult), E>,
new_logic_err: LE,
) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send
where
LE: Send + FnOnce(LogicError) -> ConnectingFailed,
E: Into<ConnectingFailed>,
{
let fut = match res {
Err(err) => Either::A(future::err(err.into())),
Ok((con, Ok(_resp))) => Either::A(future::ok(con.into())),
Ok((con, Err(err))) => Either::B(con.quit().then(|_| Err(new_logic_err(err)))),
};
fut
}
impl Connection {
pub fn connect<S, A>(
config: ConnectionConfig<A, S>,
) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send
where
S: SetupTls,
A: Cmd + Send,
{
let ConnectionConfig {
addr,
security,
client_id,
auth_cmd,
} = config;
#[allow(deprecated)]
let con_fut = match security {
Security::None => Either::B(Either::A(Connection::_connect_insecure(&addr, client_id))),
Security::DirectTls(tls_config) => Either::B(Either::B(
Connection::_connect_direct_tls(&addr, client_id, tls_config),
)),
Security::StartTls(tls_config) => {
Either::A(Connection::_connect_starttls(&addr, client_id, tls_config))
}
};
let fut = con_fut.and_then(|con| {
con.send(auth_cmd)
.then(|res| cmd_future2connecting_future(res, ConnectingFailed::Auth))
});
fut
}
#[doc(hidden)]
pub fn _connect_insecure_no_ehlo(
addr: &SocketAddr,
) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send {
let fut = Io::connect_insecure(addr)
.and_then(Io::parse_response)
.then(|res| {
let res = res.map(|(io, res)| (Connection::from(io), res));
cmd_future2connecting_future(res, ConnectingFailed::Setup)
});
fut
}
#[doc(hidden)]
pub fn _connect_direct_tls_no_ehlo<S>(
addr: &SocketAddr,
config: TlsConfig<S>,
) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send
where
S: SetupTls,
{
let fut = Io::connect_secure(addr, config)
.and_then(Io::parse_response)
.then(|res| {
let res = res.map(|(io, res)| (Connection::from(io), res));
cmd_future2connecting_future(res, ConnectingFailed::Setup)
});
fut
}
#[doc(hidden)]
pub fn _connect_insecure(
addr: &SocketAddr,
clid: ClientId,
) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send {
use crate::command::Ehlo;
let fut = Connection::_connect_insecure_no_ehlo(addr).and_then(|con| {
con.send(Ehlo::from(clid))
.then(|res| cmd_future2connecting_future(res, ConnectingFailed::Setup))
});
fut
}
#[doc(hidden)]
pub fn _connect_direct_tls<S>(
addr: &SocketAddr,
clid: ClientId,
config: TlsConfig<S>,
) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send
where
S: SetupTls,
{
use crate::command::Ehlo;
let fut = Connection::_connect_direct_tls_no_ehlo(addr, config).and_then(|con| {
con.send(Ehlo::from(clid))
.then(|res| cmd_future2connecting_future(res, ConnectingFailed::Setup))
});
fut
}
#[doc(hidden)]
pub fn _connect_starttls<S>(
addr: &SocketAddr,
clid: ClientId,
config: TlsConfig<S>,
) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send
where
S: SetupTls,
{
use crate::command::{Ehlo, StartTls};
let TlsConfig { domain, setup } = config;
let fut = Connection::_connect_insecure(&addr, clid.clone())
.and_then(|con| {
con.send(StartTls {
setup_tls: setup,
sni_domain: domain,
})
.map_err(ConnectingFailed::Io)
})
.ctx_and_then(|con, _| con.send(Ehlo::from(clid)).map_err(ConnectingFailed::Io))
.then(|res| cmd_future2connecting_future(res, ConnectingFailed::Setup));
fut
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Security<S>
where
S: SetupTls,
{
#[deprecated(
since = "0.0",
note = "it's strongly discourage to use unencrypted connections for private information/auth etc."
)]
None,
DirectTls(TlsConfig<S>),
StartTls(TlsConfig<S>),
}
#[derive(Debug, Clone)]
pub struct ConnectionConfig<A, S = DefaultTlsSetup>
where
S: SetupTls,
A: Cmd,
{
pub addr: SocketAddr,
pub auth_cmd: A,
pub security: Security<S>,
pub client_id: ClientId,
}
impl<A> ConnectionConfig<A, DefaultTlsSetup>
where
A: Cmd,
{
pub fn connect(self) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send {
Connection::connect(self)
}
}
impl ConnectionConfig<Noop, DefaultTlsSetup> {
pub fn builder_local_unencrypted() -> LocalNonSecureBuilder<Noop> {
LocalNonSecureBuilder {
client_id: None,
port: DEFAULT_SMTP_MSA_PORT,
auth_cmd: Noop,
}
}
pub fn builder(
host: Domain,
) -> Result<ConnectionBuilder<Noop, DefaultTlsSetup>, std_io::Error> {
ConnectionBuilder::new(host)
}
pub fn builder_with_port(
host: Domain,
port: u16,
) -> Result<ConnectionBuilder<Noop, DefaultTlsSetup>, std_io::Error> {
ConnectionBuilder::new_with_port(host, port)
}
pub fn builder_with_addr(
addr: SocketAddr,
domain: Domain,
) -> ConnectionBuilder<Noop, DefaultTlsSetup> {
ConnectionBuilder::new_with_addr(addr, domain)
}
}
#[derive(Debug)]
pub struct LocalNonSecureBuilder<A>
where
A: Cmd,
{
client_id: Option<ClientId>,
port: u16,
auth_cmd: A,
}
impl<A> LocalNonSecureBuilder<A>
where
A: Cmd,
{
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn client_id(mut self, client_id: ClientId) -> Self {
self.client_id = Some(client_id);
self
}
pub fn auth<NA>(self, auth_cmd: NA) -> LocalNonSecureBuilder<NA>
where
NA: Cmd,
{
let LocalNonSecureBuilder {
client_id,
port,
auth_cmd: _,
} = self;
LocalNonSecureBuilder {
client_id,
port,
auth_cmd,
}
}
pub fn build(self) -> ConnectionConfig<A, DefaultTlsSetup> {
let LocalNonSecureBuilder {
client_id,
port,
auth_cmd,
} = self;
let client_id = client_id.unwrap_or_else(|| ClientId::hostname());
let addr = SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), port);
#[allow(deprecated)]
let security = Security::None;
ConnectionConfig {
addr,
client_id,
auth_cmd,
security,
}
}
pub fn connect(self) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send {
Connection::connect(self.build())
}
}
#[derive(Debug)]
pub struct ConnectionBuilder<A, S = DefaultTlsSetup>
where
S: SetupTls,
A: Cmd,
{
client_id: Option<ClientId>,
addr: SocketAddr,
domain: Domain,
setup_tls: S,
use_security: UseSecurity,
auth_cmd: A,
}
impl ConnectionBuilder<Noop, DefaultTlsSetup> {
pub fn new(host: Domain) -> Result<Self, std_io::Error> {
Self::new_with_port(host, DEFAULT_SMTP_MSA_PORT)
}
pub fn new_with_port(host: Domain, port: u16) -> Result<Self, std_io::Error> {
let addr = get_addr((host.as_str(), port))?;
Ok(Self::new_with_addr(addr, host))
}
pub fn new_with_addr(addr: SocketAddr, domain: Domain) -> Self {
ConnectionBuilder {
addr,
domain,
use_security: UseSecurity::StartTls,
client_id: None,
setup_tls: DefaultTlsSetup,
auth_cmd: Noop,
}
}
}
impl<A, S> ConnectionBuilder<A, S>
where
S: SetupTls,
A: Cmd,
{
pub fn use_tls_setup<S2: SetupTls>(self, setup: S2) -> ConnectionBuilder<A, S2> {
let ConnectionBuilder {
addr,
domain,
use_security,
client_id,
setup_tls: _,
auth_cmd,
} = self;
ConnectionBuilder {
addr,
domain,
use_security,
client_id,
setup_tls: setup,
auth_cmd,
}
}
pub fn use_start_tls(mut self) -> Self {
self.use_security = UseSecurity::StartTls;
self
}
pub fn use_direct_tls(mut self) -> Self {
self.use_security = UseSecurity::DirectTls;
self
}
pub fn auth<NA: Cmd>(self, auth_cmd: NA) -> ConnectionBuilder<NA, S> {
let ConnectionBuilder {
addr,
domain,
use_security,
client_id,
setup_tls,
auth_cmd: _,
} = self;
ConnectionBuilder {
addr,
domain,
use_security,
client_id,
setup_tls,
auth_cmd: auth_cmd,
}
}
pub fn client_id(mut self, id: ClientId) -> Self {
self.client_id = Some(id);
self
}
pub fn build(self) -> ConnectionConfig<A, S> {
let ConnectionBuilder {
addr,
domain,
use_security,
client_id,
setup_tls: setup,
auth_cmd,
} = self;
let tls_config = TlsConfig { domain, setup };
let security = match use_security {
UseSecurity::StartTls => Security::StartTls(tls_config),
UseSecurity::DirectTls => Security::DirectTls(tls_config),
};
let client_id = client_id.unwrap_or_else(|| ClientId::hostname());
ConnectionConfig {
addr,
security,
auth_cmd,
client_id,
}
}
pub fn connect(self) -> impl Future<Item = Connection, Error = ConnectingFailed> + Send {
Connection::connect(self.build())
}
}
#[derive(Debug)]
enum UseSecurity {
StartTls,
DirectTls,
}
fn get_addr(tsas: impl ToSocketAddrs + Copy + Debug) -> Result<SocketAddr, std_io::Error> {
if let Some(addr) = tsas.to_socket_addrs()?.next() {
Ok(addr)
} else {
Err(std_io::Error::new(
std_io::ErrorKind::AddrNotAvailable,
format!("{:?} is not associated with any socket address", tsas),
))
}
}
#[cfg(test)]
mod testd {
use super::*;
use hostname::get_hostname;
const EXAMPLE_DOMAIN: &str = "1aim.com";
#[test]
fn builder_uses_right_defaults() {
let host = Domain::new_unchecked(EXAMPLE_DOMAIN.to_owned());
let cb = ConnectionBuilder::new(host.clone()).unwrap();
let ConnectionConfig {
addr,
security,
auth_cmd,
client_id,
} = cb.build();
assert!((EXAMPLE_DOMAIN, DEFAULT_SMTP_MSA_PORT)
.to_socket_addrs()
.unwrap()
.any(|other_addr| other_addr == addr));
assert_eq!(
security,
Security::StartTls(TlsConfig {
domain: host,
setup: DefaultTlsSetup
})
);
let _type_check: Noop = auth_cmd;
if let ClientId::Domain(domain) = client_id {
let expected_client_id = get_hostname().unwrap_or_else(|| "localhost".to_owned());
assert_eq!(domain.as_str(), &expected_client_id)
} else {
panic!("unexpected client id: {:?}", client_id);
}
}
}