use std::sync::Arc;
use lettre::message::Mailbox;
use lettre::transport::smtp::extension::ClientId;
use lettre::transport::stub::AsyncStubTransport;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use crate::mail::config::SmtpConfig;
use crate::mail::error::{EmailError, MailConfigError, MailSendError};
use crate::mail::message::Email;
#[derive(Clone)]
pub struct Mailer {
inner: Arc<MailerInner>,
}
enum MailerInner {
Smtp(AsyncSmtpTransport<Tokio1Executor>),
Capture(AsyncStubTransport),
}
impl Mailer {
pub fn smtp(config: SmtpConfig) -> Result<Self, MailConfigError> {
let tls_parameters = config
.build_tls_parameters()
.map_err(MailConfigError::tls_setup)?;
let tls = config.tls_enum(tls_parameters);
let port = config
.port_value()
.unwrap_or_else(|| config.tls_mode_value().default_port());
let mut builder = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(config.host())
.port(port)
.tls(tls)
.timeout(config.timeout_value())
.hello_name(config.get_hello_name().clone())
.pool_config(config.get_pool_config().clone());
if let Some(credentials) = config.get_credentials() {
builder = builder.credentials(credentials.to_lettre());
}
Ok(Self {
inner: Arc::new(MailerInner::Smtp(builder.build())),
})
}
#[must_use]
pub fn capture_ok() -> Self {
Self {
inner: Arc::new(MailerInner::Capture(AsyncStubTransport::new_ok())),
}
}
#[must_use]
pub fn capture_error() -> Self {
Self {
inner: Arc::new(MailerInner::Capture(AsyncStubTransport::new_error())),
}
}
pub async fn send(&self, message: &Message) -> Result<(), MailSendError> {
match &*self.inner {
MailerInner::Smtp(transport) => transport
.send_raw(message.envelope(), &message.formatted())
.await
.map(|_| ())
.map_err(MailSendError::smtp),
MailerInner::Capture(transport) => transport
.send_raw(message.envelope(), &message.formatted())
.await
.map(|_| ())
.map_err(MailSendError::capture),
}
}
#[must_use]
pub fn is_capture(&self) -> bool {
matches!(&*self.inner, MailerInner::Capture(_))
}
#[must_use]
pub fn is_smtp(&self) -> bool {
matches!(&*self.inner, MailerInner::Smtp(_))
}
pub async fn captured(&self) -> Option<Vec<(lettre::address::Envelope, String)>> {
match &*self.inner {
MailerInner::Capture(transport) => Some(transport.messages().await),
MailerInner::Smtp(_) => None,
}
}
pub async fn test_connection(&self) -> Result<bool, MailSendError> {
match &*self.inner {
MailerInner::Smtp(transport) => transport
.test_connection()
.await
.map_err(MailSendError::smtp),
MailerInner::Capture(_) => Ok(false),
}
}
pub async fn shutdown(&self) {
if let MailerInner::Smtp(transport) = &*self.inner {
let _ = transport.shutdown().await;
}
}
}
pub trait Mailable: Send + Sync {
fn build(&self, email: Email) -> Result<Message, EmailError>;
}
#[derive(Clone)]
pub struct Mail {
mailer: Mailer,
from: Mailbox,
}
impl Mail {
#[must_use]
pub fn new(mailer: Mailer, from: Mailbox) -> Self {
Self { mailer, from }
}
pub fn from_str(mailer: Mailer, from: &str) -> Result<Self, lettre::address::AddressError> {
Ok(Self::new(mailer, from.parse()?))
}
#[must_use]
pub fn to(&self, address: impl Into<String>) -> MailBuilder<'_> {
MailBuilder {
mail: self,
to: address.into(),
}
}
#[must_use]
pub fn mailer(&self) -> &Mailer {
&self.mailer
}
}
pub struct MailBuilder<'a> {
mail: &'a Mail,
to: String,
}
impl<'a> MailBuilder<'a> {
pub async fn send<M: Mailable>(self, mailable: &M) -> Result<(), MailSendError> {
let to_mailbox: Mailbox = self
.to
.parse()
.map_err(|e: lettre::address::AddressError| {
MailSendError::build(EmailError::address(e))
})?;
let email = Email::builder().from(self.mail.from.clone()).to(to_mailbox);
let message = mailable.build(email).map_err(MailSendError::build)?;
self.mail.mailer.send(&message).await
}
}
pub fn parse_mailbox(mailbox: &str) -> Result<Mailbox, lettre::address::AddressError> {
mailbox.parse()
}
#[must_use]
pub fn hello_name(domain: impl Into<String>) -> ClientId {
ClientId::Domain(domain.into())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mail::EmailError;
#[test]
fn capture_ok_is_capture() {
let mailer = Mailer::capture_ok();
assert!(mailer.is_capture());
assert!(!mailer.is_smtp());
}
#[test]
fn capture_records_messages() {
let mailer = Mailer::capture_ok();
let msg = Email::builder()
.from("noreply@example.com".parse().unwrap())
.to("to@example.com".parse().unwrap())
.subject("test")
.plain("hello")
.expect("build");
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(mailer.send(&msg)).unwrap();
let captured = rt.block_on(mailer.captured()).expect("capture");
assert_eq!(captured.len(), 1);
}
struct WelcomeEmail {
name: String,
}
impl Mailable for WelcomeEmail {
fn build(&self, email: Email) -> Result<Message, EmailError> {
email
.subject(format!("Welcome, {}!", self.name))
.plain(format!("Welcome, {}!", self.name))
}
}
#[test]
fn mail_facade_sends_via_capture() {
let mailer = Mailer::capture_ok();
let from: Mailbox = "noreply@example.com".parse().unwrap();
let mail = Mail::new(mailer, from);
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(mail.to("user@example.com").send(&WelcomeEmail {
name: "Alice".into(),
}))
.expect("send");
let captured = rt.block_on(mail.mailer().captured()).expect("capture");
assert_eq!(captured.len(), 1);
let body = &captured[0].1;
assert!(body.contains("Welcome, Alice!"));
assert!(body.contains("Subject: Welcome, Alice!"));
}
}