mmap/src/services/email_service.rs
tsuki e04984192c
Some checks are pending
Docker Build and Push / build (push) Waiting to run
sync email service
2025-08-19 11:34:49 +08:00

567 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>&copy; 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>&copy; 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>&copy; 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()),
}
}
}