use std::sync::Arc;
use std::time::{Duration, Instant};
use mail_send::{Credentials, SmtpClient, SmtpClientBuilder};
use tokio::net::TcpStream;
use tokio::sync::Mutex;
use tokio_rustls::client::TlsStream;
use super::lru_cache::LruCache;
use crate::connector::config::{SmtpAuth, SmtpConnectorConfig, SmtpTls};
use crate::errors::OrionError;
const MAX_IDLE_PER_CONNECTOR: usize = 4;
const MAX_IDLE_AGE: Duration = Duration::from_secs(60);
pub enum SmtpStream {
Tls(Box<TlsStream<TcpStream>>),
Plain(TcpStream),
}
macro_rules! project {
($self:ident) => {
match $self.get_mut() {
SmtpStream::Tls(s) => std::pin::Pin::new(&mut **s) as std::pin::Pin<&mut dyn Stream>,
SmtpStream::Plain(s) => std::pin::Pin::new(s) as std::pin::Pin<&mut dyn Stream>,
}
};
}
trait Stream: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send {}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send> Stream for T {}
impl tokio::io::AsyncRead for SmtpStream {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
project!(self).poll_read(cx, buf)
}
}
impl tokio::io::AsyncWrite for SmtpStream {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
project!(self).poll_write(cx, buf)
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
project!(self).poll_flush(cx)
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
project!(self).poll_shutdown(cx)
}
}
pub type PooledClient = SmtpClient<SmtpStream>;
struct Idle {
client: PooledClient,
parked_at: Instant,
}
pub struct SmtpPool {
builder: SmtpClientBuilder<String>,
tls: SmtpTls,
idle: Mutex<Vec<Idle>>,
}
impl SmtpPool {
pub async fn checkout(&self) -> Result<PooledClient, mail_send::Error> {
loop {
let Some(idle) = self.idle.lock().await.pop() else {
break;
};
if idle.parked_at.elapsed() >= MAX_IDLE_AGE {
continue; }
let mut client = idle.client;
if client.rset().await.is_ok() {
return Ok(client);
}
}
self.connect().await
}
pub async fn checkin(&self, client: PooledClient) {
let mut idle = self.idle.lock().await;
if idle.len() < MAX_IDLE_PER_CONNECTOR {
idle.push(Idle {
client,
parked_at: Instant::now(),
});
}
}
async fn connect(&self) -> Result<PooledClient, mail_send::Error> {
Ok(match self.tls {
SmtpTls::None => {
let client = self.builder.connect_plain().await?;
SmtpClient {
stream: SmtpStream::Plain(client.stream),
timeout: client.timeout,
}
}
SmtpTls::Starttls | SmtpTls::Implicit => {
let client = self.builder.connect().await?;
SmtpClient {
stream: SmtpStream::Tls(Box::new(client.stream)),
timeout: client.timeout,
}
}
})
}
}
pub struct SmtpPoolCache {
cache: LruCache<Arc<SmtpPool>>,
}
impl SmtpPoolCache {
pub fn new(max_entries: usize) -> Self {
Self {
cache: LruCache::new(max_entries, "smtp_pool"),
}
}
pub async fn get_pool(
&self,
connector_name: &str,
config: &SmtpConnectorConfig,
) -> Result<Arc<SmtpPool>, OrionError> {
let config = config.clone();
let name = connector_name.to_string();
self.cache
.get_or_create(connector_name, || async move {
if !config.allow_private_urls
&& let Err(msg) =
crate::validation::validate_hostport_not_private(&config.host, config.port)
.await
{
return Err(OrionError::validation(format!(
"SMTP connector '{name}': {msg} (set allow_private_urls for an \
internal relay)"
)));
}
build_pool(&name, &config)
})
.await
}
pub async fn evict(&self, connector_name: &str) {
self.cache.evict(connector_name).await;
}
pub async fn evict_all(&self) {
self.cache.evict_all().await;
}
}
impl Default for SmtpPoolCache {
fn default() -> Self {
Self::new(64)
}
}
fn build_pool(
connector_name: &str,
config: &SmtpConnectorConfig,
) -> Result<Arc<SmtpPool>, OrionError> {
crate::server::tls::ensure_crypto_provider();
let mut builder = SmtpClientBuilder::new(config.host.clone(), config.port)
.map_err(|e| OrionError::validation(format!("SMTP connector '{connector_name}': {e}")))?
.implicit_tls(matches!(config.tls, SmtpTls::Implicit))
.timeout(Duration::from_millis(config.timeout_ms));
if let SmtpAuth::Basic { username, password } = &config.auth {
builder = builder.credentials(Credentials::new(username.clone(), password.clone()));
}
Ok(Arc::new(SmtpPool {
builder,
tls: config.tls,
idle: Mutex::new(Vec::new()),
}))
}