use smtp_proto::{EhloResponse, EXT_START_TLS};
use std::hash::Hash;
use std::time::Duration;
use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
};
use tokio_rustls::client::TlsStream;
use crate::{Credentials, SmtpClient, SmtpClientBuilder};
use super::{tls::build_tls_connector, AssertReply};
impl<T: AsRef<str> + PartialEq + Eq + Hash> SmtpClientBuilder<T> {
pub fn new(hostname: T, port: u16) -> Self {
SmtpClientBuilder {
addr: format!("{}:{}", hostname.as_ref(), port),
timeout: Duration::from_secs(60 * 60),
tls_connector: build_tls_connector(false),
tls_hostname: hostname,
tls_implicit: true,
is_lmtp: false,
local_host: gethostname::gethostname()
.to_str()
.unwrap_or("[127.0.0.1]")
.to_string(),
credentials: None,
say_ehlo: true,
}
}
pub fn allow_invalid_certs(mut self) -> Self {
self.tls_connector = build_tls_connector(true);
self
}
pub fn implicit_tls(mut self, tls_implicit: bool) -> Self {
self.tls_implicit = tls_implicit;
self
}
pub fn lmtp(mut self, is_lmtp: bool) -> Self {
self.is_lmtp = is_lmtp;
self
}
pub fn say_ehlo(mut self, say_ehlo: bool) -> Self {
self.say_ehlo = say_ehlo;
self
}
pub fn helo_host(mut self, host: impl Into<String>) -> Self {
self.local_host = host.into();
self
}
pub fn credentials(mut self, credentials: impl Into<Credentials<T>>) -> Self {
self.credentials = Some(credentials.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub async fn connect(&self) -> crate::Result<SmtpClient<TlsStream<TcpStream>>> {
tokio::time::timeout(self.timeout, async {
let mut client = SmtpClient {
stream: TcpStream::connect(&self.addr).await?,
timeout: self.timeout,
};
let mut client = if self.tls_implicit {
let mut client = client
.into_tls(&self.tls_connector, self.tls_hostname.as_ref())
.await?;
client.read().await?.assert_positive_completion()?;
client
} else {
client.read().await?.assert_positive_completion()?;
let response = if !self.is_lmtp {
client.ehlo(&self.local_host).await?
} else {
client.lhlo(&self.local_host).await?
};
if response.has_capability(EXT_START_TLS) {
client
.start_tls(&self.tls_connector, self.tls_hostname.as_ref())
.await?
} else {
return Err(crate::Error::MissingStartTls);
}
};
if self.say_ehlo {
let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;
if let Some(credentials) = &self.credentials {
client.authenticate(&credentials, &capabilities).await?;
}
}
Ok(client)
})
.await
.map_err(|_| crate::Error::Timeout)?
}
pub async fn connect_plain(&self) -> crate::Result<SmtpClient<TcpStream>> {
let mut client = SmtpClient {
stream: tokio::time::timeout(self.timeout, async {
TcpStream::connect(&self.addr).await
})
.await
.map_err(|_| crate::Error::Timeout)??,
timeout: self.timeout,
};
client.read().await?.assert_positive_completion()?;
if self.say_ehlo {
let capabilities = client.capabilities(&self.local_host, self.is_lmtp).await?;
if let Some(credentials) = &self.credentials {
client.authenticate(&credentials, &capabilities).await?;
}
}
Ok(client)
}
}
impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> {
pub async fn capabilities(
&mut self,
local_host: &str,
is_lmtp: bool,
) -> crate::Result<EhloResponse<String>> {
if !is_lmtp {
self.ehlo(local_host).await
} else {
self.lhlo(local_host).await
}
}
}