1use async_trait::async_trait;
7use thiserror::Error;
8use uuid::Uuid;
9
10use crate::RepositoryError;
11
12#[non_exhaustive]
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Email {
18 pub to: String,
19 pub subject: String,
20 pub html: String,
21 pub text: Option<String>,
23}
24
25impl Email {
26 pub fn new(to: impl Into<String>, subject: impl Into<String>, html: impl Into<String>) -> Self {
27 Self {
28 to: to.into(),
29 subject: subject.into(),
30 html: html.into(),
31 text: None,
32 }
33 }
34
35 pub fn with_text(mut self, text: impl Into<String>) -> Self {
36 self.text = Some(text.into());
37 self
38 }
39}
40
41#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq, Eq, Error)]
44pub enum MailError {
45 #[error("mail.transient: {0}")]
47 Transient(String),
48 #[error("mail.permanent: {0}")]
50 Permanent(String),
51}
52
53#[async_trait]
55pub trait Mailer: Send + Sync {
56 async fn send(&self, email: &Email) -> Result<(), MailError>;
57}
58
59#[async_trait]
62pub trait SentMails: Send + Sync {
63 async fn was_sent(&self, event_id: Uuid, mail: &str) -> Result<bool, RepositoryError>;
64 async fn mark_sent(&self, event_id: Uuid, mail: &str) -> Result<(), RepositoryError>;
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn should_be_html_only_when_built_without_text() {
74 let email = Email::new("ada@example.com", "Welcome", "<p>Hi Ada</p>");
76
77 assert_eq!(email.to, "ada@example.com");
79 assert_eq!(email.subject, "Welcome");
80 assert_eq!(email.html, "<p>Hi Ada</p>");
81 assert_eq!(email.text, None);
82 }
83
84 #[test]
85 fn should_carry_the_text_alternative_when_given_one() {
86 let email = Email::new("ada@example.com", "Welcome", "<p>Hi</p>").with_text("Hi");
88
89 assert_eq!(email.text.as_deref(), Some("Hi"));
91 }
92
93 #[test]
94 fn should_keep_error_codes_stable_when_logged() {
95 assert_eq!(
96 MailError::Transient("refused".into()).to_string(),
97 "mail.transient: refused"
98 );
99 assert_eq!(
100 MailError::Permanent("550 no such user".into()).to_string(),
101 "mail.permanent: 550 no such user"
102 );
103 }
104}