Skip to main content

aetheris_server/auth/
email.rs

1use 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}