1use std::future::Future;
2use std::pin::Pin;
3
4use crate::error::AuthError;
5
6#[derive(Clone)]
8pub struct EmailMessage {
9 pub to: String,
10 pub subject: String,
11 pub template: EmailTemplate,
12}
13
14#[non_exhaustive]
20#[derive(Clone)]
21pub enum EmailTemplate {
22 EmailVerification {
23 url: String,
24 username: String,
25 },
26 PasswordReset {
27 url: String,
28 username: String,
29 },
30 MfaRecovery {
31 codes: Vec<String>,
32 username: String,
33 },
34 Invitation {
40 url: String,
41 invited_by: String,
42 },
43}
44
45pub trait EmailSender: Send + Sync {
56 fn send<'a>(
57 &'a self,
58 message: &'a EmailMessage,
59 ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'a>>;
60}
61
62pub struct LogEmailSender;
67
68impl EmailSender for LogEmailSender {
69 fn send<'a>(
70 &'a self,
71 message: &'a EmailMessage,
72 ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'a>> {
73 tracing::info!(
74 to = %message.to,
75 subject = %message.subject,
76 template = ?message.template,
77 "dev email (not delivered)"
78 );
79 Box::pin(std::future::ready(Ok(())))
80 }
81}
82
83pub struct NoopEmailSender;
93
94impl EmailSender for NoopEmailSender {
95 fn send<'a>(
96 &'a self,
97 message: &'a EmailMessage,
98 ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'a>> {
99 tracing::debug!(
100 to = %message.to,
101 subject = %message.subject,
102 "email dropped (NoopEmailSender)"
103 );
104 Box::pin(std::future::ready(Ok(())))
105 }
106}
107
108impl<T: EmailSender + ?Sized> EmailSender for std::sync::Arc<T> {
114 fn send<'a>(
115 &'a self,
116 message: &'a EmailMessage,
117 ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'a>> {
118 (**self).send(message)
119 }
120}
121
122pub(crate) fn fallback_username(user: &crate::types::User) -> String {
130 if let Some(u) = &user.username {
131 return u.as_str().to_owned();
132 }
133 user.email
134 .as_str()
135 .split('@')
136 .next()
137 .filter(|s| !s.is_empty())
138 .unwrap_or("there")
139 .to_owned()
140}
141
142impl std::fmt::Debug for EmailTemplate {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 match self {
146 EmailTemplate::EmailVerification { url, username } => f
147 .debug_struct("EmailVerification")
148 .field("url", url)
149 .field("username", username)
150 .finish(),
151 EmailTemplate::PasswordReset { url, username } => f
152 .debug_struct("PasswordReset")
153 .field("url", url)
154 .field("username", username)
155 .finish(),
156 EmailTemplate::MfaRecovery { codes, username } => f
157 .debug_struct("MfaRecovery")
158 .field("codes_count", &codes.len())
159 .field("username", username)
160 .finish(),
161 EmailTemplate::Invitation { url, invited_by } => f
162 .debug_struct("Invitation")
163 .field("url", url)
164 .field("invited_by", invited_by)
165 .finish(),
166 }
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn _assert_object_safe(_: &dyn EmailSender) {}
176
177 fn make_reset_message() -> EmailMessage {
178 EmailMessage {
179 to: "user@example.com".to_owned(),
180 subject: "Reset your password".to_owned(),
181 template: EmailTemplate::PasswordReset {
182 url: "https://example.com/reset?token=abc".to_owned(),
183 username: "user".to_owned(),
184 },
185 }
186 }
187
188 #[tokio::test]
189 async fn log_sender_succeeds() {
190 let sender = LogEmailSender;
191 let msg = make_reset_message();
192 let result = sender.send(&msg).await;
193 assert!(result.is_ok());
194 }
195
196 #[tokio::test]
197 async fn log_sender_succeeds_with_invitation_template() {
198 let sender = LogEmailSender;
199 let msg = EmailMessage {
200 to: "invitee@example.com".to_owned(),
201 subject: "You've been invited".to_owned(),
202 template: EmailTemplate::Invitation {
203 url: "https://example.com/invite/tok123".to_owned(),
204 invited_by: "Acme Corp".to_owned(),
205 },
206 };
207 let result = sender.send(&msg).await;
208 assert!(result.is_ok());
209 }
210
211 #[tokio::test]
212 async fn noop_sender_succeeds() {
213 let sender = NoopEmailSender;
214 let msg = make_reset_message();
215 let result = sender.send(&msg).await;
216 assert!(result.is_ok());
217 }
218
219 #[tokio::test]
220 async fn trait_object_dispatch_works() {
221 let sender: Box<dyn EmailSender> = Box::new(LogEmailSender);
222 let msg = make_reset_message();
223 let result = sender.send(&msg).await;
224 assert!(result.is_ok());
225 }
226
227 #[tokio::test]
228 async fn noop_trait_object_dispatch_works() {
229 let sender: Box<dyn EmailSender> = Box::new(NoopEmailSender);
230 let msg = make_reset_message();
231 let result = sender.send(&msg).await;
232 assert!(result.is_ok());
233 }
234}