Skip to main content

better_auth_core/
email.rs

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