Skip to main content

allowthem_core/
email.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use crate::error::AuthError;
5
6/// An email message to be sent.
7pub struct EmailMessage<'a> {
8    pub to: &'a str,
9    pub subject: &'a str,
10    pub body: &'a str,
11    pub html: Option<&'a str>,
12}
13
14/// Abstraction over email delivery.
15///
16/// Implementors are responsible for the actual transport (SMTP, SES, SendGrid,
17/// etc.). The library provides [`LogEmailSender`] for development, which
18/// prints the message to the tracing log instead of delivering it.
19///
20/// Implement this trait and pass it to the builder when email delivery is
21/// needed (password reset, email verification, etc.).
22pub trait EmailSender: Send + Sync {
23    fn send<'a>(
24        &'a self,
25        message: EmailMessage<'a>,
26    ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'a>>;
27}
28
29/// Development email sender that logs messages instead of delivering them.
30///
31/// Writes each field of the message at `info` level so they appear in
32/// local dev output. Does not perform any network I/O. Returns `Ok(())`.
33pub struct LogEmailSender;
34
35impl EmailSender for LogEmailSender {
36    fn send<'a>(
37        &'a self,
38        message: EmailMessage<'a>,
39    ) -> Pin<Box<dyn Future<Output = Result<(), AuthError>> + Send + 'a>> {
40        tracing::info!(
41            to = message.to,
42            subject = message.subject,
43            body = message.body,
44            html = message.html,
45            "dev email (not delivered)"
46        );
47        Box::pin(std::future::ready(Ok(())))
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    // Compile-time proof that EmailSender is dyn-compatible.
56    fn _assert_object_safe(_: &dyn EmailSender) {}
57
58    #[tokio::test]
59    async fn log_sender_succeeds() {
60        let sender = LogEmailSender;
61        let msg = EmailMessage {
62            to: "user@example.com",
63            subject: "Test",
64            body: "Hello",
65            html: None,
66        };
67        let result = sender.send(msg).await;
68        assert!(result.is_ok());
69    }
70
71    #[tokio::test]
72    async fn log_sender_succeeds_with_html() {
73        let sender = LogEmailSender;
74        let msg = EmailMessage {
75            to: "user@example.com",
76            subject: "Test",
77            body: "Hello",
78            html: Some("<p>Hello</p>"),
79        };
80        let result = sender.send(msg).await;
81        assert!(result.is_ok());
82    }
83
84    #[tokio::test]
85    async fn trait_object_dispatch_works() {
86        let sender: Box<dyn EmailSender> = Box::new(LogEmailSender);
87        let msg = EmailMessage {
88            to: "user@example.com",
89            subject: "Subject",
90            body: "Body",
91            html: None,
92        };
93        let result = sender.send(msg).await;
94        assert!(result.is_ok());
95    }
96}