use async_trait::async_trait;
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct EmailMessage {
pub to: String,
pub from: String,
pub reply_to: Option<String>,
pub subject: String,
pub text: String,
pub html: Option<String>,
}
impl EmailMessage {
pub fn new(
to: impl Into<String>,
from: impl Into<String>,
subject: impl Into<String>,
text: impl Into<String>,
) -> Self {
Self {
to: to.into(),
from: from.into(),
reply_to: None,
subject: subject.into(),
text: text.into(),
html: None,
}
}
pub fn with_html(mut self, html: impl Into<String>) -> Self {
self.html = Some(html.into());
self
}
pub fn with_reply_to(mut self, reply_to: impl Into<String>) -> Self {
self.reply_to = Some(reply_to.into());
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum MailerError {
#[error("build: {0}")]
Build(String),
#[error("transport: {0}")]
Transport(String),
}
#[async_trait]
pub trait Mailer: Send + Sync {
async fn send(&self, msg: &EmailMessage) -> Result<(), MailerError>;
}
#[derive(Default)]
pub struct CapturingMailer {
sent: Mutex<Vec<EmailMessage>>,
}
impl CapturingMailer {
pub fn new() -> Self {
Self::default()
}
pub fn captured(&self) -> Vec<EmailMessage> {
self.sent.lock().unwrap().clone()
}
pub fn last(&self) -> Option<EmailMessage> {
self.sent.lock().unwrap().last().cloned()
}
pub fn len(&self) -> usize {
self.sent.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.sent.lock().unwrap().is_empty()
}
pub fn clear(&self) {
self.sent.lock().unwrap().clear();
}
}
#[async_trait]
impl Mailer for CapturingMailer {
async fn send(&self, msg: &EmailMessage) -> Result<(), MailerError> {
self.sent.lock().unwrap().push(msg.clone());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn email_message_builder_sets_optional_fields() {
let m = EmailMessage::new("to@x", "from@x", "Hi", "plain")
.with_html("<p>html</p>")
.with_reply_to("reply@x");
assert_eq!(m.html.as_deref(), Some("<p>html</p>"));
assert_eq!(m.reply_to.as_deref(), Some("reply@x"));
}
#[test]
fn capturing_mailer_records_each_send() {
pollster::block_on(async {
let m = CapturingMailer::new();
assert!(m.is_empty());
let msg = EmailMessage::new("to@x", "from@x", "s", "t");
m.send(&msg).await.unwrap();
m.send(&msg).await.unwrap();
assert_eq!(m.len(), 2);
assert_eq!(m.last().unwrap(), msg);
m.clear();
assert!(m.is_empty());
});
}
}