This commit is contained in:
parent
58ea36e73c
commit
e04984192c
567
src/services/email_service.rs
Normal file
567
src/services/email_service.rs
Normal file
@ -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<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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user