use async_trait::async_trait;
use thiserror::Error;
use uuid::Uuid;
use crate::RepositoryError;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Email {
pub to: String,
pub subject: String,
pub html: String,
pub text: Option<String>,
}
impl Email {
pub fn new(to: impl Into<String>, subject: impl Into<String>, html: impl Into<String>) -> Self {
Self {
to: to.into(),
subject: subject.into(),
html: html.into(),
text: None,
}
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum MailError {
#[error("mail.transient: {0}")]
Transient(String),
#[error("mail.permanent: {0}")]
Permanent(String),
}
#[async_trait]
pub trait Mailer: Send + Sync {
async fn send(&self, email: &Email) -> Result<(), MailError>;
}
#[async_trait]
pub trait SentMails: Send + Sync {
async fn was_sent(&self, event_id: Uuid, mail: &str) -> Result<bool, RepositoryError>;
async fn mark_sent(&self, event_id: Uuid, mail: &str) -> Result<(), RepositoryError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_be_html_only_when_built_without_text() {
let email = Email::new("ada@example.com", "Welcome", "<p>Hi Ada</p>");
assert_eq!(email.to, "ada@example.com");
assert_eq!(email.subject, "Welcome");
assert_eq!(email.html, "<p>Hi Ada</p>");
assert_eq!(email.text, None);
}
#[test]
fn should_carry_the_text_alternative_when_given_one() {
let email = Email::new("ada@example.com", "Welcome", "<p>Hi</p>").with_text("Hi");
assert_eq!(email.text.as_deref(), Some("Hi"));
}
#[test]
fn should_keep_error_codes_stable_when_logged() {
assert_eq!(
MailError::Transient("refused".into()).to_string(),
"mail.transient: refused"
);
assert_eq!(
MailError::Permanent("550 no such user".into()).to_string(),
"mail.permanent: 550 no such user"
);
}
}