Procházet zdrojové kódy

Rust邮件发送服务First Commit

刘清 před 2 týdny
revize
319cef4db9
12 změnil soubory, kde provedl 1232 přidání a 0 odebrání
  1. 21
    0
      .gitignore
  2. 13
    0
      Cargo.toml
  3. 157
    0
      README.md
  4. 47
    0
      src/config.rs
  5. 21
    0
      src/error.rs
  6. 201
    0
      src/mailer.rs
  7. 150
    0
      src/main.rs
  8. 119
    0
      src/models.rs
  9. 97
    0
      src/templates/mod.rs
  10. 194
    0
      src/templates/registry.rs
  11. 120
    0
      src/templates/welcome.html
  12. 92
    0
      src/utils.rs

+ 21
- 0
.gitignore Zobrazit soubor

@@ -0,0 +1,21 @@
1
+# 编译输出
2
+/target/
3
+**/*.rs.bk
4
+
5
+# 环境文件
6
+.env
7
+.env.example
8
+Cargo.lock
9
+
10
+# 日志文件
11
+*.log
12
+
13
+# 编辑器文件
14
+.vscode/
15
+.idea/
16
+*.swp
17
+*.swo
18
+
19
+# 系统文件
20
+.DS_Store
21
+Thumbs.db

+ 13
- 0
Cargo.toml Zobrazit soubor

@@ -0,0 +1,13 @@
1
+[package]
2
+name = "mail-sender"
3
+version = "0.1.0"
4
+edition = "2024"
5
+
6
+[dependencies]
7
+lettre = { version = "0.11", features = ["smtp-transport", "builder", "rustls-tls"] }
8
+dotenv = "0.15"
9
+serde = { version = "1.0", features = ["derive"] }
10
+thiserror = "1.0"
11
+log = "0.4"
12
+env_logger = "0.10"
13
+chrono = "0.4"  # 时间处理

+ 157
- 0
README.md Zobrazit soubor

@@ -0,0 +1,157 @@
1
+# Rust邮件发送服务
2
+
3
+基于Rust语言开发的异步邮件发送服务,支持SMTP协议。
4
+
5
+## 功能特性
6
+
7
+- ✅ 异步发送邮件
8
+- ✅ 支持纯文本和HTML格式
9
+- ✅ 支持抄送、密送、回复地址
10
+- ✅ 支持邮件优先级设置
11
+- ✅ 连接池和连接复用
12
+- ✅ 自动重试机制
13
+- ✅ 完整的错误处理
14
+- ✅ 详细的日志记录
15
+- ✅ 健康检查
16
+- ✅ 统计信息
17
+
18
+## 快速开始
19
+
20
+### 1. 克隆项目
21
+
22
+```bash
23
+git clone <repository-url>
24
+cd mail-sender
25
+```
26
+
27
+## 项目结构
28
+
29
+```bash
30
+
31
+mail-sender/
32
+├── Cargo.toml                # 项目配置和依赖
33
+├── .env                      # 环境变量(不要提交到git)
34
+├── .env.example              # 环境变量示例
35
+├── .gitignore                # Git忽略文件
36
+├── README.md                 # 项目说明
37
+└── src/
38
+    ├── main.rs              # 主程序入口
39
+    ├── config.rs            # 配置管理
40
+    ├── mailer.rs            # 邮件服务核心
41
+    ├── models.rs            # 数据模型
42
+    ├── error.rs             # 错误处理
43
+    ├── utils.rs             # 工具函数
44
+    └── templates/           # 模板系统
45
+        ├── mod.rs              # 模板模块入口
46
+        ├── registry.rs         # 模板注册和管理
47
+        └── welcome.html        # 示例模板文件
48
+
49
+```
50
+
51
+## 如何使用
52
+
53
+### 1. 初始化项目
54
+
55
+```bash
56
+cargo new --bin mail-sender
57
+cd mail-sender
58
+
59
+```
60
+
61
+### 2. 创建目录结构
62
+
63
+```bash
64
+mkdir -p src
65
+touch src/{config.rs,email_service.rs,error.rs,models.rs,utils.rs}
66
+
67
+```
68
+
69
+### 3. 安装依赖
70
+
71
+```bash
72
+# 将上述Cargo.toml内容复制到项目中
73
+cargo add lettre --features smtp-transport,builder,rustls-tls,async-std1
74
+cargo add async-std --features attributes
75
+cargo add dotenv config serde thiserror anyhow
76
+cargo add log env_logger chrono uuid
77
+
78
+```
79
+
80
+### 4. 配置环境变量
81
+
82
+```bash
83
+cp .env.example .env
84
+# 编辑.env文件,填入你的配置
85
+
86
+```
87
+
88
+### 5. 运行项目
89
+
90
+```bash
91
+cargo run
92
+
93
+```
94
+
95
+### 6. 构建发布版本
96
+
97
+```bash
98
+cargo build --release
99
+
100
+```
101
+
102
+## 代码说明
103
+
104
+### 核心组件
105
+
106
+1. EmailService: 邮件发送服务的核心类,负责管理SMTP连接、发送邮件、重试逻辑等。
107
+
108
+2. EmailConfig: 配置管理类,支持从环境变量加载配置,包含完整的SMTP服务器配置。
109
+
110
+3. EmailRequest: 邮件请求数据结构,包含收件人、主题、正文、附件等信息。
111
+
112
+4. EmailResult: 发送结果数据结构,包含发送状态、错误信息等。
113
+
114
+### 异步设计
115
+
116
+- 使用async-std作为异步运行时
117
+
118
+- lettre库支持异步SMTP传输
119
+
120
+- 所有发送操作都是非阻塞的
121
+
122
+### 错误处理
123
+
124
+- 使用thiserror定义详细的错误类型
125
+
126
+- 为外部库的错误实现了自动转换
127
+
128
+- 所有可能失败的操作都返回Result<T, EmailError>
129
+
130
+### 功能总结
131
+
132
+- 重试机制: 自动重试失败的发送操作
133
+
134
+- 连接池: 重用SMTP连接,提高性能
135
+
136
+- 健康检查: 定期检查SMTP连接状态
137
+
138
+- 统计信息: 记录发送统计,便于监控
139
+
140
+- 多种格式: 支持纯文本和HTML邮件
141
+
142
+- 完整头部: 支持抄送、密送、回复地址等
143
+
144
+
145
+## 🚀 下一步扩展建议
146
+
147
+你现在已经有了坚实的基础,可以扩展:
148
+
149
+1. 邮件模板:预定义HTML模板,动态填充内容
150
+
151
+2. 附件支持:发送带附件的邮件
152
+
153
+3. 邮件队列:异步批量发送
154
+
155
+4. 发送统计:记录发送成功/失败率
156
+
157
+5. API接口:提供HTTP API供其他服务调用

+ 47
- 0
src/config.rs Zobrazit soubor

@@ -0,0 +1,47 @@
1
+use serde::Deserialize;
2
+use std::env;
3
+
4
+#[derive(Debug, Deserialize, Clone)]
5
+pub struct SmtpConfig {
6
+    pub host: String,
7
+    pub port: u16,
8
+    pub username: String,
9
+    pub password: String,
10
+    pub security: String,  // "starttls", "tls", "none"
11
+    pub from_email: String,
12
+    pub from_name: String,
13
+    pub timeout_seconds: u64,
14
+    pub max_retries: u32,
15
+}
16
+
17
+impl SmtpConfig {
18
+    pub fn from_env() -> Result<Self, String> {
19
+        dotenv::dotenv().ok();
20
+        
21
+        Ok(SmtpConfig {
22
+            host: env::var("SMTP_HOST").unwrap_or_else(|_| "afanai.top".to_string()),
23
+            port: env::var("SMTP_PORT")
24
+                .unwrap_or_else(|_| "587".to_string())
25
+                .parse()
26
+                .map_err(|e| format!("无效的端口: {}", e))?,
27
+            username: env::var("SMTP_USERNAME")
28
+                .unwrap_or_else(|_| "caoyuheng@afanai.top".to_string()),
29
+            password: env::var("SMTP_PASSWORD")
30
+                .unwrap_or_else(|_| "111111".to_string()),
31
+            security: env::var("SMTP_SECURITY")
32
+                .unwrap_or_else(|_| "starttls".to_string()),
33
+            from_email: env::var("FROM_EMAIL")
34
+                .unwrap_or_else(|_| "caoyuheng@afanai.top".to_string()),
35
+            from_name: env::var("FROM_NAME")
36
+                .unwrap_or_else(|_| "邮件服务".to_string()),
37
+            timeout_seconds: env::var("SMTP_TIMEOUT_SECONDS")
38
+                .unwrap_or_else(|_| "30".to_string())
39
+                .parse()
40
+                .unwrap_or(30),
41
+            max_retries: env::var("MAX_RETRIES")
42
+                .unwrap_or_else(|_| "3".to_string())
43
+                .parse()
44
+                .unwrap_or(3),
45
+        })
46
+    }
47
+}

+ 21
- 0
src/error.rs Zobrazit soubor

@@ -0,0 +1,21 @@
1
+use thiserror::Error;
2
+
3
+#[derive(Debug, Error)]
4
+pub enum MailError {
5
+    #[error("配置错误: {0}")]
6
+    Config(String),
7
+    
8
+    #[error("SMTP连接错误: {0}")]
9
+    Connection(String),
10
+    
11
+    #[error("邮件构建错误: {0}")]
12
+    Build(String),
13
+    
14
+    #[error("发送错误: {0}")]
15
+    Send(String),
16
+    
17
+    #[error("TLS错误: {0}")]
18
+    Tls(String),
19
+}
20
+
21
+pub type Result<T> = std::result::Result<T, MailError>;

+ 201
- 0
src/mailer.rs Zobrazit soubor

@@ -0,0 +1,201 @@
1
+use crate::config::SmtpConfig;
2
+use crate::error::{MailError, Result};
3
+use crate::models::{ContentType, EmailRequest, EmailResult};
4
+use crate::templates::{EmailTemplate, TemplateRegistry};
5
+use lettre::{
6
+    message::{header::ContentType as LettreContentType, Mailbox},
7
+    transport::smtp::{
8
+        authentication::Credentials,
9
+        client::{Tls, TlsParameters},
10
+    },
11
+    Message, SmtpTransport, Transport,
12
+};
13
+
14
+pub struct Mailer {
15
+    config: SmtpConfig,
16
+    transport: SmtpTransport,
17
+}
18
+
19
+impl Mailer {
20
+    pub fn new(config: SmtpConfig) -> Result<Self> {
21
+        // 创建基础传输器
22
+        let mut builder = SmtpTransport::relay(&config.host)
23
+            .map_err(|e| MailError::Connection(format!("无法连接到服务器: {}", e)))?
24
+            .port(config.port)
25
+            .credentials(Credentials::new(
26
+                config.username.clone(),
27
+                config.password.clone(),
28
+            ));
29
+        
30
+        // 配置TLS
31
+        match config.security.to_lowercase().as_str() {
32
+            "starttls" | "tls" => {
33
+                let tls_params = TlsParameters::builder(config.host.clone())
34
+                    .dangerous_accept_invalid_certs(true)
35
+                    .dangerous_accept_invalid_hostnames(true)
36
+                    .build()
37
+                    .map_err(|e| MailError::Tls(format!("TLS配置失败: {}", e)))?;
38
+                
39
+                builder = builder.tls(Tls::Required(tls_params));
40
+            }
41
+            "none" => {
42
+                // 不启用TLS
43
+            }
44
+            _ => {
45
+                return Err(MailError::Config(format!(
46
+                    "未知的安全模式: {}",
47
+                    config.security
48
+                )));
49
+            }
50
+        }
51
+        
52
+        let transport = builder.build();
53
+        
54
+        Ok(Self { config, transport })
55
+    }
56
+    
57
+    /// 发送邮件(使用 EmailRequest 结构)
58
+    pub fn send(&self, request: &EmailRequest) -> Result<EmailResult> {
59
+        // 验证收件人邮箱
60
+        if !crate::utils::validate_email(&request.to) {
61
+            return Err(MailError::Build(format!(
62
+                "无效的收件人邮箱: {}",
63
+                request.to
64
+            )));
65
+        }
66
+        
67
+        // 构建发件人邮箱
68
+        let from_mailbox = Mailbox::new(
69
+            Some(self.config.from_name.clone()),
70
+            self.config.from_email
71
+                .parse()
72
+                .map_err(|e| MailError::Build(format!("发件人地址无效: {}", e)))?,
73
+        );
74
+        
75
+        // 构建收件人邮箱
76
+        let to_mailbox = Mailbox::new(
77
+            request.to_name.clone(),
78
+            request.to
79
+                .parse()
80
+                .map_err(|e| MailError::Build(format!("收件人地址无效: {}", e)))?,
81
+        );
82
+        
83
+        // 开始构建邮件
84
+        let mut builder = Message::builder()
85
+            .from(from_mailbox)
86
+            .to(to_mailbox)
87
+            .subject(&request.subject);
88
+        
89
+        // 设置内容类型
90
+        let email = match request.content_type {
91
+            ContentType::Text => {
92
+                builder
93
+                    .header(LettreContentType::TEXT_PLAIN)
94
+                    .body(request.body.clone())
95
+            }
96
+            ContentType::Html => {
97
+                builder
98
+                    .header(LettreContentType::TEXT_HTML)
99
+                    .body(request.body.clone())
100
+            }
101
+        }
102
+        .map_err(|e| MailError::Build(format!("构建邮件失败: {}", e)))?;
103
+        
104
+        // 发送邮件(带重试)
105
+        let mut last_error = String::new();
106
+        for attempt in 1..=self.config.max_retries {
107
+            match self.transport.send(&email) {
108
+                Ok(response) => {
109
+                    // 获取消息ID
110
+                    let message_id = format!("{:?}", response);
111
+                    
112
+                    return Ok(EmailResult::success(
113
+                        &request.to,
114
+                        Some(&message_id),
115
+                    ));
116
+                }
117
+                Err(e) => {
118
+                    let error_str = e.to_string();
119
+                    last_error = error_str.clone();
120
+                    log::warn!("发送失败 (尝试{}): {}", attempt, error_str);
121
+                    
122
+                    if attempt < self.config.max_retries {
123
+                        std::thread::sleep(std::time::Duration::from_secs(1));
124
+                    }
125
+                }
126
+            }
127
+        }
128
+        
129
+        Ok(EmailResult::failure(&request.to, &last_error))
130
+    }
131
+    
132
+    /// 便捷方法:发送纯文本邮件
133
+    pub fn send_text(
134
+        &self,
135
+        to: &str,
136
+        subject: &str,
137
+        body: &str,
138
+    ) -> Result<EmailResult> {
139
+        let request = EmailRequest::simple(to, subject, body);
140
+        self.send(&request)
141
+    }
142
+    
143
+    /// 便捷方法:发送HTML邮件
144
+    pub fn send_html(
145
+        &self,
146
+        to: &str,
147
+        subject: &str,
148
+        html_body: &str,
149
+    ) -> Result<EmailResult> {
150
+        let request = EmailRequest::html(to, subject, html_body);
151
+        self.send(&request)
152
+    }
153
+    
154
+    pub fn test_connection(&self) -> Result<bool> {
155
+        self.transport
156
+            .test_connection()
157
+            .map_err(|e| MailError::Connection(format!("连接测试失败: {}", e)))
158
+    }
159
+
160
+    /// 使用模板发送邮件
161
+    pub fn send_with_template(
162
+        &self,
163
+        template: &EmailTemplate,
164
+        to: &str,
165
+        to_name: Option<&str>,
166
+        subject: &str,
167
+        variables: std::collections::HashMap<String, String>,
168
+    ) -> Result<EmailResult> {
169
+        // 渲染模板
170
+        let html_body = template.render(&variables)?;
171
+        
172
+        // 创建邮件请求
173
+        let request = crate::models::EmailRequest {
174
+            to: to.to_string(),
175
+            to_name: to_name.map(|s| s.to_string()),
176
+            subject: subject.to_string(),
177
+            body: html_body,
178
+            content_type: crate::models::ContentType::Html,
179
+            ..Default::default()
180
+        };
181
+        
182
+        // 发送邮件
183
+        self.send(&request)
184
+    }
185
+    
186
+    /// 从模板注册器发送邮件
187
+    pub fn send_from_registry(
188
+        &self,
189
+        registry: &TemplateRegistry,
190
+        template_name: &str,
191
+        to: &str,
192
+        to_name: Option<&str>,
193
+        subject: &str,
194
+        variables: std::collections::HashMap<String, String>,
195
+    ) -> Result<EmailResult> {
196
+        match registry.get(template_name) {
197
+            Some(template) => self.send_with_template(template, to, to_name, subject, variables),
198
+            None => Err(MailError::Build(format!("模板不存在: {}", template_name))),
199
+        }
200
+    }
201
+}

+ 150
- 0
src/main.rs Zobrazit soubor

@@ -0,0 +1,150 @@
1
+// 声明所有模块
2
+mod config;
3
+mod error;
4
+mod mailer;
5
+mod models;
6
+mod templates;  // 模板模块
7
+mod utils;
8
+
9
+// 导入需要的类型
10
+use crate::config::SmtpConfig;
11
+use crate::mailer::Mailer;
12
+use crate::templates::{builtin, TemplateRegistry};
13
+use crate::utils::{current_timestamp};
14
+
15
+// 标准库导入
16
+use std::collections::HashMap;
17
+
18
+fn main() -> Result<(), Box<dyn std::error::Error>> {
19
+    // 初始化日志
20
+    env_logger::init();
21
+    
22
+    println!("🚀 Rust邮件服务启动...");
23
+    println!("版本: 模板系统测试版");
24
+    
25
+    // 1. 加载配置
26
+    let config = SmtpConfig::from_env()
27
+        .map_err(|e| format!("配置加载失败: {}", e))?;
28
+    
29
+    println!("📧 配置信息:");
30
+    println!("  服务器: {}:{}", config.host, config.port);
31
+    println!("  发件人: {} <{}>", config.from_name, config.from_email);
32
+    
33
+    // 2. 创建邮件发送器
34
+    let mailer = Mailer::new(config)
35
+        .map_err(|e| format!("创建邮件发送器失败: {}", e))?;
36
+    
37
+    println!("✅ 邮件发送器初始化完成");
38
+    
39
+    // 3. 测试连接
40
+    println!("🔗 测试SMTP连接...");
41
+    match mailer.test_connection() {
42
+        Ok(true) => println!("✅ 连接测试成功"),
43
+        Ok(false) => println!("⚠️  连接测试返回false"),
44
+        Err(e) => {
45
+            println!("❌ 连接测试失败: {}", e);
46
+            return Ok(());
47
+        }
48
+    }
49
+    
50
+    // 4. 创建模板注册器并注册内置模板
51
+    println!("\n📋 初始化模板系统...");
52
+    let mut registry = TemplateRegistry::new();
53
+    
54
+    // 注册内置模板
55
+    registry.register(builtin::welcome());
56
+    registry.register(builtin::password_reset());
57
+    registry.register(builtin::notification());
58
+    
59
+    println!("✅ 已注册模板: {}", registry.list_templates().join(", "));
60
+    
61
+    // 5. 测试1:使用内置欢迎模板
62
+    println!("\n🎯 测试1:内置欢迎模板");
63
+    
64
+    let mut vars = HashMap::new();
65
+    vars.insert("name".to_string(), "张三".to_string());
66
+    vars.insert("site_name".to_string(), "阿凡AI".to_string());
67
+    vars.insert("code".to_string(), "123456".to_string());
68
+    vars.insert("expiry".to_string(), "30分钟".to_string());
69
+    
70
+    match mailer.send_from_registry(
71
+        &registry,
72
+        "welcome_simple",
73
+        "aaa@afanai.top",
74
+        Some("测试用户"),
75
+        "欢迎加入阿凡AI!",
76
+        vars,
77
+    ) {
78
+        Ok(result) => {
79
+            if result.success {
80
+                println!("✅ 模板邮件发送成功");
81
+            } else {
82
+                println!("❌ 模板邮件发送失败: {:?}", result.error);
83
+            }
84
+        }
85
+        Err(e) => println!("❌ 模板错误: {}", e),
86
+    }
87
+    
88
+    // 6. 测试2:密码重置模板
89
+    println!("\n🎯 测试2:密码重置模板");
90
+    
91
+    let mut reset_vars = HashMap::new();
92
+    reset_vars.insert("username".to_string(), "caoyuheng".to_string());
93
+    reset_vars.insert("reset_link".to_string(), 
94
+        "https://afanai.top/reset/password?token=xyz789".to_string());
95
+    reset_vars.insert("expiry_time".to_string(), "1小时".to_string());
96
+    reset_vars.insert("ip_address".to_string(), "192.168.1.100".to_string());
97
+    
98
+    match mailer.send_from_registry(
99
+        &registry,
100
+        "password_reset",
101
+        "aaa@afanai.top",
102
+        Some("曹雨恒"),
103
+        "密码重置请求 - 阿凡AI",
104
+        reset_vars,
105
+    ) {
106
+        Ok(result) => {
107
+            if result.success {
108
+                println!("✅ 密码重置邮件发送成功");
109
+            } else {
110
+                println!("❌ 密码重置邮件发送失败: {:?}", result.error);
111
+            }
112
+        }
113
+        Err(e) => println!("❌ 密码重置模板错误: {}", e),
114
+    }
115
+    
116
+    // 7. 测试3:通知模板
117
+    println!("\n🎯 测试3:系统通知模板");
118
+    
119
+    let mut notify_vars = HashMap::new();
120
+    notify_vars.insert("title".to_string(), "系统维护通知".to_string());
121
+    notify_vars.insert("time".to_string(), current_timestamp());
122
+    notify_vars.insert("message".to_string(), 
123
+        "服务器将于今晚10点进行维护,预计耗时2小时。".to_string());
124
+    notify_vars.insert("action_url".to_string(), 
125
+        "https://status.afanai.top".to_string());
126
+    
127
+    match mailer.send_from_registry(
128
+        &registry,
129
+        "notification",
130
+        "aaa@afanai.top",
131
+        Some("系统管理员"),
132
+        "系统维护通知",
133
+        notify_vars,
134
+    ) {
135
+        Ok(result) => {
136
+            if result.success {
137
+                println!("✅ 通知邮件发送成功");
138
+            } else {
139
+                println!("❌ 通知邮件发送失败: {:?}", result.error);
140
+            }
141
+        }
142
+        Err(e) => println!("❌ 通知模板错误: {}", e),
143
+    }
144
+    
145
+    println!("\n🎉 模板系统测试完成!");
146
+    println!("📬 请检查 aaa@afanai.top 的收件箱");
147
+    println!("   应该收到3封不同模板的测试邮件");
148
+    
149
+    Ok(())
150
+}

+ 119
- 0
src/models.rs Zobrazit soubor

@@ -0,0 +1,119 @@
1
+use serde::{Deserialize, Serialize};
2
+
3
+/// 邮件内容类型
4
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
5
+pub enum ContentType {
6
+    /// 纯文本
7
+    Text,
8
+    /// HTML格式
9
+    Html,
10
+}
11
+
12
+/// 邮件优先级
13
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
14
+pub enum Priority {
15
+    Low,
16
+    Normal,
17
+    High,
18
+}
19
+
20
+/// 邮件请求
21
+#[derive(Debug, Clone, Serialize, Deserialize)]
22
+pub struct EmailRequest {
23
+    /// 收件人邮箱
24
+    pub to: String,
25
+    /// 收件人姓名(可选)
26
+    pub to_name: Option<String>,
27
+    /// 邮件主题
28
+    pub subject: String,
29
+    /// 邮件正文
30
+    pub body: String,
31
+    /// 内容类型
32
+    pub content_type: ContentType,
33
+    /// 优先级
34
+    pub priority: Option<Priority>,
35
+    /// 抄送列表(可选)
36
+    pub cc: Option<Vec<String>>,
37
+    /// 密送列表(可选)
38
+    pub bcc: Option<Vec<String>>,
39
+}
40
+
41
+impl Default for EmailRequest {
42
+    fn default() -> Self {
43
+        Self {
44
+            to: String::new(),
45
+            to_name: None,
46
+            subject: String::new(),
47
+            body: String::new(),
48
+            content_type: ContentType::Text,
49
+            priority: Some(Priority::Normal),
50
+            cc: None,
51
+            bcc: None,
52
+        }
53
+    }
54
+}
55
+
56
+impl EmailRequest {
57
+    /// 创建简单的文本邮件请求
58
+    pub fn simple(to: &str, subject: &str, body: &str) -> Self {
59
+        Self {
60
+            to: to.to_string(),
61
+            to_name: None,
62
+            subject: subject.to_string(),
63
+            body: body.to_string(),
64
+            content_type: ContentType::Text,
65
+            priority: Some(Priority::Normal),
66
+            cc: None,
67
+            bcc: None,
68
+        }
69
+    }
70
+    
71
+    /// 创建HTML邮件请求
72
+    pub fn html(to: &str, subject: &str, html_body: &str) -> Self {
73
+        Self {
74
+            to: to.to_string(),
75
+            to_name: None,
76
+            subject: subject.to_string(),
77
+            body: html_body.to_string(),
78
+            content_type: ContentType::Html,
79
+            priority: Some(Priority::Normal),
80
+            cc: None,
81
+            bcc: None,
82
+        }
83
+    }
84
+}
85
+
86
+/// 邮件发送结果
87
+#[derive(Debug, Clone, Serialize, Deserialize)]
88
+pub struct EmailResult {
89
+    /// 是否成功
90
+    pub success: bool,
91
+    /// 收件人
92
+    pub recipient: String,
93
+    /// 错误信息(如果有)
94
+    pub error: Option<String>,
95
+    /// 消息ID(SMTP服务器返回)
96
+    pub message_id: Option<String>,
97
+}
98
+
99
+impl EmailResult {
100
+    /// 创建成功结果
101
+    pub fn success(recipient: &str, message_id: Option<&str>) -> Self {
102
+        Self {
103
+            success: true,
104
+            recipient: recipient.to_string(),
105
+            error: None,
106
+            message_id: message_id.map(|s| s.to_string()),
107
+        }
108
+    }
109
+    
110
+    /// 创建失败结果
111
+    pub fn failure(recipient: &str, error: &str) -> Self {
112
+        Self {
113
+            success: false,
114
+            recipient: recipient.to_string(),
115
+            error: Some(error.to_string()),
116
+            message_id: None,
117
+        }
118
+    }
119
+}

+ 97
- 0
src/templates/mod.rs Zobrazit soubor

@@ -0,0 +1,97 @@
1
+// 声明子模块
2
+mod registry;
3
+
4
+// 公开导出
5
+pub use registry::{EmailTemplate, TemplateRegistry};
6
+
7
+// 内置模板模块
8
+pub mod builtin {
9
+    use super::EmailTemplate;
10
+    
11
+    /// 欢迎邮件模板
12
+    pub fn welcome() -> EmailTemplate {
13
+        let content = r#"
14
+<!DOCTYPE html>
15
+<html>
16
+<head>
17
+    <meta charset="UTF-8">
18
+    <style>
19
+        body { font-family: Arial; max-width: 600px; margin: 0 auto; padding: 20px; }
20
+        .header { background: #4CAF50; color: white; padding: 20px; text-align: center; }
21
+        .content { padding: 20px; background: #f9f9f9; }
22
+    </style>
23
+</head>
24
+<body>
25
+    <div class="header">
26
+        <h1>欢迎 {{name}}!</h1>
27
+    </div>
28
+    <div class="content">
29
+        <p>感谢您加入 {{site_name}}。</p>
30
+        <p>您的验证码是:<strong>{{code}}</strong></p>
31
+        <p>有效期:{{expiry}}</p>
32
+    </div>
33
+</body>
34
+</html>
35
+        "#.to_string();
36
+        
37
+        EmailTemplate::from_string("welcome_simple", &content, "简单的欢迎邮件模板")
38
+    }
39
+    
40
+    /// 密码重置模板
41
+    pub fn password_reset() -> EmailTemplate {
42
+        let content = r#"
43
+<!DOCTYPE html>
44
+<html>
45
+<head>
46
+    <meta charset="UTF-8">
47
+    <style>
48
+        body { font-family: Arial; max-width: 600px; margin: 0 auto; padding: 20px; }
49
+        .alert { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; }
50
+        .button { background: #dc3545; color: white; padding: 10px 20px; text-decoration: none; }
51
+    </style>
52
+</head>
53
+<body>
54
+    <div class="alert">
55
+        <h2>密码重置请求</h2>
56
+        <p>亲爱的 {{username}},</p>
57
+        <p>我们收到了您的密码重置请求。</p>
58
+        <p>请点击下方链接重置密码:</p>
59
+        <a href="{{reset_link}}" class="button">重置密码</a>
60
+        <p>如果您未请求重置密码,请忽略此邮件。</p>
61
+        <p>链接有效期:{{expiry_time}}</p>
62
+        <p>IP地址:{{ip_address}}</p>
63
+    </div>
64
+</body>
65
+</html>
66
+        "#.to_string();
67
+        
68
+        EmailTemplate::from_string("password_reset", &content, "密码重置邮件模板")
69
+    }
70
+    
71
+    /// 通知模板
72
+    pub fn notification() -> EmailTemplate {
73
+        let content = r#"
74
+<!DOCTYPE html>
75
+<html>
76
+<head>
77
+    <meta charset="UTF-8">
78
+    <style>
79
+        body { font-family: Arial; max-width: 600px; margin: 0 auto; padding: 20px; }
80
+        .info { background: #d1ecf1; border-left: 4px solid #17a2b8; padding: 15px; }
81
+    </style>
82
+</head>
83
+<body>
84
+    <div class="info">
85
+        <h2>{{title}}</h2>
86
+        <p><strong>时间:</strong>{{time}}</p>
87
+        <p><strong>内容:</strong>{{message}}</p>
88
+        {{#if action_url}}<p><a href="{{action_url}}">查看详情</a></p>{{/if}}
89
+        <p><small>此通知由系统自动生成</small></p>
90
+    </div>
91
+</body>
92
+</html>
93
+        "#.to_string();
94
+        
95
+        EmailTemplate::from_string("notification", &content, "系统通知模板")
96
+    }
97
+}

+ 194
- 0
src/templates/registry.rs Zobrazit soubor

@@ -0,0 +1,194 @@
1
+use std::collections::HashMap;
2
+use std::fs;
3
+use std::path::Path;
4
+
5
+use crate::error::{MailError, Result};
6
+
7
+/// 邮件模板
8
+#[derive(Debug, Clone)]
9
+pub struct EmailTemplate {
10
+    /// 模板名称
11
+    pub name: String,
12
+    /// 模板内容(HTML)
13
+    pub content: String,
14
+    /// 模板描述
15
+    pub description: String,
16
+    /// 需要的变量列表
17
+    pub required_vars: Vec<String>,
18
+}
19
+
20
+impl EmailTemplate {
21
+    /// 从文件加载模板
22
+    pub fn from_file(name: &str, file_path: &str, description: &str) -> Result<Self> {
23
+        let content = fs::read_to_string(file_path)
24
+            .map_err(|e| MailError::Config(format!("无法读取模板文件 {}: {}", file_path, e)))?;
25
+        
26
+        // 简单提取变量(以 {{ 开头 }} 结尾的标识符)
27
+        let required_vars = Self::extract_variables(&content);
28
+        
29
+        Ok(Self {
30
+            name: name.to_string(),
31
+            content,
32
+            description: description.to_string(),
33
+            required_vars,
34
+        })
35
+    }
36
+    
37
+    /// 从字符串创建模板
38
+    pub fn from_string(name: &str, content: &str, description: &str) -> Self {
39
+        let required_vars = Self::extract_variables(content);
40
+        
41
+        Self {
42
+            name: name.to_string(),
43
+            content: content.to_string(),
44
+            description: description.to_string(),
45
+            required_vars,
46
+        }
47
+    }
48
+    
49
+    /// 提取模板中的变量
50
+    fn extract_variables(content: &str) -> Vec<String> {
51
+        let mut vars = Vec::new();
52
+        let mut in_var = false;
53
+        let mut current_var = String::new();
54
+        
55
+        let chars: Vec<char> = content.chars().collect();
56
+        let mut i = 0;
57
+        
58
+        while i < chars.len() {
59
+            // 检查是否开始变量 {{ 
60
+            if i + 1 < chars.len() && chars[i] == '{' && chars[i + 1] == '{' {
61
+                in_var = true;
62
+                i += 2; // 跳过 {{
63
+                continue;
64
+            }
65
+            
66
+            // 检查是否结束变量 }}
67
+            if i + 1 < chars.len() && chars[i] == '}' && chars[i + 1] == '}' {
68
+                if in_var && !current_var.trim().is_empty() {
69
+                    vars.push(current_var.trim().to_string());
70
+                }
71
+                in_var = false;
72
+                current_var.clear();
73
+                i += 2; // 跳过 }}
74
+                continue;
75
+            }
76
+            
77
+            // 如果在变量内部,收集字符
78
+            if in_var {
79
+                current_var.push(chars[i]);
80
+            }
81
+            
82
+            i += 1;
83
+        }
84
+        
85
+        // 去重并排序
86
+        vars.sort();
87
+        vars.dedup();
88
+        vars
89
+    }
90
+    
91
+    /// 渲染模板(简单替换)
92
+    pub fn render(&self, variables: &HashMap<String, String>) -> Result<String> {
93
+        let mut rendered = self.content.clone();
94
+        
95
+        // 检查必需变量
96
+        for required_var in &self.required_vars {
97
+            if !variables.contains_key(required_var) {
98
+                return Err(MailError::Build(format!(
99
+                    "模板 '{}' 缺少必需变量: {}",
100
+                    self.name, required_var
101
+                )));
102
+            }
103
+        }
104
+        
105
+        // 替换变量
106
+        for (key, value) in variables {
107
+            let placeholder = format!("{{{{{}}}}}", key);
108
+            rendered = rendered.replace(&placeholder, value);
109
+        }
110
+        
111
+        Ok(rendered)
112
+    }
113
+    
114
+    /// 获取模板信息
115
+    pub fn info(&self) -> String {
116
+        format!(
117
+            "模板: {} (需要变量: {})",
118
+            self.name,
119
+            self.required_vars.join(", ")
120
+        )
121
+    }
122
+}
123
+
124
+/// 模板注册器
125
+#[derive(Debug, Default)]
126
+pub struct TemplateRegistry {
127
+    templates: HashMap<String, EmailTemplate>,
128
+}
129
+
130
+impl TemplateRegistry {
131
+    pub fn new() -> Self {
132
+        Self {
133
+            templates: HashMap::new(),
134
+        }
135
+    }
136
+    
137
+    /// 注册模板
138
+    pub fn register(&mut self, template: EmailTemplate) {
139
+        self.templates.insert(template.name.clone(), template);
140
+    }
141
+    
142
+    /// 从目录加载所有模板
143
+    pub fn load_from_dir(&mut self, dir_path: &str) -> Result<()> {
144
+        let path = Path::new(dir_path);
145
+        
146
+        if !path.exists() {
147
+            return Err(MailError::Config(format!("模板目录不存在: {}", dir_path)));
148
+        }
149
+        
150
+        for entry in fs::read_dir(path).map_err(|e| {
151
+            MailError::Config(format!("无法读取模板目录 {}: {}", dir_path, e))
152
+        })? {
153
+            let entry = entry.map_err(|e| {
154
+                MailError::Config(format!("读取目录项失败: {}", e))
155
+            })?;
156
+            
157
+            let file_path = entry.path();
158
+            if file_path.extension().map_or(false, |ext| ext == "html") {
159
+                let name = file_path.file_stem()
160
+                    .and_then(|n| n.to_str())
161
+                    .unwrap_or("unknown")
162
+                    .to_string();
163
+                
164
+                let template = EmailTemplate::from_file(
165
+                    &name,
166
+                    file_path.to_str().unwrap(),
167
+                    &format!("从文件加载: {}", file_path.display()),
168
+                )?;
169
+                
170
+                self.register(template);
171
+            }
172
+        }
173
+        
174
+        Ok(())
175
+    }
176
+    
177
+    /// 获取模板
178
+    pub fn get(&self, name: &str) -> Option<&EmailTemplate> {
179
+        self.templates.get(name)
180
+    }
181
+    
182
+    /// 获取所有模板名称
183
+    pub fn list_templates(&self) -> Vec<String> {
184
+        self.templates.keys().cloned().collect()
185
+    }
186
+    
187
+    /// 渲染模板
188
+    pub fn render(&self, name: &str, variables: &HashMap<String, String>) -> Result<String> {
189
+        match self.get(name) {
190
+            Some(template) => template.render(variables),
191
+            None => Err(MailError::Build(format!("模板不存在: {}", name))),
192
+        }
193
+    }
194
+}

+ 120
- 0
src/templates/welcome.html Zobrazit soubor

@@ -0,0 +1,120 @@
1
+<!DOCTYPE html>
2
+<html>
3
+<head>
4
+    <meta charset="UTF-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+    <title>{{subject}}</title>
7
+    <style>
8
+        body {
9
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
+            line-height: 1.6;
11
+            color: #333;
12
+            max-width: 600px;
13
+            margin: 0 auto;
14
+            padding: 20px;
15
+            background-color: #f9f9f9;
16
+        }
17
+        .header {
18
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
+            color: white;
20
+            padding: 30px;
21
+            border-radius: 10px 10px 0 0;
22
+            text-align: center;
23
+        }
24
+        .content {
25
+            background: white;
26
+            padding: 30px;
27
+            border-radius: 0 0 10px 10px;
28
+            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
29
+        }
30
+        .greeting {
31
+            font-size: 24px;
32
+            margin-bottom: 20px;
33
+            color: #2c3e50;
34
+        }
35
+        .message {
36
+            margin-bottom: 25px;
37
+            font-size: 16px;
38
+        }
39
+        .highlight {
40
+            background-color: #fff3cd;
41
+            padding: 15px;
42
+            border-left: 4px solid #ffc107;
43
+            margin: 20px 0;
44
+            border-radius: 4px;
45
+        }
46
+        .button {
47
+            display: inline-block;
48
+            background: #4CAF50;
49
+            color: white;
50
+            padding: 12px 30px;
51
+            text-decoration: none;
52
+            border-radius: 5px;
53
+            font-weight: bold;
54
+            margin: 10px 0;
55
+        }
56
+        .footer {
57
+            margin-top: 30px;
58
+            padding-top: 20px;
59
+            border-top: 1px solid #eee;
60
+            font-size: 12px;
61
+            color: #777;
62
+            text-align: center;
63
+        }
64
+        .user-info {
65
+            background: #f8f9fa;
66
+            padding: 15px;
67
+            border-radius: 5px;
68
+            margin: 15px 0;
69
+        }
70
+    </style>
71
+</head>
72
+<body>
73
+    <div class="header">
74
+        <h1>🎉 欢迎加入 {{site_name}}!</h1>
75
+        <p>我们很高兴您的到来</p>
76
+    </div>
77
+    
78
+    <div class="content">
79
+        <div class="greeting">
80
+            亲爱的 {{name}},您好!
81
+        </div>
82
+        
83
+        <div class="message">
84
+            感谢您注册 {{site_name}}。您的账户已经成功创建,现在可以开始使用我们的服务了。
85
+        </div>
86
+        
87
+        <div class="user-info">
88
+            <p><strong>账户信息:</strong></p>
89
+            <p>用户名:{{username}}</p>
90
+            <p>注册时间:{{signup_date}}</p>
91
+            <p>用户ID:{{user_id}}</p>
92
+        </div>
93
+        
94
+        <div class="highlight">
95
+            <p><strong>下一步操作:</strong></p>
96
+            <p>请点击下方按钮验证您的邮箱地址:</p>
97
+            <a href="{{verification_link}}" class="button">✅ 验证邮箱</a>
98
+            <p style="font-size: 14px; margin-top: 10px;">
99
+                如果按钮无法点击,请复制以下链接:<br>
100
+                <code>{{verification_link}}</code>
101
+            </p>
102
+        </div>
103
+        
104
+        <div class="message">
105
+            <p><strong>开始探索:</strong></p>
106
+            <ul>
107
+                <li>📚 查看 <a href="{{docs_link}}">使用文档</a></li>
108
+                <li>🎬 观看 <a href="{{tutorial_link}}">入门教程</a></li>
109
+                <li>💬 加入 <a href="{{community_link}}">社区讨论</a></li>
110
+            </ul>
111
+        </div>
112
+        
113
+        <div class="footer">
114
+            <p>此邮件由 {{site_name}} 系统自动发送,请勿直接回复。</p>
115
+            <p>如果您未注册 {{site_name}},请忽略此邮件。</p>
116
+            <p>发送时间:{{current_time}} | 邮件ID:{{email_id}}</p>
117
+        </div>
118
+    </div>
119
+</body>
120
+</html>

+ 92
- 0
src/utils.rs Zobrazit soubor

@@ -0,0 +1,92 @@
1
+use std::env;
2
+use chrono::{Local, DateTime};
3
+
4
+/// 验证邮箱地址格式(简单版本)
5
+pub fn validate_email(email: &str) -> bool {
6
+    email.contains('@') && email.contains('.') && email.len() > 5
7
+}
8
+
9
+/// 清理邮箱地址(去除空格,转为小写)
10
+pub fn sanitize_email(email: &str) -> String {
11
+    email.trim().to_lowercase()
12
+}
13
+
14
+/// 获取当前时间戳字符串
15
+pub fn current_timestamp() -> String {
16
+    Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
17
+}
18
+
19
+/// 获取格式化时间
20
+pub fn format_datetime(dt: &DateTime<Local>) -> String {
21
+    dt.format("%Y-%m-%d %H:%M:%S").to_string()
22
+}
23
+
24
+/// 生成测试邮件内容
25
+pub fn generate_test_email(to: &str) -> crate::models::EmailRequest {
26
+    use crate::models::{EmailRequest, ContentType};
27
+    
28
+    let body = format!(
29
+        "这是一封测试邮件。\n\n\
30
+         发送时间: {}\n\
31
+         发件人: Rust邮件服务\n\n\
32
+         恭喜!你的邮件服务正在正常工作。",
33
+        current_timestamp()
34
+    );
35
+    
36
+    EmailRequest {
37
+        to: to.to_string(),
38
+        subject: format!("测试邮件 - {}", current_timestamp()),
39
+        body,
40
+        content_type: ContentType::Text,
41
+        ..Default::default()
42
+    }
43
+}
44
+
45
+/// 生成HTML测试邮件
46
+pub fn generate_html_test_email(to: &str) -> crate::models::EmailRequest {
47
+    use crate::models::{EmailRequest, ContentType};
48
+    
49
+    let html_body = format!(
50
+        r#"<!DOCTYPE html>
51
+<html>
52
+<head>
53
+    <meta charset="UTF-8">
54
+    <title>测试邮件</title>
55
+    <style>
56
+        body {{ font-family: Arial, sans-serif; line-height: 1.6; }}
57
+        .header {{ background-color: #4CAF50; color: white; padding: 20px; }}
58
+        .content {{ padding: 20px; }}
59
+        .footer {{ background-color: #f1f1f1; padding: 10px; font-size: 12px; }}
60
+    </style>
61
+</head>
62
+<body>
63
+    <div class="header">
64
+        <h1>HTML测试邮件</h1>
65
+    </div>
66
+    <div class="content">
67
+        <p>这是一封<strong>HTML格式</strong>的测试邮件。</p>
68
+        <p>发送时间: <em>{}</em></p>
69
+        <p>状态: <span style="color: green;">✅ 正常</span></p>
70
+    </div>
71
+    <div class="footer">
72
+        <p>由Rust邮件服务发送</p>
73
+    </div>
74
+</body>
75
+</html>"#,
76
+        current_timestamp()
77
+    );
78
+    
79
+    EmailRequest {
80
+        to: to.to_string(),
81
+        subject: format!("HTML测试邮件 - {}", current_timestamp()),
82
+        body: html_body,
83
+        content_type: ContentType::Html,
84
+        ..Default::default()
85
+    }
86
+}
87
+
88
+/// 从环境变量读取收件人,如果没有则使用默认值
89
+pub fn get_default_recipient() -> String {
90
+    env::var("TEST_RECIPIENT")
91
+        .unwrap_or_else(|_| "aaa@afanai.top".to_string())
92
+}