sync email service
Some checks are pending
Docker Build and Push / build (push) Waiting to run

This commit is contained in:
tsuki 2025-08-19 11:34:49 +08:00
parent 58ea36e73c
commit e04984192c
2 changed files with 568 additions and 0 deletions

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

View File

@ -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;