better_auth_core/
email.rs1use async_trait::async_trait;
2
3use crate::error::AuthResult;
4use std::{future::Future, pin::Pin, sync::Arc};
5
6pub type EmailVerificationHook = Arc<
8 dyn Fn(&crate::wire::UserView) -> Pin<Box<dyn Future<Output = AuthResult<()>> + Send>>
9 + Send
10 + Sync,
11>;
12
13#[async_trait]
15pub trait SendVerificationEmail: Send + Sync {
16 async fn send(&self, user: &crate::wire::UserView, url: &str, token: &str) -> AuthResult<()>;
18}
19
20#[derive(Clone, Default)]
22pub struct EmailVerificationRuntimePolicy {
23 pub auto_sign_in_after_verification: bool,
25 pub before_email_verification: Option<EmailVerificationHook>,
27 pub after_email_verification: Option<EmailVerificationHook>,
29 pub override_sender: Option<Arc<dyn SendVerificationEmail>>,
31}
32
33#[async_trait]
36pub trait EmailProvider: Send + Sync {
37 async fn send(&self, to: &str, subject: &str, html: &str, text: &str) -> AuthResult<()>;
44}
45
46pub 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 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 #[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 #[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 #[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 #[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}