1use std::sync::Arc;
4
5use async_trait::async_trait;
6use lettre::message::header::ContentType;
7use lettre::transport::smtp::authentication::Credentials;
8use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
9use parking_lot::Mutex;
10
11use crate::config::MailConfig;
12use crate::Error;
13
14#[async_trait]
15pub trait MailDriver: Send + Sync {
16 async fn send(&self, message: OutgoingMessage) -> Result<(), Error>;
17}
18
19#[derive(Debug, Clone)]
20pub struct OutgoingMessage {
21 pub from: String,
22 pub to: Vec<String>,
23 pub cc: Vec<String>,
24 pub bcc: Vec<String>,
25 pub subject: String,
26 pub html_body: Option<String>,
27 pub text_body: Option<String>,
28}
29
30#[derive(Clone)]
31pub struct MailerHandle {
32 driver: Arc<dyn MailDriver>,
33}
34
35impl MailerHandle {
36 pub fn new(driver: Arc<dyn MailDriver>) -> Self {
37 Self { driver }
38 }
39
40 pub fn null() -> Self {
41 Self {
42 driver: Arc::new(NullMailDriver),
43 }
44 }
45
46 pub fn fake() -> (Self, Arc<Mutex<Vec<OutgoingMessage>>>) {
47 let outbox = Arc::new(Mutex::new(Vec::new()));
48 let driver = FakeDriver {
49 outbox: outbox.clone(),
50 };
51 (
52 Self {
53 driver: Arc::new(driver),
54 },
55 outbox,
56 )
57 }
58
59 pub fn smtp(config: &MailConfig) -> Result<Self, Error> {
60 let mut builder = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&config.host)
61 .port(config.port);
62 if !config.username.is_empty() {
63 builder = builder.credentials(Credentials::new(
64 config.username.clone(),
65 config.password.clone(),
66 ));
67 }
68 let transport = builder.build();
69 Ok(Self {
70 driver: Arc::new(SmtpDriver {
71 transport,
72 default_from: format!("{} <{}>", config.from_name, config.from_address),
73 }),
74 })
75 }
76
77 pub async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
78 self.driver.send(message).await
79 }
80}
81
82#[async_trait]
84pub trait Mailable: Send + Sync {
85 async fn build(&self) -> Result<OutgoingMessage, Error>;
86}
87
88pub async fn to(addr: impl Into<String>, mailer: &MailerHandle, mailable: impl Mailable) -> Result<(), Error> {
89 let mut msg = mailable.build().await?;
90 msg.to.push(addr.into());
91 mailer.send(msg).await
92}
93
94struct NullMailDriver;
95
96#[async_trait]
97impl MailDriver for NullMailDriver {
98 async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
99 tracing::debug!(?message, "null mail driver dropped message");
100 Ok(())
101 }
102}
103
104struct FakeDriver {
105 outbox: Arc<Mutex<Vec<OutgoingMessage>>>,
106}
107
108#[async_trait]
109impl MailDriver for FakeDriver {
110 async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
111 self.outbox.lock().push(message);
112 Ok(())
113 }
114}
115
116struct SmtpDriver {
117 transport: AsyncSmtpTransport<Tokio1Executor>,
118 default_from: String,
119}
120
121#[async_trait]
122impl MailDriver for SmtpDriver {
123 async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
124 let from = if message.from.is_empty() {
125 self.default_from.clone()
126 } else {
127 message.from.clone()
128 };
129
130 let mut builder = Message::builder()
131 .from(from.parse().map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?);
132 for to in &message.to {
133 builder = builder.to(to.parse().map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?);
134 }
135 for cc in &message.cc {
136 builder = builder.cc(cc.parse().map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?);
137 }
138 for bcc in &message.bcc {
139 builder = builder.bcc(bcc.parse().map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?);
140 }
141 let builder = builder.subject(&message.subject);
142
143 let msg = if let Some(html) = &message.html_body {
144 builder
145 .header(ContentType::TEXT_HTML)
146 .body(html.clone())
147 .map_err(|e| Error::Mail(e.to_string()))?
148 } else if let Some(text) = &message.text_body {
149 builder
150 .header(ContentType::TEXT_PLAIN)
151 .body(text.clone())
152 .map_err(|e| Error::Mail(e.to_string()))?
153 } else {
154 return Err(Error::Mail("mail has no body".into()));
155 };
156
157 self.transport
158 .send(msg)
159 .await
160 .map_err(|e| Error::Mail(e.to_string()))?;
161 Ok(())
162 }
163}