Skip to main content

better_auth_core/
email.rs

1use async_trait::async_trait;
2
3use crate::error::AuthResult;
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.
21pub struct ConsoleEmailProvider;
22
23#[async_trait]
24impl EmailProvider for ConsoleEmailProvider {
25    async fn send(&self, to: &str, subject: &str, _html: &str, text: &str) -> AuthResult<()> {
26        eprintln!("[EMAIL] To: {to} | Subject: {subject} | Body: {text}");
27        Ok(())
28    }
29}
30
31#[cfg(test)]
32#[allow(clippy::type_complexity)]
33mod tests {
34    use super::*;
35    use std::sync::{Arc, Mutex};
36
37    /// Mock email provider for testing.
38    struct MockEmailProvider {
39        sent: Arc<Mutex<Vec<(String, String, String, String)>>>,
40    }
41
42    impl MockEmailProvider {
43        fn new() -> (Self, Arc<Mutex<Vec<(String, String, String, String)>>>) {
44            let sent = Arc::new(Mutex::new(Vec::new()));
45            (Self { sent: sent.clone() }, sent)
46        }
47    }
48
49    #[async_trait]
50    impl EmailProvider for MockEmailProvider {
51        async fn send(&self, to: &str, subject: &str, html: &str, text: &str) -> AuthResult<()> {
52            self.sent.lock().unwrap().push((
53                to.to_string(),
54                subject.to_string(),
55                html.to_string(),
56                text.to_string(),
57            ));
58            Ok(())
59        }
60    }
61
62    #[tokio::test]
63    async fn test_console_email_provider_send() {
64        let provider = ConsoleEmailProvider;
65        let result = provider
66            .send("user@example.com", "Test Subject", "<h1>Hi</h1>", "Hi")
67            .await;
68        assert!(result.is_ok());
69    }
70
71    #[tokio::test]
72    async fn test_mock_email_provider_records_sends() {
73        let (provider, sent) = MockEmailProvider::new();
74        provider
75            .send("a@b.com", "Sub", "<p>html</p>", "text")
76            .await
77            .unwrap();
78        provider.send("c@d.com", "Sub2", "", "text2").await.unwrap();
79
80        let messages = sent.lock().unwrap();
81        assert_eq!(messages.len(), 2);
82        assert_eq!(messages[0].0, "a@b.com");
83        assert_eq!(messages[1].0, "c@d.com");
84    }
85
86    #[tokio::test]
87    async fn test_trait_object_works() {
88        let provider: Box<dyn EmailProvider> = Box::new(ConsoleEmailProvider);
89        let result = provider.send("user@example.com", "Test", "", "body").await;
90        assert!(result.is_ok());
91    }
92
93    #[tokio::test]
94    async fn test_missing_provider_returns_error() {
95        use crate::adapters::MemoryDatabaseAdapter;
96        use crate::config::AuthConfig;
97        use crate::plugin::AuthContext;
98
99        let config = Arc::new(AuthConfig::new("test-secret-key-at-least-32-chars-long"));
100        let database = Arc::new(MemoryDatabaseAdapter::new());
101        let ctx = AuthContext::new(config, database);
102
103        let result = ctx.email_provider();
104        assert!(result.is_err());
105    }
106}