use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use secrecy::{ExposeSecret, Secret};
#[cfg(feature = "tracing")]
use tracing::{error, info, instrument};
use async_mailer_core::mail_send::{self, smtp::message::Message, SmtpClientBuilder};
use async_mailer_core::{util, ArcMailer, BoxMailer, DynMailer, DynMailerError, Mailer};
#[derive(Debug, thiserror::Error)]
pub enum SmtpMailerError {
#[error("could not connect to SMTP host")]
Connect(mail_send::Error),
#[error("could not send SMTP mail")]
Send(mail_send::Error),
}
#[derive(Clone, Debug)]
pub enum SmtpInvalidCertsPolicy {
Allow,
Deny,
}
#[derive(Clone)]
pub struct SmtpMailer {
inner: SmtpClientBuilder<String>,
}
impl std::fmt::Debug for SmtpMailer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client (SMTP)").finish()
}
}
impl SmtpMailer {
#[cfg_attr(feature = "tracing", instrument)]
pub fn new(
host: String,
port: u16,
invalid_certs: SmtpInvalidCertsPolicy,
user: String,
password: Secret<String>,
) -> Self {
let mut smtp_client = SmtpClientBuilder::new(host, port)
.credentials((user, password.expose_secret().into()))
.timeout(Duration::from_secs(30));
if matches!(invalid_certs, SmtpInvalidCertsPolicy::Allow) {
smtp_client = smtp_client.allow_invalid_certs();
}
Self { inner: smtp_client }
}
#[cfg_attr(feature = "tracing", instrument)]
pub fn new_box(
host: String,
port: u16,
invalid_certs: SmtpInvalidCertsPolicy,
user: String,
password: Secret<String>,
) -> BoxMailer {
Box::new(Self::new(host, port, invalid_certs, user, password))
}
#[cfg_attr(feature = "tracing", instrument)]
pub fn new_arc(
host: String,
port: u16,
invalid_certs: SmtpInvalidCertsPolicy,
user: String,
password: Secret<String>,
) -> ArcMailer {
Arc::new(Self::new(host, port, invalid_certs, user, password))
}
}
#[async_trait]
impl Mailer for SmtpMailer {
type Error = SmtpMailerError;
async fn send_mail(&self, message: Message<'_>) -> Result<(), Self::Error> {
#[cfg(feature = "tracing")]
let recipient_addresses = util::format_recipient_addresses(&message);
info!("Sending SMTP mail to {recipient_addresses}...");
let connection = self.inner.connect().await;
#[cfg(feature = "tracing")]
match &connection {
Ok(_) => {}
Err(error) => error!(
?error,
"Failed to connect to SMTP host for mail to {recipient_addresses}"
),
}
let response = connection
.map_err(SmtpMailerError::Connect)?
.send(message)
.await;
#[cfg(feature = "tracing")]
match &response {
Ok(_) => {
info!("Sent SMTP mail to {recipient_addresses}");
}
Err(error) => {
error!(?error, "Failed to send SMTP mail to {recipient_addresses}");
}
}
Ok(response.map_err(SmtpMailerError::Send)?)
}
}
#[async_trait]
impl DynMailer for SmtpMailer {
#[cfg_attr(feature = "tracing", instrument(skip(message)))]
async fn send_mail(&self, message: Message<'_>) -> Result<(), DynMailerError> {
Mailer::send_mail(self, message).await.map_err(Into::into)
}
}