Skip to main content

alibi_core/email/
mod.rs

1use crate::error::AuthResult;
2use async_trait::async_trait;
3use std::io::Write;
4
5/// Trait for sending emails. Implement this to integrate with your
6/// email service (SMTP, `SendGrid`, SES, etc.).
7#[async_trait]
8pub trait EmailProvider: Send + Sync {
9    /// Send an email.
10    ///
11    /// - `to`: recipient email address
12    /// - `subject`: email subject line
13    /// - `html`: HTML body (may be empty)
14    /// - `text`: plain-text body (may be empty)
15    async fn send(&self, to: &str, subject: &str, html: &str, text: &str) -> AuthResult<()>;
16}
17
18/// Development email provider that logs emails to stderr.
19///
20/// Useful for local development and testing — no external dependencies.
21#[derive(Debug)]
22pub struct ConsoleEmailProvider;
23
24#[async_trait]
25impl EmailProvider for ConsoleEmailProvider {
26    async fn send(&self, to: &str, subject: &str, _html: &str, text: &str) -> AuthResult<()> {
27        _ = writeln!(
28            std::io::stderr().lock(),
29            "[EMAIL] To: {to} | Subject: {subject} | Body: {text}"
30        );
31        Ok(())
32    }
33}
34
35// LCOV_EXCL_START
36#[cfg(test)]
37mod tests {
38    use super::*;
39    use std::sync::{Arc, Mutex};
40
41    type SentMessages = Arc<Mutex<Vec<(String, String, String, String)>>>;
42
43    /// Mock email provider for testing.
44    struct MockEmailProvider {
45        sent: SentMessages,
46    }
47
48    impl MockEmailProvider {
49        fn new() -> (Self, SentMessages) {
50            let sent = Arc::new(Mutex::new(Vec::new()));
51            (
52                Self {
53                    sent: Arc::clone(&sent),
54                },
55                sent,
56            )
57        }
58    }
59
60    #[async_trait]
61    impl EmailProvider for MockEmailProvider {
62        async fn send(&self, to: &str, subject: &str, html: &str, text: &str) -> AuthResult<()> {
63            self.sent.lock().unwrap().push((
64                to.to_owned(),
65                subject.to_owned(),
66                html.to_owned(),
67                text.to_owned(),
68            ));
69            Ok(())
70        }
71    }
72
73    // Rust-specific surface: Rust email provider helpers are library-specific behavior with no direct TS analogue.
74    #[tokio::test]
75    async fn test_console_email_provider_send() {
76        let provider = ConsoleEmailProvider;
77        let result = provider
78            .send("user@example.com", "Test Subject", "<h1>Hi</h1>", "Hi")
79            .await;
80        assert!(result.is_ok());
81    }
82
83    // Rust-specific surface: Rust email provider helpers are library-specific behavior with no direct TS analogue.
84    #[tokio::test]
85    async fn test_mock_email_provider_records_sends() {
86        let (provider, sent) = MockEmailProvider::new();
87        provider
88            .send("a@b.com", "Sub", "<p>html</p>", "text")
89            .await
90            .unwrap();
91        provider.send("c@d.com", "Sub2", "", "text2").await.unwrap();
92
93        let messages = sent.lock().unwrap();
94        assert_eq!(messages.len(), 2);
95        assert_eq!(
96            (messages)
97                .first()
98                .expect("fixture contains the requested index")
99                .0,
100            "a@b.com"
101        );
102        assert_eq!(
103            (messages)
104                .get(1)
105                .expect("fixture contains the requested index")
106                .0,
107            "c@d.com"
108        );
109    }
110
111    // Rust-specific surface: Rust email provider helpers are library-specific behavior with no direct TS analogue.
112    #[tokio::test]
113    async fn test_trait_object_works() {
114        let provider: Box<dyn EmailProvider> = Box::new(ConsoleEmailProvider);
115        let result = provider.send("user@example.com", "Test", "", "body").await;
116        assert!(result.is_ok());
117    }
118
119    // Rust-specific surface: Rust email provider helpers are library-specific behavior with no direct TS analogue.
120    #[tokio::test]
121    async fn test_missing_provider_returns_error() {
122        use crate::plugin::AuthContext;
123        use crate::test_store::{test_config, test_database};
124
125        let config = test_config();
126        let database = test_database().await;
127        let ctx = AuthContext::new(config, database);
128
129        let result = ctx.email_provider();
130        assert!(result.is_err());
131    }
132}
133// LCOV_EXCL_STOP