1#![doc = include_str!("readme.md")]
2#![warn(missing_docs)]
3
4pub mod mime;
5pub mod sendmail;
6pub mod smtp;
7
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11use wae_types::{WaeError, WaeResult};
12
13use crate::{
14 mime::{EmailBuilder, EmailMessage},
15 sendmail::{SendmailConfig, SendmailTransport},
16 smtp::{AuthMechanism, SmtpClient, SmtpClientBuilder, SmtpError},
17};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SmtpConfig {
22 pub host: String,
24 pub port: u16,
26 pub username: String,
28 pub password: String,
30 pub from_email: String,
32}
33
34#[async_trait]
38pub trait EmailProvider: Send + Sync {
39 async fn send_email(&self, to: &str, subject: &str, body: &str) -> WaeResult<()>;
46}
47
48pub struct SmtpEmailProvider {
52 config: SmtpConfig,
54}
55
56impl SmtpEmailProvider {
57 pub fn new(config: SmtpConfig) -> Self {
59 Self { config }
60 }
61
62 fn build_client(&self) -> SmtpClient {
64 SmtpClientBuilder::new(&self.config.host, self.config.port)
65 .timeout(Duration::from_secs(30))
66 .use_starttls(true)
67 .auth_mechanism(AuthMechanism::Login)
68 .build()
69 }
70
71 fn map_smtp_error(e: SmtpError) -> WaeError {
73 WaeError::connection_failed(format!("SMTP error: {}", e))
74 }
75
76 pub fn build_email(&self, to: &str, subject: &str, body: &str) -> EmailMessage {
78 EmailBuilder::new().from(&self.config.from_email).to(to).subject(subject).body(body).build()
79 }
80}
81
82#[async_trait]
83impl EmailProvider for SmtpEmailProvider {
84 async fn send_email(&self, to: &str, subject: &str, body: &str) -> WaeResult<()> {
85 let mut client = self.build_client();
86
87 client.connect().await.map_err(Self::map_smtp_error)?;
88 client.ehlo().await.map_err(Self::map_smtp_error)?;
89 client.starttls().await.map_err(Self::map_smtp_error)?;
90 client.authenticate(&self.config.username, &self.config.password).await.map_err(Self::map_smtp_error)?;
91
92 let email = self.build_email(to, subject, body);
93 let content = email.to_string();
94
95 client.send_mail(&self.config.from_email, &[to], &content).await.map_err(Self::map_smtp_error)?;
96
97 let _ = client.quit().await;
98
99 Ok(())
100 }
101}
102
103pub struct SendmailEmailProvider {
107 transport: SendmailTransport,
109 from_email: String,
111}
112
113impl SendmailEmailProvider {
114 pub fn new(from_email: String) -> Self {
116 Self { transport: SendmailTransport::new(), from_email }
117 }
118
119 pub fn with_config(from_email: String, config: SendmailConfig) -> Self {
121 Self { transport: SendmailTransport::with_config(config), from_email }
122 }
123
124 pub fn build_email(&self, to: &str, subject: &str, body: &str) -> EmailMessage {
126 EmailBuilder::new().from(&self.from_email).to(to).subject(subject).body(body).build()
127 }
128}
129
130#[async_trait]
131impl EmailProvider for SendmailEmailProvider {
132 async fn send_email(&self, to: &str, subject: &str, body: &str) -> WaeResult<()> {
133 let email = self.build_email(to, subject, body);
134 let raw_email = email.to_string();
135 self.transport.send_raw(&raw_email).await
136 }
137}
138
139pub struct DirectEmailProvider {
144 from_email: String,
146}
147
148impl DirectEmailProvider {
149 pub fn new(from_email: String) -> Self {
151 Self { from_email }
152 }
153
154 async fn resolve_mx(&self, domain: &str) -> WaeResult<String> {
156 use hickory_resolver::{Resolver, name_server::TokioConnectionProvider};
157
158 let resolver = Resolver::builder_with_config(
159 hickory_resolver::config::ResolverConfig::default(),
160 TokioConnectionProvider::default(),
161 )
162 .build();
163
164 let response = resolver
165 .mx_lookup(domain)
166 .await
167 .map_err(|_| WaeError::connection_failed(format!("DNS resolution failed for {}", domain)))?;
168
169 let mx = response
170 .iter()
171 .min_by_key(|mx| mx.preference())
172 .ok_or_else(|| WaeError::storage_file_not_found(format!("No MX records found for {}", domain)))?;
173
174 Ok(mx.exchange().to_string().trim_end_matches('.').to_string())
175 }
176
177 pub fn build_email(&self, to: &str, subject: &str, body: &str) -> EmailMessage {
179 EmailBuilder::new().from(&self.from_email).to(to).subject(subject).body(body).build()
180 }
181}
182
183#[async_trait]
184impl EmailProvider for DirectEmailProvider {
185 async fn send_email(&self, to: &str, subject: &str, body: &str) -> WaeResult<()> {
186 let domain =
187 to.split('@').nth(1).ok_or_else(|| WaeError::invalid_format("email", format!("Invalid email address: {}", to)))?;
188
189 let mx_host = self.resolve_mx(domain).await?;
190
191 let mut client = SmtpClientBuilder::new(&mx_host, 25).timeout(Duration::from_secs(30)).use_starttls(false).build();
192
193 client.connect().await.map_err(|e| WaeError::connection_failed(format!("{}: {}", mx_host, e)))?;
194 client.ehlo().await.map_err(|e| WaeError::connection_failed(format!("{}: EHLO failed: {}", mx_host, e)))?;
195
196 let email = self.build_email(to, subject, body);
197 let content = email.to_string();
198
199 client
200 .send_mail(&self.from_email, &[to], &content)
201 .await
202 .map_err(|e| WaeError::connection_failed(format!("{}: {}", mx_host, e)))?;
203
204 let _ = client.quit().await;
205
206 Ok(())
207 }
208}