567 lines
19 KiB
Rust
567 lines
19 KiB
Rust
use anyhow::{anyhow, Result};
|
||
use lettre::{
|
||
message::{header::ContentType, Mailbox},
|
||
transport::smtp::{authentication::Credentials},
|
||
Message, SmtpTransport, Transport,
|
||
};
|
||
use serde::{Deserialize, Serialize};
|
||
use std::time::Duration;
|
||
use tracing::{error, info, warn};
|
||
|
||
/// 邮件配置
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct EmailConfig {
|
||
pub smtp_host: String,
|
||
pub smtp_port: u16,
|
||
pub smtp_username: String,
|
||
pub smtp_password: String,
|
||
pub smtp_from: String,
|
||
pub smtp_from_name: String,
|
||
pub use_tls: bool,
|
||
pub use_starttls: bool,
|
||
pub timeout: u64, // 超时时间,秒
|
||
}
|
||
|
||
impl Default for EmailConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
smtp_host: "localhost".to_string(),
|
||
smtp_port: 587,
|
||
smtp_username: "".to_string(),
|
||
smtp_password: "".to_string(),
|
||
smtp_from: "noreply@example.com".to_string(),
|
||
smtp_from_name: "MAPP System".to_string(),
|
||
use_tls: false,
|
||
use_starttls: true,
|
||
timeout: 30,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl EmailConfig {
|
||
/// 从环境变量加载配置
|
||
pub fn from_env() -> Self {
|
||
Self {
|
||
smtp_host: std::env::var("EMAIL_SMTP_HOST").unwrap_or_else(|_| "localhost".to_string()),
|
||
smtp_port: std::env::var("EMAIL_SMTP_PORT")
|
||
.unwrap_or_else(|_| "587".to_string())
|
||
.parse()
|
||
.unwrap_or(587),
|
||
smtp_username: std::env::var("EMAIL_SMTP_USERNAME").unwrap_or_default(),
|
||
smtp_password: std::env::var("EMAIL_SMTP_PASSWORD").unwrap_or_default(),
|
||
smtp_from: std::env::var("EMAIL_SMTP_FROM")
|
||
.unwrap_or_else(|_| "noreply@example.com".to_string()),
|
||
smtp_from_name: std::env::var("EMAIL_SMTP_FROM_NAME")
|
||
.unwrap_or_else(|_| "MAPP System".to_string()),
|
||
use_tls: std::env::var("EMAIL_USE_TLS")
|
||
.unwrap_or_else(|_| "false".to_string())
|
||
.parse()
|
||
.unwrap_or(false),
|
||
use_starttls: std::env::var("EMAIL_USE_STARTTLS")
|
||
.unwrap_or_else(|_| "true".to_string())
|
||
.parse()
|
||
.unwrap_or(true),
|
||
timeout: std::env::var("EMAIL_TIMEOUT")
|
||
.unwrap_or_else(|_| "30".to_string())
|
||
.parse()
|
||
.unwrap_or(30),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 邮件内容类型
|
||
#[derive(Debug, Clone)]
|
||
pub enum EmailContentType {
|
||
Text,
|
||
Html,
|
||
}
|
||
|
||
/// 邮件消息
|
||
#[derive(Debug, Clone)]
|
||
pub struct EmailMessage {
|
||
pub to: Vec<String>,
|
||
pub cc: Option<Vec<String>>,
|
||
pub bcc: Option<Vec<String>>,
|
||
pub subject: String,
|
||
pub body: String,
|
||
pub content_type: EmailContentType,
|
||
pub reply_to: Option<String>,
|
||
}
|
||
|
||
impl EmailMessage {
|
||
/// 创建新的邮件消息
|
||
pub fn new(to: Vec<String>, subject: String, body: String) -> Self {
|
||
Self {
|
||
to,
|
||
cc: None,
|
||
bcc: None,
|
||
subject,
|
||
body,
|
||
content_type: EmailContentType::Html,
|
||
reply_to: None,
|
||
}
|
||
}
|
||
|
||
/// 设置抄送
|
||
pub fn with_cc(mut self, cc: Vec<String>) -> Self {
|
||
self.cc = Some(cc);
|
||
self
|
||
}
|
||
|
||
/// 设置密送
|
||
pub fn with_bcc(mut self, bcc: Vec<String>) -> Self {
|
||
self.bcc = Some(bcc);
|
||
self
|
||
}
|
||
|
||
/// 设置内容类型
|
||
pub fn with_content_type(mut self, content_type: EmailContentType) -> Self {
|
||
self.content_type = content_type;
|
||
self
|
||
}
|
||
|
||
/// 设置回复地址
|
||
pub fn with_reply_to(mut self, reply_to: String) -> Self {
|
||
self.reply_to = Some(reply_to);
|
||
self
|
||
}
|
||
}
|
||
|
||
/// 邮件发送结果
|
||
#[derive(Debug)]
|
||
pub struct EmailResult {
|
||
pub success: bool,
|
||
pub message_id: Option<String>,
|
||
pub error: Option<String>,
|
||
}
|
||
|
||
/// 邮件服务
|
||
#[derive(Debug, Clone)]
|
||
pub struct EmailService {
|
||
config: EmailConfig,
|
||
}
|
||
|
||
impl EmailService {
|
||
/// 创建新的邮件服务实例
|
||
pub fn new(config: EmailConfig) -> Self {
|
||
Self { config }
|
||
}
|
||
|
||
/// 从环境变量创建邮件服务
|
||
pub fn from_env() -> Self {
|
||
Self::new(EmailConfig::from_env())
|
||
}
|
||
|
||
/// 验证邮件配置
|
||
pub fn validate_config(&self) -> Result<()> {
|
||
if self.config.smtp_host.is_empty() {
|
||
return Err(anyhow!("SMTP host is required"));
|
||
}
|
||
if self.config.smtp_from.is_empty() {
|
||
return Err(anyhow!("SMTP from address is required"));
|
||
}
|
||
if !self.config.smtp_username.is_empty() && self.config.smtp_password.is_empty() {
|
||
return Err(anyhow!("SMTP password is required when username is provided"));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 构建 SMTP 传输器
|
||
fn build_transport(&self) -> Result<SmtpTransport> {
|
||
let mut transport_builder = if self.config.use_tls {
|
||
SmtpTransport::relay(&self.config.smtp_host)?
|
||
} else {
|
||
SmtpTransport::builder_dangerous(&self.config.smtp_host)
|
||
};
|
||
|
||
transport_builder = transport_builder
|
||
.port(self.config.smtp_port)
|
||
.timeout(Some(Duration::from_secs(self.config.timeout)));
|
||
|
||
// 配置认证
|
||
if !self.config.smtp_username.is_empty() {
|
||
let credentials = Credentials::new(
|
||
self.config.smtp_username.clone(),
|
||
self.config.smtp_password.clone(),
|
||
);
|
||
transport_builder = transport_builder.credentials(credentials);
|
||
}
|
||
|
||
Ok(transport_builder.build())
|
||
}
|
||
|
||
/// 构建邮件消息
|
||
fn build_message(&self, email: &EmailMessage) -> Result<Message> {
|
||
// 解析发件人
|
||
let from_mailbox: Mailbox = format!("{} <{}>", self.config.smtp_from_name, self.config.smtp_from)
|
||
.parse()
|
||
.map_err(|e| anyhow!("Invalid from address: {}", e))?;
|
||
|
||
// 构建消息
|
||
let mut message_builder = Message::builder().from(from_mailbox);
|
||
|
||
// 添加收件人
|
||
for to_addr in &email.to {
|
||
let to_mailbox: Mailbox = to_addr
|
||
.parse()
|
||
.map_err(|e| anyhow!("Invalid to address '{}': {}", to_addr, e))?;
|
||
message_builder = message_builder.to(to_mailbox);
|
||
}
|
||
|
||
// 添加抄送
|
||
if let Some(cc_addrs) = &email.cc {
|
||
for cc_addr in cc_addrs {
|
||
let cc_mailbox: Mailbox = cc_addr
|
||
.parse()
|
||
.map_err(|e| anyhow!("Invalid cc address '{}': {}", cc_addr, e))?;
|
||
message_builder = message_builder.cc(cc_mailbox);
|
||
}
|
||
}
|
||
|
||
// 添加密送
|
||
if let Some(bcc_addrs) = &email.bcc {
|
||
for bcc_addr in bcc_addrs {
|
||
let bcc_mailbox: Mailbox = bcc_addr
|
||
.parse()
|
||
.map_err(|e| anyhow!("Invalid bcc address '{}': {}", bcc_addr, e))?;
|
||
message_builder = message_builder.bcc(bcc_mailbox);
|
||
}
|
||
}
|
||
|
||
// 添加回复地址
|
||
if let Some(reply_to) = &email.reply_to {
|
||
let reply_mailbox: Mailbox = reply_to
|
||
.parse()
|
||
.map_err(|e| anyhow!("Invalid reply-to address '{}': {}", reply_to, e))?;
|
||
message_builder = message_builder.reply_to(reply_mailbox);
|
||
}
|
||
|
||
// 设置主题和内容
|
||
message_builder = message_builder.subject(&email.subject);
|
||
|
||
let message = match email.content_type {
|
||
EmailContentType::Html => message_builder
|
||
.header(ContentType::TEXT_HTML)
|
||
.body(email.body.clone()),
|
||
EmailContentType::Text => message_builder
|
||
.header(ContentType::TEXT_PLAIN)
|
||
.body(email.body.clone()),
|
||
}
|
||
.map_err(|e| anyhow!("Failed to build message: {}", e))?;
|
||
|
||
Ok(message)
|
||
}
|
||
|
||
/// 发送邮件
|
||
pub async fn send_email(&self, email: EmailMessage) -> Result<EmailResult> {
|
||
// 验证配置
|
||
if let Err(e) = self.validate_config() {
|
||
error!("Email configuration validation failed: {}", e);
|
||
return Ok(EmailResult {
|
||
success: false,
|
||
message_id: None,
|
||
error: Some(e.to_string()),
|
||
});
|
||
}
|
||
|
||
// 构建传输器
|
||
let transport = match self.build_transport() {
|
||
Ok(transport) => transport,
|
||
Err(e) => {
|
||
error!("Failed to build SMTP transport: {}", e);
|
||
return Ok(EmailResult {
|
||
success: false,
|
||
message_id: None,
|
||
error: Some(e.to_string()),
|
||
});
|
||
}
|
||
};
|
||
|
||
// 构建消息
|
||
let message = match self.build_message(&email) {
|
||
Ok(message) => message,
|
||
Err(e) => {
|
||
error!("Failed to build email message: {}", e);
|
||
return Ok(EmailResult {
|
||
success: false,
|
||
message_id: None,
|
||
error: Some(e.to_string()),
|
||
});
|
||
}
|
||
};
|
||
|
||
// 发送邮件
|
||
info!(
|
||
"Sending email to {:?} with subject: '{}'",
|
||
email.to, email.subject
|
||
);
|
||
|
||
match transport.send(&message) {
|
||
Ok(response) => {
|
||
info!("Email sent successfully: {:?}", response);
|
||
Ok(EmailResult {
|
||
success: true,
|
||
message_id: Some(format!("{:?}", response)),
|
||
error: None,
|
||
})
|
||
}
|
||
Err(e) => {
|
||
error!("Failed to send email: {}", e);
|
||
Ok(EmailResult {
|
||
success: false,
|
||
message_id: None,
|
||
error: Some(e.to_string()),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 测试邮件连接
|
||
pub async fn test_connection(&self) -> Result<bool> {
|
||
// 验证配置
|
||
self.validate_config()?;
|
||
|
||
// 构建传输器
|
||
let transport = self.build_transport()?;
|
||
|
||
// 测试连接
|
||
match transport.test_connection() {
|
||
Ok(true) => {
|
||
info!("SMTP connection test successful");
|
||
Ok(true)
|
||
}
|
||
Ok(false) => {
|
||
warn!("SMTP connection test failed");
|
||
Ok(false)
|
||
}
|
||
Err(e) => {
|
||
error!("SMTP connection test error: {}", e);
|
||
Err(anyhow!("Connection test failed: {}", e))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 发送文本邮件
|
||
pub async fn send_text_email(
|
||
&self,
|
||
to: Vec<String>,
|
||
subject: String,
|
||
body: String,
|
||
) -> Result<EmailResult> {
|
||
let email = EmailMessage::new(to, subject, body)
|
||
.with_content_type(EmailContentType::Text);
|
||
self.send_email(email).await
|
||
}
|
||
|
||
/// 发送 HTML 邮件
|
||
pub async fn send_html_email(
|
||
&self,
|
||
to: Vec<String>,
|
||
subject: String,
|
||
body: String,
|
||
) -> Result<EmailResult> {
|
||
let email = EmailMessage::new(to, subject, body)
|
||
.with_content_type(EmailContentType::Html);
|
||
self.send_email(email).await
|
||
}
|
||
|
||
/// 发送验证邮件
|
||
pub async fn send_verification_email(
|
||
&self,
|
||
to: String,
|
||
username: String,
|
||
verification_code: String,
|
||
) -> Result<EmailResult> {
|
||
let subject = "请验证您的邮箱地址".to_string();
|
||
let body = self.build_verification_email_body(&username, &verification_code);
|
||
|
||
self.send_html_email(vec![to], subject, body).await
|
||
}
|
||
|
||
/// 发送密码重置邮件
|
||
pub async fn send_password_reset_email(
|
||
&self,
|
||
to: String,
|
||
username: String,
|
||
reset_token: String,
|
||
) -> Result<EmailResult> {
|
||
let subject = "重置您的密码".to_string();
|
||
let body = self.build_password_reset_email_body(&username, &reset_token);
|
||
|
||
self.send_html_email(vec![to], subject, body).await
|
||
}
|
||
|
||
/// 发送欢迎邮件
|
||
pub async fn send_welcome_email(
|
||
&self,
|
||
to: String,
|
||
username: String,
|
||
) -> Result<EmailResult> {
|
||
let subject = "欢迎加入 MAPP!".to_string();
|
||
let body = self.build_welcome_email_body(&username);
|
||
|
||
self.send_html_email(vec![to], subject, body).await
|
||
}
|
||
|
||
/// 构建验证邮件内容
|
||
fn build_verification_email_body(&self, username: &str, verification_code: &str) -> String {
|
||
let template = r#"<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>邮箱验证</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||
.header { background-color: #3B82F6; color: white; padding: 20px; text-align: center; }
|
||
.content { padding: 20px; background-color: #f9f9f9; }
|
||
.code { background-color: #e3f2fd; padding: 15px; text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 3px; margin: 20px 0; }
|
||
.footer { padding: 20px; text-align: center; color: #666; font-size: 12px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>验证您的邮箱地址</h1>
|
||
</div>
|
||
<div class="content">
|
||
<p>亲爱的 <strong>{{username}}</strong>,</p>
|
||
<p>感谢您注册 MAPP 账户!为了完成注册过程,请使用以下验证码验证您的邮箱地址:</p>
|
||
<div class="code">{{verification_code}}</div>
|
||
<p>此验证码在 15 分钟内有效。如果您没有注册 MAPP 账户,请忽略此邮件。</p>
|
||
<p>如有任何问题,请联系我们的客服团队。</p>
|
||
</div>
|
||
<div class="footer">
|
||
<p>此邮件由 MAPP 系统自动发送,请勿直接回复。</p>
|
||
<p>© 2024 MAPP. All rights reserved.</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>"#;
|
||
|
||
template
|
||
.replace("{{username}}", username)
|
||
.replace("{{verification_code}}", verification_code)
|
||
}
|
||
|
||
/// 构建密码重置邮件内容
|
||
fn build_password_reset_email_body(&self, username: &str, reset_token: &str) -> String {
|
||
let template = r#"<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>密码重置</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||
.header { background-color: #EF4444; color: white; padding: 20px; text-align: center; }
|
||
.content { padding: 20px; background-color: #f9f9f9; }
|
||
.token { background-color: #fef3c7; padding: 15px; text-align: center; font-size: 16px; font-weight: bold; margin: 20px 0; word-break: break-all; }
|
||
.footer { padding: 20px; text-align: center; color: #666; font-size: 12px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>重置您的密码</h1>
|
||
</div>
|
||
<div class="content">
|
||
<p>亲爱的 <strong>{{username}}</strong>,</p>
|
||
<p>我们收到了您的密码重置请求。请使用以下重置令牌来重置您的密码:</p>
|
||
<div class="token">{{reset_token}}</div>
|
||
<p>此重置令牌在 30 分钟内有效。如果您没有请求重置密码,请忽略此邮件,您的密码将保持不变。</p>
|
||
<p><strong>注意:</strong>为了您的账户安全,请不要与他人分享此重置令牌。</p>
|
||
</div>
|
||
<div class="footer">
|
||
<p>此邮件由 MAPP 系统自动发送,请勿直接回复。</p>
|
||
<p>© 2024 MAPP. All rights reserved.</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>"#;
|
||
|
||
template
|
||
.replace("{{username}}", username)
|
||
.replace("{{reset_token}}", reset_token)
|
||
}
|
||
|
||
/// 构建欢迎邮件内容
|
||
fn build_welcome_email_body(&self, username: &str) -> String {
|
||
let template = r#"<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>欢迎加入 MAPP</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||
.header { background-color: #10B981; color: white; padding: 20px; text-align: center; }
|
||
.content { padding: 20px; background-color: #f9f9f9; }
|
||
.features { background-color: white; padding: 20px; margin: 20px 0; border-left: 4px solid #10B981; }
|
||
.footer { padding: 20px; text-align: center; color: #666; font-size: 12px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🎉 欢迎加入 MAPP!</h1>
|
||
</div>
|
||
<div class="content">
|
||
<p>亲爱的 <strong>{{username}}</strong>,</p>
|
||
<p>恭喜您成功注册 MAPP 账户!我们很高兴您加入我们的社区。</p>
|
||
|
||
<div class="features">
|
||
<h3>您现在可以:</h3>
|
||
<ul>
|
||
<li>📊 创建和管理数据可视化</li>
|
||
<li>🗺️ 访问地图服务和功能</li>
|
||
<li>📝 发布和管理博客内容</li>
|
||
<li>⚙️ 自定义您的账户设置</li>
|
||
<li>🔒 享受安全的权限管理</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<p>如果您在使用过程中遇到任何问题,请随时联系我们的支持团队。我们很乐意为您提供帮助!</p>
|
||
<p>再次欢迎您,祝您使用愉快!</p>
|
||
</div>
|
||
<div class="footer">
|
||
<p>此邮件由 MAPP 系统自动发送,请勿直接回复。</p>
|
||
<p>© 2024 MAPP. All rights reserved.</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>"#;
|
||
|
||
template.replace("{{username}}", username)
|
||
}
|
||
}
|
||
|
||
/// 邮件模板类型
|
||
#[derive(Debug, Clone)]
|
||
pub enum EmailTemplate {
|
||
Verification { username: String, code: String },
|
||
PasswordReset { username: String, token: String },
|
||
Welcome { username: String },
|
||
Custom { subject: String, body: String },
|
||
}
|
||
|
||
impl EmailTemplate {
|
||
/// 渲染邮件模板
|
||
pub fn render(&self, email_service: &EmailService) -> (String, String) {
|
||
match self {
|
||
EmailTemplate::Verification { username, code } => (
|
||
"请验证您的邮箱地址".to_string(),
|
||
email_service.build_verification_email_body(username, code),
|
||
),
|
||
EmailTemplate::PasswordReset { username, token } => (
|
||
"重置您的密码".to_string(),
|
||
email_service.build_password_reset_email_body(username, token),
|
||
),
|
||
EmailTemplate::Welcome { username } => (
|
||
"欢迎加入 MAPP!".to_string(),
|
||
email_service.build_welcome_email_body(username),
|
||
),
|
||
EmailTemplate::Custom { subject, body } => (subject.clone(), body.clone()),
|
||
}
|
||
}
|
||
} |