From e04984192c409040775cab7d4e1baf663fa13c84 Mon Sep 17 00:00:00 2001 From: tsuki Date: Tue, 19 Aug 2025 11:34:49 +0800 Subject: [PATCH] sync email service --- src/services/email_service.rs | 567 ++++++++++++++++++++++++++++++++++ src/services/mod.rs | 1 + 2 files changed, 568 insertions(+) create mode 100644 src/services/email_service.rs diff --git a/src/services/email_service.rs b/src/services/email_service.rs new file mode 100644 index 0000000..5f917ad --- /dev/null +++ b/src/services/email_service.rs @@ -0,0 +1,567 @@ +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, + pub cc: Option>, + pub bcc: Option>, + pub subject: String, + pub body: String, + pub content_type: EmailContentType, + pub reply_to: Option, +} + +impl EmailMessage { + /// 创建新的邮件消息 + pub fn new(to: Vec, 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) -> Self { + self.cc = Some(cc); + self + } + + /// 设置密送 + pub fn with_bcc(mut self, bcc: Vec) -> 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, + pub error: Option, +} + +/// 邮件服务 +#[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 { + 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 { + // 解析发件人 + 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 { + // 验证配置 + 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 { + // 验证配置 + 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, + subject: String, + body: String, + ) -> Result { + 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, + subject: String, + body: String, + ) -> Result { + 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 { + 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 { + 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 { + 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#" + + + + 邮箱验证 + + + +
+
+

验证您的邮箱地址

+
+
+

亲爱的 {{username}}

+

感谢您注册 MAPP 账户!为了完成注册过程,请使用以下验证码验证您的邮箱地址:

+
{{verification_code}}
+

此验证码在 15 分钟内有效。如果您没有注册 MAPP 账户,请忽略此邮件。

+

如有任何问题,请联系我们的客服团队。

+
+ +
+ +"#; + + 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#" + + + + 密码重置 + + + +
+
+

重置您的密码

+
+
+

亲爱的 {{username}}

+

我们收到了您的密码重置请求。请使用以下重置令牌来重置您的密码:

+
{{reset_token}}
+

此重置令牌在 30 分钟内有效。如果您没有请求重置密码,请忽略此邮件,您的密码将保持不变。

+

注意:为了您的账户安全,请不要与他人分享此重置令牌。

+
+ +
+ +"#; + + template + .replace("{{username}}", username) + .replace("{{reset_token}}", reset_token) + } + + /// 构建欢迎邮件内容 + fn build_welcome_email_body(&self, username: &str) -> String { + let template = r#" + + + + 欢迎加入 MAPP + + + +
+
+

🎉 欢迎加入 MAPP!

+
+
+

亲爱的 {{username}}

+

恭喜您成功注册 MAPP 账户!我们很高兴您加入我们的社区。

+ +
+

您现在可以:

+
    +
  • 📊 创建和管理数据可视化
  • +
  • 🗺️ 访问地图服务和功能
  • +
  • 📝 发布和管理博客内容
  • +
  • ⚙️ 自定义您的账户设置
  • +
  • 🔒 享受安全的权限管理
  • +
+
+ +

如果您在使用过程中遇到任何问题,请随时联系我们的支持团队。我们很乐意为您提供帮助!

+

再次欢迎您,祝您使用愉快!

+
+ +
+ +"#; + + 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()), + } + } +} \ No newline at end of file diff --git a/src/services/mod.rs b/src/services/mod.rs index 0140c51..3790e57 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -2,6 +2,7 @@ pub mod blog_service; pub mod casbin_service; pub mod config_manager; pub mod config_service; +pub mod email_service; pub mod invite_code_service; pub mod mosaic_service; pub mod query_builder;