Skip to main content

sz_rust_core/
mail.rs

1//! Mail 模块 — 对齐 PHP `think\facade\Mail`
2//!
3//! 本模块实现邮件抽象层,对齐 PHP `think\facade\Mail` 的核心 API。
4//!
5//! ## PHP 对齐
6//!
7//! ### 核心 API 映射
8//!
9//! | PHP 方法 | Rust 方法 | 说明 |
10//! |---------|-----------|------|
11//! | `Mail::to($address)` | [`MailMessage::to`] | 添加收件人 |
12//! | `Mail::cc($address)` | [`MailMessage::cc`] | 添加抄送人 |
13//! | `Mail::bcc($address)` | [`MailMessage::bcc`] | 添加密送人 |
14//! | `Mail::from($address)` | [`MailMessage::from`] | 设置发件人 |
15//! | `Mail::subject($subject)` | [`MailMessage::subject`] | 设置主题 |
16//! | `Mail::html($content)` | [`MailMessage::html`] | 设置 HTML 内容 |
17//! | `Mail::text($content)` | [`MailMessage::text`] | 设置纯文本内容 |
18//! | `Mail::attach($file)` | [`MailMessage::attach`] | 添加附件 |
19//! | `Mail::send()` | [`Mailer::send`] | 发送邮件 |
20//!
21//! ### PHP 行为对齐
22//!
23//! - **Builder 模式**:PHP `Mail::to()->subject()->html()->send()` 链式调用。
24//!   Rust 通过 [`MailMessage`] builder 实现相同链式 API。
25//! - **多收件人**:PHP `to()` 支持数组或逗号分隔字符串。Rust 通过多次调用 `to()` 累加。
26//! - **双内容格式**:PHP 支持 `html()` 和 `text()` 两种内容。Rust 同样支持两种。
27//!
28//! ## 架构说明
29//!
30//! - **Mailer trait 抽象**:对齐 PHP `think\mail\Mailer` 接口,业务方实现具体发送逻辑
31//! - **MemoryMailer**:内置内存实现,将邮件暂存到 Vec,用于测试和开发环境
32//! - **无外部依赖**:不依赖 `lettre` crate,保持框架核心包依赖最小化
33//! - **SMTP 实现延后**:生产环境 SMTP 发送由业务方通过 feature gate 或独立包实现
34
35use parking_lot::Mutex;
36use std::sync::Arc;
37use thiserror::Error;
38
39// ============================================================================
40// 错误类型
41// ============================================================================
42
43/// Mail 错误
44#[derive(Debug, Error)]
45pub enum MailError {
46    /// 缺少必填字段(收件人、主题、内容等)
47    #[error("邮件字段缺失: {0}")]
48    MissingField(String),
49    /// 邮件发送失败
50    #[error("邮件发送失败: {0}")]
51    SendFailed(String),
52    /// 附件读取失败
53    #[error("附件读取失败: {path} — {source}")]
54    AttachmentRead {
55        /// 附件路径
56        path: String,
57        /// 底层 IO 错误
58        #[source]
59        source: std::io::Error,
60    },
61}
62
63// ============================================================================
64// 邮件地址
65// ============================================================================
66
67/// 邮件地址(含可选显示名)
68///
69/// 对齐 PHP `think\mail\Address` 的地址表示。
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct MailAddress {
72    /// 邮箱地址
73    pub email: String,
74    /// 可选显示名(如 `"张三" <zhangsan@example.com>` 中的 `张三`)
75    pub name: Option<String>,
76}
77
78impl MailAddress {
79    /// 创建仅含邮箱的地址
80    ///
81    /// # 参数
82    ///
83    /// - `email`: 邮箱地址
84    pub fn new(email: impl Into<String>) -> Self {
85        Self {
86            email: email.into(),
87            name: None,
88        }
89    }
90
91    /// 创建含显示名的地址
92    ///
93    /// # 参数
94    ///
95    /// - `email`: 邮箱地址
96    /// - `name`: 显示名
97    pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
98        Self {
99            email: email.into(),
100            name: Some(name.into()),
101        }
102    }
103
104    /// 格式化为 RFC 5322 地址字符串
105    ///
106    /// - 无显示名:`user@example.com`
107    /// - 有显示名:`"张三" <user@example.com>`
108    pub fn to_rfc5322_string(&self) -> String {
109        match &self.name {
110            Some(name) => format!("\"{}\" <{}>", name, self.email),
111            None => self.email.clone(),
112        }
113    }
114}
115
116impl From<&str> for MailAddress {
117    fn from(email: &str) -> Self {
118        Self::new(email)
119    }
120}
121
122impl From<String> for MailAddress {
123    fn from(email: String) -> Self {
124        Self::new(email)
125    }
126}
127
128// ============================================================================
129// 附件
130// ============================================================================
131
132/// 邮件附件
133///
134/// 对齐 PHP `think\mail\Attachment`。
135#[derive(Debug, Clone)]
136pub struct MailAttachment {
137    /// 附件文件名
138    pub filename: String,
139    /// 附件内容(字节)
140    pub content: Vec<u8>,
141    /// MIME 类型(如 `application/pdf`)
142    pub mime_type: String,
143}
144
145impl MailAttachment {
146    /// 从字节数据创建附件
147    ///
148    /// # 参数
149    ///
150    /// - `filename`: 文件名
151    /// - `content`: 文件内容字节
152    /// - `mime_type`: MIME 类型
153    pub fn new(
154        filename: impl Into<String>,
155        content: Vec<u8>,
156        mime_type: impl Into<String>,
157    ) -> Self {
158        Self {
159            filename: filename.into(),
160            content,
161            mime_type: mime_type.into(),
162        }
163    }
164
165    /// 从文件路径创建附件
166    ///
167    /// # 参数
168    ///
169    /// - `path`: 文件路径
170    /// - `filename`: 指定文件名(`None` 时使用路径中的文件名)
171    /// - `mime_type`: MIME 类型
172    ///
173    /// # 返回
174    ///
175    /// 成功返回 [`MailAttachment`],失败返回 [`MailError::AttachmentRead`]。
176    pub fn from_file(
177        path: impl AsRef<std::path::Path>,
178        filename: Option<&str>,
179        mime_type: impl Into<String>,
180    ) -> Result<Self, MailError> {
181        let path_ref = path.as_ref();
182        let content = std::fs::read(path_ref).map_err(|e| MailError::AttachmentRead {
183            path: path_ref.display().to_string(),
184            source: e,
185        })?;
186
187        let filename = match filename {
188            Some(name) => name.to_string(),
189            None => path_ref
190                .file_name()
191                .and_then(|n| n.to_str())
192                .unwrap_or("attachment")
193                .to_string(),
194        };
195
196        Ok(Self::new(filename, content, mime_type))
197    }
198}
199
200// ============================================================================
201// 邮件消息(Builder 模式)
202// ============================================================================
203
204/// 邮件消息 — 对齐 PHP `think\mail\Message`
205///
206/// 使用 Builder 模式构建邮件内容,通过 [`Mailer::send`] 发送。
207///
208/// # PHP 对齐
209///
210/// ```php
211/// // PHP think\facade\Mail
212/// Mail::to('user@example.com')
213///     ->subject('Hello')
214///     ->html('<h1>Welcome</h1>')
215///     ->send();
216/// ```
217///
218/// # Rust 用法
219///
220/// ```rust,ignore
221/// use sz_rust_core::mail::{MailMessage, MemoryMailer};
222///
223/// let msg = MailMessage::new()
224///     .to("user@example.com")
225///     .subject("Hello")
226///     .html("<h1>Welcome</h1>");
227///
228/// let mailer = MemoryMailer::new();
229/// mailer.send(msg).unwrap();
230/// ```
231#[derive(Debug, Clone, Default)]
232pub struct MailMessage {
233    /// 发件人
234    pub from: Option<MailAddress>,
235    /// 回复地址
236    pub reply_to: Option<MailAddress>,
237    /// 收件人列表
238    pub to: Vec<MailAddress>,
239    /// 抄送人列表
240    pub cc: Vec<MailAddress>,
241    /// 密送人列表
242    pub bcc: Vec<MailAddress>,
243    /// 邮件主题
244    pub subject: String,
245    /// HTML 内容
246    pub html_body: Option<String>,
247    /// 纯文本内容
248    pub text_body: Option<String>,
249    /// 附件列表
250    pub attachments: Vec<MailAttachment>,
251}
252
253impl MailMessage {
254    /// 创建空邮件消息
255    pub fn new() -> Self {
256        Self::default()
257    }
258
259    /// 设置发件人
260    ///
261    /// # 参数
262    ///
263    /// - `address`: 发件人邮箱(实现 `Into<MailAddress>`,可传 `&str` 或 `MailAddress`)
264    pub fn from(mut self, address: impl Into<MailAddress>) -> Self {
265        self.from = Some(address.into());
266        self
267    }
268
269    /// 设置回复地址
270    pub fn reply_to(mut self, address: impl Into<MailAddress>) -> Self {
271        self.reply_to = Some(address.into());
272        self
273    }
274
275    /// 添加收件人(可多次调用累加)
276    pub fn to(mut self, address: impl Into<MailAddress>) -> Self {
277        self.to.push(address.into());
278        self
279    }
280
281    /// 添加抄送人(可多次调用累加)
282    pub fn cc(mut self, address: impl Into<MailAddress>) -> Self {
283        self.cc.push(address.into());
284        self
285    }
286
287    /// 添加密送人(可多次调用累加)
288    pub fn bcc(mut self, address: impl Into<MailAddress>) -> Self {
289        self.bcc.push(address.into());
290        self
291    }
292
293    /// 设置邮件主题
294    pub fn subject(mut self, subject: impl Into<String>) -> Self {
295        self.subject = subject.into();
296        self
297    }
298
299    /// 设置 HTML 内容
300    pub fn html(mut self, content: impl Into<String>) -> Self {
301        self.html_body = Some(content.into());
302        self
303    }
304
305    /// 设置纯文本内容
306    pub fn text(mut self, content: impl Into<String>) -> Self {
307        self.text_body = Some(content.into());
308        self
309    }
310
311    /// 添加附件
312    pub fn attach(mut self, attachment: MailAttachment) -> Self {
313        self.attachments.push(attachment);
314        self
315    }
316
317    /// 校验邮件必要字段
318    ///
319    /// 必须满足:至少一个收件人 + 非空主题 + 至少一种内容(HTML 或纯文本)
320    pub fn validate(&self) -> Result<(), MailError> {
321        if self.to.is_empty() {
322            return Err(MailError::MissingField("收件人 (to) 不能为空".to_string()));
323        }
324        if self.subject.is_empty() {
325            return Err(MailError::MissingField(
326                "主题 (subject) 不能为空".to_string(),
327            ));
328        }
329        if self.html_body.is_none() && self.text_body.is_none() {
330            return Err(MailError::MissingField(
331                "内容 (html 或 text) 不能同时为空".to_string(),
332            ));
333        }
334        Ok(())
335    }
336}
337
338// ============================================================================
339// Mailer trait
340// ============================================================================
341
342/// 邮件发送器 trait — 对齐 PHP `think\mail\Mailer`
343///
344/// 抽象邮件发送行为,业务方实现具体发送逻辑(SMTP / API / 日志等)。
345///
346/// # PHP 对齐
347///
348/// ```php
349/// // PHP think\mail\Mailer 接口
350/// interface Mailer {
351///     public function send(Message $message): bool;
352///     public function sendRaw(string $to, string $subject, string $body): bool;
353/// }
354/// ```
355pub trait Mailer: Send + Sync {
356    /// 发送邮件
357    ///
358    /// # 参数
359    ///
360    /// - `message`: 邮件消息
361    ///
362    /// # 返回
363    ///
364    /// 成功返回 `Ok(())`,失败返回 [`MailError`]。
365    fn send(&self, message: MailMessage) -> Result<(), MailError>;
366}
367
368// ============================================================================
369// MemoryMailer(测试/开发用实现)
370// ============================================================================
371
372/// 内存邮件发送器 — 用于测试和开发环境
373///
374/// 不实际发送邮件,而是将邮件暂存到内部 Vec,供测试断言使用。
375///
376/// # 线程安全
377///
378/// 通过 `Arc<Mutex<Vec<MailMessage>>>` 保护,支持并发写入。
379#[derive(Debug, Clone, Default)]
380pub struct MemoryMailer {
381    /// 已"发送"的邮件列表
382    sent: Arc<Mutex<Vec<MailMessage>>>,
383}
384
385impl MemoryMailer {
386    /// 创建新的内存邮件发送器
387    pub fn new() -> Self {
388        Self::default()
389    }
390
391    /// 获取已发送邮件数量
392    pub fn count(&self) -> usize {
393        self.sent.lock().len()
394    }
395
396    /// 获取所有已发送邮件(快照)
397    pub fn all(&self) -> Vec<MailMessage> {
398        self.sent.lock().clone()
399    }
400
401    /// 获取最后发送的邮件
402    pub fn last(&self) -> Option<MailMessage> {
403        self.sent.lock().last().cloned()
404    }
405
406    /// 清空已发送邮件
407    pub fn clear(&self) {
408        self.sent.lock().clear();
409    }
410}
411
412impl Mailer for MemoryMailer {
413    fn send(&self, message: MailMessage) -> Result<(), MailError> {
414        // 校验必要字段
415        message.validate()?;
416
417        // 暂存到内存
418        self.sent.lock().push(message);
419        Ok(())
420    }
421}
422
423// ============================================================================
424// 单元测试
425// ============================================================================
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    /// 测试 MailAddress 基本创建
432    #[test]
433    fn test_mail_address_new() {
434        let addr = MailAddress::new("user@example.com");
435        assert_eq!(addr.email, "user@example.com");
436        assert_eq!(addr.name, None);
437        assert_eq!(addr.to_rfc5322_string(), "user@example.com");
438    }
439
440    /// 测试 MailAddress 含显示名
441    #[test]
442    fn test_mail_address_with_name() {
443        let addr = MailAddress::with_name("user@example.com", "张三");
444        assert_eq!(addr.email, "user@example.com");
445        assert_eq!(addr.name, Some("张三".to_string()));
446        assert_eq!(addr.to_rfc5322_string(), "\"张三\" <user@example.com>");
447    }
448
449    /// 测试 MailAddress From<&str> 转换
450    #[test]
451    fn test_mail_address_from_str() {
452        let addr: MailAddress = "test@example.com".into();
453        assert_eq!(addr.email, "test@example.com");
454        assert_eq!(addr.name, None);
455    }
456
457    /// 测试 MailMessage Builder 链式调用
458    #[test]
459    fn test_mail_message_builder_chain() {
460        let msg = MailMessage::new()
461            .from("sender@example.com")
462            .to("user1@example.com")
463            .to("user2@example.com")
464            .cc("cc@example.com")
465            .bcc("bcc@example.com")
466            .subject("Test Subject")
467            .html("<h1>Hello</h1>")
468            .text("Hello");
469
470        assert_eq!(msg.from.unwrap().email, "sender@example.com");
471        assert_eq!(msg.to.len(), 2);
472        assert_eq!(msg.to[0].email, "user1@example.com");
473        assert_eq!(msg.to[1].email, "user2@example.com");
474        assert_eq!(msg.cc.len(), 1);
475        assert_eq!(msg.bcc.len(), 1);
476        assert_eq!(msg.subject, "Test Subject");
477        assert_eq!(msg.html_body, Some("<h1>Hello</h1>".to_string()));
478        assert_eq!(msg.text_body, Some("Hello".to_string()));
479    }
480
481    /// 测试 MailMessage validate 成功
482    #[test]
483    fn test_validate_success() {
484        let msg = MailMessage::new()
485            .to("user@example.com")
486            .subject("Test")
487            .html("<p>Hello</p>");
488
489        assert!(msg.validate().is_ok());
490    }
491
492    /// 测试 MailMessage validate 缺少收件人
493    #[test]
494    fn test_validate_missing_to() {
495        let msg = MailMessage::new().subject("Test").html("<p>Hello</p>");
496
497        let result = msg.validate();
498        assert!(result.is_err());
499        match result {
500            Err(MailError::MissingField(msg)) => {
501                assert!(msg.contains("收件人"));
502            }
503            _ => panic!("期望 MissingField 错误"),
504        }
505    }
506
507    /// 测试 MailMessage validate 缺少主题
508    #[test]
509    fn test_validate_missing_subject() {
510        let msg = MailMessage::new()
511            .to("user@example.com")
512            .html("<p>Hello</p>");
513
514        let result = msg.validate();
515        assert!(result.is_err());
516        match result {
517            Err(MailError::MissingField(msg)) => {
518                assert!(msg.contains("主题"));
519            }
520            _ => panic!("期望 MissingField 错误"),
521        }
522    }
523
524    /// 测试 MailMessage validate 缺少内容
525    #[test]
526    fn test_validate_missing_content() {
527        let msg = MailMessage::new().to("user@example.com").subject("Test");
528
529        let result = msg.validate();
530        assert!(result.is_err());
531        match result {
532            Err(MailError::MissingField(msg)) => {
533                assert!(msg.contains("内容"));
534            }
535            _ => panic!("期望 MissingField 错误"),
536        }
537    }
538
539    /// 测试 MemoryMailer 发送邮件
540    #[test]
541    fn test_memory_mailer_send() {
542        let mailer = MemoryMailer::new();
543        let msg = MailMessage::new()
544            .to("user@example.com")
545            .subject("Test")
546            .html("<p>Hello</p>");
547
548        let result = mailer.send(msg);
549        assert!(result.is_ok());
550        assert_eq!(mailer.count(), 1);
551
552        let sent = mailer.last().unwrap();
553        assert_eq!(sent.subject, "Test");
554        assert_eq!(sent.to.len(), 1);
555        assert_eq!(sent.to[0].email, "user@example.com");
556    }
557
558    /// 测试 MemoryMailer 发送多封邮件
559    #[test]
560    fn test_memory_mailer_send_multiple() {
561        let mailer = MemoryMailer::new();
562
563        for i in 0..3 {
564            let msg = MailMessage::new()
565                .to(format!("user{}@example.com", i))
566                .subject(format!("Subject {}", i))
567                .text(format!("Body {}", i));
568            mailer.send(msg).unwrap();
569        }
570
571        assert_eq!(mailer.count(), 3);
572
573        let all = mailer.all();
574        assert_eq!(all[0].subject, "Subject 0");
575        assert_eq!(all[2].subject, "Subject 2");
576    }
577
578    /// 测试 MemoryMailer 发送无效邮件返回错误
579    #[test]
580    fn test_memory_mailer_send_invalid_errors() {
581        let mailer = MemoryMailer::new();
582        let msg = MailMessage::new().subject("No recipient").html("<p>Hi</p>");
583
584        let result = mailer.send(msg);
585        assert!(result.is_err());
586        assert_eq!(mailer.count(), 0);
587    }
588
589    /// 测试 MemoryMailer clear
590    #[test]
591    fn test_memory_mailer_clear() {
592        let mailer = MemoryMailer::new();
593        let msg = MailMessage::new()
594            .to("user@example.com")
595            .subject("Test")
596            .text("Hi");
597        mailer.send(msg).unwrap();
598        assert_eq!(mailer.count(), 1);
599
600        mailer.clear();
601        assert_eq!(mailer.count(), 0);
602        assert!(mailer.last().is_none());
603    }
604
605    /// 测试 MailAttachment 从字节创建
606    #[test]
607    fn test_attachment_from_bytes() {
608        let attachment = MailAttachment::new("doc.pdf", vec![1, 2, 3], "application/pdf");
609        assert_eq!(attachment.filename, "doc.pdf");
610        assert_eq!(attachment.content, vec![1, 2, 3]);
611        assert_eq!(attachment.mime_type, "application/pdf");
612    }
613
614    /// 测试 MailAttachment 从文件创建
615    #[test]
616    fn test_attachment_from_file() {
617        let temp_dir = std::env::temp_dir().join("sz_rust_mail_test");
618        let _ = std::fs::create_dir_all(&temp_dir);
619        let file_path = temp_dir.join("test.txt");
620        std::fs::write(&file_path, b"hello attachment").unwrap();
621
622        let attachment = MailAttachment::from_file(&file_path, None, "text/plain").unwrap();
623        assert_eq!(attachment.filename, "test.txt");
624        assert_eq!(attachment.content, b"hello attachment");
625        assert_eq!(attachment.mime_type, "text/plain");
626
627        let _ = std::fs::remove_dir_all(&temp_dir);
628    }
629
630    /// 测试 MailAttachment 从不存在文件创建返回错误
631    #[test]
632    fn test_attachment_from_nonexistent_file_errors() {
633        let result = MailAttachment::from_file("/nonexistent/file.txt", None, "text/plain");
634        assert!(result.is_err());
635        match result {
636            Err(MailError::AttachmentRead { .. }) => {}
637            _ => panic!("期望 AttachmentRead 错误"),
638        }
639    }
640
641    /// 测试邮件含附件发送
642    #[test]
643    fn test_send_mail_with_attachment() {
644        let mailer = MemoryMailer::new();
645        let attachment = MailAttachment::new("report.pdf", vec![1, 2, 3, 4], "application/pdf");
646
647        let msg = MailMessage::new()
648            .to("user@example.com")
649            .subject("Report")
650            .text("Please find the attached report.")
651            .attach(attachment);
652
653        mailer.send(msg).unwrap();
654        let sent = mailer.last().unwrap();
655        assert_eq!(sent.attachments.len(), 1);
656        assert_eq!(sent.attachments[0].filename, "report.pdf");
657    }
658
659    /// 测试 MailMessage 默认值
660    #[test]
661    fn test_mail_message_default() {
662        let msg = MailMessage::default();
663        assert!(msg.from.is_none());
664        assert!(msg.reply_to.is_none());
665        assert!(msg.to.is_empty());
666        assert!(msg.cc.is_empty());
667        assert!(msg.bcc.is_empty());
668        assert!(msg.subject.is_empty());
669        assert!(msg.html_body.is_none());
670        assert!(msg.text_body.is_none());
671        assert!(msg.attachments.is_empty());
672    }
673
674    /// 测试纯文本邮件(无 HTML)
675    #[test]
676    fn test_text_only_email() {
677        let mailer = MemoryMailer::new();
678        let msg = MailMessage::new()
679            .to("user@example.com")
680            .subject("Plain Text")
681            .text("This is plain text content.");
682
683        mailer.send(msg).unwrap();
684        let sent = mailer.last().unwrap();
685        assert!(sent.html_body.is_none());
686        assert_eq!(
687            sent.text_body,
688            Some("This is plain text content.".to_string())
689        );
690    }
691
692    /// 测试 HTML 邮件(无纯文本)
693    #[test]
694    fn test_html_only_email() {
695        let mailer = MemoryMailer::new();
696        let msg = MailMessage::new()
697            .to("user@example.com")
698            .subject("HTML Only")
699            .html("<h1>HTML Content</h1>");
700
701        mailer.send(msg).unwrap();
702        let sent = mailer.last().unwrap();
703        assert_eq!(sent.html_body, Some("<h1>HTML Content</h1>".to_string()));
704        assert!(sent.text_body.is_none());
705    }
706}