Skip to main content

wae_email/
lib.rs

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/// SMTP 配置
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SmtpConfig {
22    /// 服务器主机名
23    pub host: String,
24    /// 服务器端口
25    pub port: u16,
26    /// 用户名
27    pub username: String,
28    /// 密码或授权码
29    pub password: String,
30    /// 发件人邮箱
31    pub from_email: String,
32}
33
34/// 邮件提供者 trait
35///
36/// 定义邮件发送的统一接口。
37#[async_trait]
38pub trait EmailProvider: Send + Sync {
39    /// 发送邮件
40    ///
41    /// # 参数
42    /// - `to`: 收件人邮箱地址
43    /// - `subject`: 邮件主题
44    /// - `body`: 邮件正文(纯文本)
45    async fn send_email(&self, to: &str, subject: &str, body: &str) -> WaeResult<()>;
46}
47
48/// SMTP 邮件提供者
49///
50/// 通过 SMTP 协议发送邮件,支持 STARTTLS 加密和认证。
51pub struct SmtpEmailProvider {
52    /// 配置
53    config: SmtpConfig,
54}
55
56impl SmtpEmailProvider {
57    /// 创建新的 SMTP 邮件提供者
58    pub fn new(config: SmtpConfig) -> Self {
59        Self { config }
60    }
61
62    /// 构建 SMTP 客户端
63    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    /// 将 SMTP 错误转换为 WaeError
72    fn map_smtp_error(e: SmtpError) -> WaeError {
73        WaeError::connection_failed(format!("SMTP error: {}", e))
74    }
75
76    /// 构建邮件消息
77    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
103/// Sendmail 邮件提供者
104///
105/// 通过本地 sendmail 命令发送邮件。
106pub struct SendmailEmailProvider {
107    /// 传输实例
108    transport: SendmailTransport,
109    /// 发件人邮箱
110    from_email: String,
111}
112
113impl SendmailEmailProvider {
114    /// 创建新的 Sendmail 邮件提供者
115    pub fn new(from_email: String) -> Self {
116        Self { transport: SendmailTransport::new(), from_email }
117    }
118
119    /// 使用指定配置创建 Sendmail 邮件提供者
120    pub fn with_config(from_email: String, config: SendmailConfig) -> Self {
121        Self { transport: SendmailTransport::with_config(config), from_email }
122    }
123
124    /// 构建邮件消息
125    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
139/// 直接交付邮件提供者
140///
141/// 直接查询收件人域名的 MX 记录,并连接目标邮件服务器发送邮件。
142/// 这种方式让程序直接充当 MTA(邮件传输代理)。
143pub struct DirectEmailProvider {
144    /// 发件人邮箱
145    from_email: String,
146}
147
148impl DirectEmailProvider {
149    /// 创建新的直接交付邮件提供者
150    pub fn new(from_email: String) -> Self {
151        Self { from_email }
152    }
153
154    /// 解析域名的 MX 记录
155    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    /// 构建邮件消息
178    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}