aetheris_server/auth/
email.rs1use async_trait::async_trait;
2use lettre::transport::smtp::authentication::Credentials;
3use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
4use tracing::{debug, error, info};
5
6#[async_trait]
7pub trait EmailSender: Send + Sync {
8 async fn send(
9 &self,
10 to: &str,
11 subject: &str,
12 plaintext: &str,
13 html: &str,
14 ) -> Result<(), String>;
15}
16
17pub struct LogEmailSender;
18
19#[async_trait]
20impl EmailSender for LogEmailSender {
21 async fn send(
22 &self,
23 to: &str,
24 subject: &str,
25 plaintext: &str,
26 _html: &str,
27 ) -> Result<(), String> {
28 info!("Sending email to: {} Subject: {}", to, subject);
29 debug!("Body: {}", plaintext);
30 Ok(())
31 }
32}
33
34pub struct LettreSmtpEmailSender {
35 transport: AsyncSmtpTransport<Tokio1Executor>,
36 from: String,
37}
38
39impl LettreSmtpEmailSender {
40 pub fn from_env() -> Result<Self, String> {
41 let smtp_url = std::env::var("SMTP_URL").map_err(|_| "SMTP_URL missing")?;
42 let smtp_user = std::env::var("SMTP_USERNAME").map_err(|_| "SMTP_USERNAME missing")?;
43 let smtp_pass = std::env::var("SMTP_PASSWORD").map_err(|_| "SMTP_PASSWORD missing")?;
44 let from = std::env::var("SMTP_FROM").map_err(|_| "SMTP_FROM missing")?;
45
46 let creds = Credentials::new(smtp_user, smtp_pass);
47 let transport = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp_url)
48 .map_err(|e| e.to_string())?
49 .credentials(creds)
50 .build();
51
52 Ok(Self { transport, from })
53 }
54}
55
56#[async_trait]
57impl EmailSender for LettreSmtpEmailSender {
58 async fn send(
59 &self,
60 to: &str,
61 subject: &str,
62 plaintext: &str,
63 html: &str,
64 ) -> Result<(), String> {
65 let email = Message::builder()
66 .from(
67 self.from
68 .parse()
69 .map_err(|e: lettre::address::AddressError| e.to_string())?,
70 )
71 .to(to
72 .parse()
73 .map_err(|e: lettre::address::AddressError| e.to_string())?)
74 .subject(subject)
75 .multipart(
76 lettre::message::MultiPart::alternative()
77 .singlepart(lettre::message::SinglePart::plain(plaintext.to_string()))
78 .singlepart(lettre::message::SinglePart::html(html.to_string())),
79 )
80 .map_err(|e| e.to_string())?;
81
82 let _ = self.transport.send(email).await.map_err(|e| {
83 error!("Failed to send email: {}", e);
84 e.to_string()
85 })?;
86 Ok(())
87 }
88}
89pub struct ResendEmailSender {
90 client: reqwest::Client,
91 api_key: String,
92 from: String,
93}
94
95impl ResendEmailSender {
96 #[must_use]
97 pub fn new(api_key: String, from: String) -> Self {
98 let client = reqwest::Client::builder()
99 .timeout(std::time::Duration::from_secs(10))
100 .connect_timeout(std::time::Duration::from_secs(5))
101 .pool_max_idle_per_host(2)
102 .build()
103 .unwrap_or_else(|_| reqwest::Client::new());
104
105 Self {
106 client,
107 api_key,
108 from,
109 }
110 }
111
112 pub fn from_env() -> Result<Self, String> {
113 let api_key = std::env::var("RESEND_API_KEY").map_err(|_| "RESEND_API_KEY missing")?;
114 let from =
115 std::env::var("RESEND_FROM").unwrap_or_else(|_| "onboarding@resend.dev".to_string());
116 Ok(Self::new(api_key, from))
117 }
118}
119
120#[async_trait]
121impl EmailSender for ResendEmailSender {
122 async fn send(
123 &self,
124 to: &str,
125 subject: &str,
126 plaintext: &str,
127 html: &str,
128 ) -> Result<(), String> {
129 let body = serde_json::json!({
130 "from": self.from,
131 "to": [to],
132 "subject": subject,
133 "text": plaintext,
134 "html": html,
135 });
136
137 let response = self
138 .client
139 .post("https://api.resend.com/emails")
140 .header("Authorization", format!("Bearer {}", self.api_key))
141 .json(&body)
142 .send()
143 .await
144 .map_err(|e| e.to_string())?;
145
146 if response.status().is_success() {
147 Ok(())
148 } else {
149 let status = response.status();
150 let error_text = response.text().await.unwrap_or_default();
151 error!("Resend API error ({status}): {error_text}");
152 Err(format!("Resend API error ({status}): {error_text}"))
153 }
154 }
155}