use async_trait::async_trait;
use lettre::message::{Mailbox, MultiPart, SinglePart, header::ContentType};
use lettre::transport::smtp::AsyncSmtpTransport;
use lettre::transport::smtp::authentication::Credentials;
use lettre::{AsyncTransport, Message, Tokio1Executor};
use crate::email::mailer::{EmailMessage, Mailer, MailerError};
#[derive(Clone)]
pub struct LettreMailer {
transport: AsyncSmtpTransport<Tokio1Executor>,
}
impl LettreMailer {
pub fn starttls(
host: &str,
username: impl Into<String>,
password: impl Into<String>,
) -> Result<Self, MailerError> {
let creds = Credentials::new(username.into(), password.into());
let transport = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(host)
.map_err(|e| MailerError::Build(format!("starttls_relay: {e}")))?
.credentials(creds)
.build();
Ok(Self { transport })
}
pub fn implicit_tls(
host: &str,
username: impl Into<String>,
password: impl Into<String>,
) -> Result<Self, MailerError> {
let creds = Credentials::new(username.into(), password.into());
let transport = AsyncSmtpTransport::<Tokio1Executor>::relay(host)
.map_err(|e| MailerError::Build(format!("relay: {e}")))?
.credentials(creds)
.build();
Ok(Self { transport })
}
pub fn from_transport(transport: AsyncSmtpTransport<Tokio1Executor>) -> Self {
Self { transport }
}
pub fn transport(&self) -> &AsyncSmtpTransport<Tokio1Executor> {
&self.transport
}
}
fn parse_mailbox(role: &str, raw: &str) -> Result<Mailbox, MailerError> {
raw.parse::<Mailbox>()
.map_err(|e| MailerError::Build(format!("{role}: {e} ({raw:?})")))
}
#[async_trait]
impl Mailer for LettreMailer {
async fn send(&self, msg: &EmailMessage) -> Result<(), MailerError> {
let from = parse_mailbox("from", &msg.from)?;
let to = parse_mailbox("to", &msg.to)?;
let mut builder = Message::builder().from(from).to(to).subject(&msg.subject);
if let Some(reply) = &msg.reply_to {
builder = builder.reply_to(parse_mailbox("reply_to", reply)?);
}
let email = match &msg.html {
None => builder
.header(ContentType::TEXT_PLAIN)
.body(msg.text.clone()),
Some(html) => {
let multipart = MultiPart::alternative()
.singlepart(
SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.body(msg.text.clone()),
)
.singlepart(
SinglePart::builder()
.header(ContentType::TEXT_HTML)
.body(html.clone()),
);
builder.multipart(multipart)
}
}
.map_err(|e| MailerError::Build(format!("message: {e}")))?;
self.transport
.send(email)
.await
.map_err(|e| MailerError::Transport(format!("smtp: {e}")))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starttls_constructor_does_not_dial() {
let m = LettreMailer::starttls("smtp.example.com", "u", "p").unwrap();
let _ = m.transport();
}
#[test]
fn implicit_tls_constructor_does_not_dial() {
let m = LettreMailer::implicit_tls("smtp.example.com", "u", "p").unwrap();
let _ = m.transport();
}
#[tokio::test]
async fn send_returns_build_error_for_invalid_mailbox() {
let m = LettreMailer::starttls("smtp.example.com", "u", "p").unwrap();
let bad = EmailMessage::new("not-a-mailbox", "from@x", "s", "t");
let err = m.send(&bad).await.unwrap_err();
assert!(matches!(err, MailerError::Build(_)));
}
}