Skip to main content

sz_rust_state_facade/
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 async 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 = tokio::fs::read(path_ref)
183            .await
184            .map_err(|e| MailError::AttachmentRead {
185                path: path_ref.display().to_string(),
186                source: e,
187            })?;
188
189        let filename = match filename {
190            Some(name) => name.to_string(),
191            None => path_ref
192                .file_name()
193                .and_then(|n| n.to_str())
194                .unwrap_or("attachment")
195                .to_string(),
196        };
197
198        Ok(Self::new(filename, content, mime_type))
199    }
200}
201
202// ============================================================================
203// 邮件消息(Builder 模式)
204// ============================================================================
205
206/// 邮件消息 — 对齐 PHP `think\mail\Message`
207///
208/// 使用 Builder 模式构建邮件内容,通过 [`Mailer::send`] 发送。
209///
210/// # PHP 对齐
211///
212/// ```php
213/// // PHP think\facade\Mail
214/// Mail::to('user@example.com')
215///     ->subject('Hello')
216///     ->html('<h1>Welcome</h1>')
217///     ->send();
218/// ```
219///
220/// # Rust 用法
221///
222/// ```rust,ignore
223/// use sz_rust_state_facade::mail::{MailMessage, MemoryMailer};
224///
225/// let msg = MailMessage::new()
226///     .to("user@example.com")
227///     .subject("Hello")
228///     .html("<h1>Welcome</h1>");
229///
230/// let mailer = MemoryMailer::new();
231/// mailer.send(msg).unwrap();
232/// ```
233#[derive(Debug, Clone, Default)]
234pub struct MailMessage {
235    /// 发件人
236    pub from: Option<MailAddress>,
237    /// 回复地址
238    pub reply_to: Option<MailAddress>,
239    /// 收件人列表
240    pub to: Vec<MailAddress>,
241    /// 抄送人列表
242    pub cc: Vec<MailAddress>,
243    /// 密送人列表
244    pub bcc: Vec<MailAddress>,
245    /// 邮件主题
246    pub subject: String,
247    /// HTML 内容
248    pub html_body: Option<String>,
249    /// 纯文本内容
250    pub text_body: Option<String>,
251    /// 附件列表
252    pub attachments: Vec<MailAttachment>,
253}
254
255impl MailMessage {
256    /// 创建空邮件消息
257    pub fn new() -> Self {
258        Self::default()
259    }
260
261    /// 设置发件人
262    ///
263    /// # 参数
264    ///
265    /// - `address`: 发件人邮箱(实现 `Into<MailAddress>`,可传 `&str` 或 `MailAddress`)
266    pub fn from(mut self, address: impl Into<MailAddress>) -> Self {
267        self.from = Some(address.into());
268        self
269    }
270
271    /// 设置回复地址
272    pub fn reply_to(mut self, address: impl Into<MailAddress>) -> Self {
273        self.reply_to = Some(address.into());
274        self
275    }
276
277    /// 添加收件人(可多次调用累加)
278    pub fn to(mut self, address: impl Into<MailAddress>) -> Self {
279        self.to.push(address.into());
280        self
281    }
282
283    /// 添加抄送人(可多次调用累加)
284    pub fn cc(mut self, address: impl Into<MailAddress>) -> Self {
285        self.cc.push(address.into());
286        self
287    }
288
289    /// 添加密送人(可多次调用累加)
290    pub fn bcc(mut self, address: impl Into<MailAddress>) -> Self {
291        self.bcc.push(address.into());
292        self
293    }
294
295    /// 设置邮件主题
296    pub fn subject(mut self, subject: impl Into<String>) -> Self {
297        self.subject = subject.into();
298        self
299    }
300
301    /// 设置 HTML 内容
302    pub fn html(mut self, content: impl Into<String>) -> Self {
303        self.html_body = Some(content.into());
304        self
305    }
306
307    /// 设置纯文本内容
308    pub fn text(mut self, content: impl Into<String>) -> Self {
309        self.text_body = Some(content.into());
310        self
311    }
312
313    /// 添加附件
314    pub fn attach(mut self, attachment: MailAttachment) -> Self {
315        self.attachments.push(attachment);
316        self
317    }
318
319    /// 校验邮件必要字段
320    ///
321    /// 必须满足:至少一个收件人 + 非空主题 + 至少一种内容(HTML 或纯文本)
322    pub fn validate(&self) -> Result<(), MailError> {
323        if self.to.is_empty() {
324            return Err(MailError::MissingField("收件人 (to) 不能为空".to_string()));
325        }
326        if self.subject.is_empty() {
327            return Err(MailError::MissingField(
328                "主题 (subject) 不能为空".to_string(),
329            ));
330        }
331        if self.html_body.is_none() && self.text_body.is_none() {
332            return Err(MailError::MissingField(
333                "内容 (html 或 text) 不能同时为空".to_string(),
334            ));
335        }
336        Ok(())
337    }
338}
339
340// ============================================================================
341// Mailer trait
342// ============================================================================
343
344/// 邮件发送器 trait — 对齐 PHP `think\mail\Mailer`
345///
346/// 抽象邮件发送行为,业务方实现具体发送逻辑(SMTP / API / 日志等)。
347///
348/// # PHP 对齐
349///
350/// ```php
351/// // PHP think\mail\Mailer 接口
352/// interface Mailer {
353///     public function send(Message $message): bool;
354///     public function sendRaw(string $to, string $subject, string $body): bool;
355/// }
356/// ```
357pub trait Mailer: Send + Sync {
358    /// 发送邮件
359    ///
360    /// # 参数
361    ///
362    /// - `message`: 邮件消息
363    ///
364    /// # 返回
365    ///
366    /// 成功返回 `Ok(())`,失败返回 [`MailError`]。
367    fn send(&self, message: MailMessage) -> Result<(), MailError>;
368}
369
370// ============================================================================
371// MemoryMailer(测试/开发用实现)
372// ============================================================================
373
374/// 内存邮件发送器 — 用于测试和开发环境
375///
376/// 不实际发送邮件,而是将邮件暂存到内部 Vec,供测试断言使用。
377///
378/// # 线程安全
379///
380/// 通过 `Arc<Mutex<Vec<MailMessage>>>` 保护,支持并发写入。
381#[derive(Debug, Clone, Default)]
382pub struct MemoryMailer {
383    /// 已"发送"的邮件列表
384    sent: Arc<Mutex<Vec<MailMessage>>>,
385}
386
387impl MemoryMailer {
388    /// 创建新的内存邮件发送器
389    pub fn new() -> Self {
390        Self::default()
391    }
392
393    /// 获取已发送邮件数量
394    pub fn count(&self) -> usize {
395        self.sent.lock().len()
396    }
397
398    /// 获取所有已发送邮件(快照)
399    pub fn all(&self) -> Vec<MailMessage> {
400        self.sent.lock().clone()
401    }
402
403    /// 获取最后发送的邮件
404    pub fn last(&self) -> Option<MailMessage> {
405        self.sent.lock().last().cloned()
406    }
407
408    /// 清空已发送邮件
409    pub fn clear(&self) {
410        self.sent.lock().clear();
411    }
412}
413
414impl Mailer for MemoryMailer {
415    fn send(&self, message: MailMessage) -> Result<(), MailError> {
416        // 校验必要字段
417        message.validate()?;
418
419        // 暂存到内存
420        self.sent.lock().push(message);
421        Ok(())
422    }
423}
424
425// ============================================================================
426// 单元测试
427// ============================================================================
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    /// 测试 MailAddress 基本创建
434    #[test]
435    fn test_mail_address_new() {
436        let addr = MailAddress::new("user@example.com");
437        assert_eq!(addr.email, "user@example.com");
438        assert_eq!(addr.name, None);
439        assert_eq!(addr.to_rfc5322_string(), "user@example.com");
440    }
441
442    /// 测试 MailAddress 含显示名
443    #[test]
444    fn test_mail_address_with_name() {
445        let addr = MailAddress::with_name("user@example.com", "张三");
446        assert_eq!(addr.email, "user@example.com");
447        assert_eq!(addr.name, Some("张三".to_string()));
448        assert_eq!(addr.to_rfc5322_string(), "\"张三\" <user@example.com>");
449    }
450
451    /// 测试 MailAddress From<&str> 转换
452    #[test]
453    fn test_mail_address_from_str() {
454        let addr: MailAddress = "test@example.com".into();
455        assert_eq!(addr.email, "test@example.com");
456        assert_eq!(addr.name, None);
457    }
458
459    /// 测试 MailMessage Builder 链式调用
460    #[test]
461    fn test_mail_message_builder_chain() {
462        let msg = MailMessage::new()
463            .from("sender@example.com")
464            .to("user1@example.com")
465            .to("user2@example.com")
466            .cc("cc@example.com")
467            .bcc("bcc@example.com")
468            .subject("Test Subject")
469            .html("<h1>Hello</h1>")
470            .text("Hello");
471
472        assert_eq!(msg.from.unwrap().email, "sender@example.com");
473        assert_eq!(msg.to.len(), 2);
474        assert_eq!(msg.to[0].email, "user1@example.com");
475        assert_eq!(msg.to[1].email, "user2@example.com");
476        assert_eq!(msg.cc.len(), 1);
477        assert_eq!(msg.bcc.len(), 1);
478        assert_eq!(msg.subject, "Test Subject");
479        assert_eq!(msg.html_body, Some("<h1>Hello</h1>".to_string()));
480        assert_eq!(msg.text_body, Some("Hello".to_string()));
481    }
482
483    /// 测试 MailMessage validate 成功
484    #[test]
485    fn test_validate_success() {
486        let msg = MailMessage::new()
487            .to("user@example.com")
488            .subject("Test")
489            .html("<p>Hello</p>");
490
491        assert!(msg.validate().is_ok());
492    }
493
494    /// 测试 MailMessage validate 缺少收件人
495    #[test]
496    fn test_validate_missing_to() {
497        let msg = MailMessage::new().subject("Test").html("<p>Hello</p>");
498
499        let result = msg.validate();
500        assert!(result.is_err());
501        match result {
502            Err(MailError::MissingField(msg)) => {
503                assert!(msg.contains("收件人"));
504            }
505            _ => panic!("期望 MissingField 错误"),
506        }
507    }
508
509    /// 测试 MailMessage validate 缺少主题
510    #[test]
511    fn test_validate_missing_subject() {
512        let msg = MailMessage::new()
513            .to("user@example.com")
514            .html("<p>Hello</p>");
515
516        let result = msg.validate();
517        assert!(result.is_err());
518        match result {
519            Err(MailError::MissingField(msg)) => {
520                assert!(msg.contains("主题"));
521            }
522            _ => panic!("期望 MissingField 错误"),
523        }
524    }
525
526    /// 测试 MailMessage validate 缺少内容
527    #[test]
528    fn test_validate_missing_content() {
529        let msg = MailMessage::new().to("user@example.com").subject("Test");
530
531        let result = msg.validate();
532        assert!(result.is_err());
533        match result {
534            Err(MailError::MissingField(msg)) => {
535                assert!(msg.contains("内容"));
536            }
537            _ => panic!("期望 MissingField 错误"),
538        }
539    }
540
541    /// 测试 MemoryMailer 发送邮件
542    #[test]
543    fn test_memory_mailer_send() {
544        let mailer = MemoryMailer::new();
545        let msg = MailMessage::new()
546            .to("user@example.com")
547            .subject("Test")
548            .html("<p>Hello</p>");
549
550        let result = mailer.send(msg);
551        assert!(result.is_ok());
552        assert_eq!(mailer.count(), 1);
553
554        let sent = mailer.last().unwrap();
555        assert_eq!(sent.subject, "Test");
556        assert_eq!(sent.to.len(), 1);
557        assert_eq!(sent.to[0].email, "user@example.com");
558    }
559
560    /// 测试 MemoryMailer 发送多封邮件
561    #[test]
562    fn test_memory_mailer_send_multiple() {
563        let mailer = MemoryMailer::new();
564
565        for i in 0..3 {
566            let msg = MailMessage::new()
567                .to(format!("user{}@example.com", i))
568                .subject(format!("Subject {}", i))
569                .text(format!("Body {}", i));
570            mailer.send(msg).unwrap();
571        }
572
573        assert_eq!(mailer.count(), 3);
574
575        let all = mailer.all();
576        assert_eq!(all[0].subject, "Subject 0");
577        assert_eq!(all[2].subject, "Subject 2");
578    }
579
580    /// 测试 MemoryMailer 发送无效邮件返回错误
581    #[test]
582    fn test_memory_mailer_send_invalid_errors() {
583        let mailer = MemoryMailer::new();
584        let msg = MailMessage::new().subject("No recipient").html("<p>Hi</p>");
585
586        let result = mailer.send(msg);
587        assert!(result.is_err());
588        assert_eq!(mailer.count(), 0);
589    }
590
591    /// 测试 MemoryMailer clear
592    #[test]
593    fn test_memory_mailer_clear() {
594        let mailer = MemoryMailer::new();
595        let msg = MailMessage::new()
596            .to("user@example.com")
597            .subject("Test")
598            .text("Hi");
599        mailer.send(msg).unwrap();
600        assert_eq!(mailer.count(), 1);
601
602        mailer.clear();
603        assert_eq!(mailer.count(), 0);
604        assert!(mailer.last().is_none());
605    }
606
607    /// 测试 MailAttachment 从字节创建
608    #[test]
609    fn test_attachment_from_bytes() {
610        let attachment = MailAttachment::new("doc.pdf", vec![1, 2, 3], "application/pdf");
611        assert_eq!(attachment.filename, "doc.pdf");
612        assert_eq!(attachment.content, vec![1, 2, 3]);
613        assert_eq!(attachment.mime_type, "application/pdf");
614    }
615
616    /// 测试 MailAttachment 从文件创建
617    #[tokio::test]
618    async fn test_attachment_from_file() {
619        let temp_dir = std::env::temp_dir().join("sz_rust_mail_test");
620        let _ = std::fs::create_dir_all(&temp_dir);
621        let file_path = temp_dir.join("test.txt");
622        std::fs::write(&file_path, b"hello attachment").unwrap();
623
624        let attachment = MailAttachment::from_file(&file_path, None, "text/plain")
625            .await
626            .unwrap();
627        assert_eq!(attachment.filename, "test.txt");
628        assert_eq!(attachment.content, b"hello attachment");
629        assert_eq!(attachment.mime_type, "text/plain");
630
631        let _ = std::fs::remove_dir_all(&temp_dir);
632    }
633
634    /// 测试 MailAttachment 从不存在文件创建返回错误
635    #[tokio::test]
636    async fn test_attachment_from_nonexistent_file_errors() {
637        let result = MailAttachment::from_file("/nonexistent/file.txt", None, "text/plain").await;
638        assert!(result.is_err());
639        match result {
640            Err(MailError::AttachmentRead { .. }) => {}
641            _ => panic!("期望 AttachmentRead 错误"),
642        }
643    }
644
645    /// 测试邮件含附件发送
646    #[test]
647    fn test_send_mail_with_attachment() {
648        let mailer = MemoryMailer::new();
649        let attachment = MailAttachment::new("report.pdf", vec![1, 2, 3, 4], "application/pdf");
650
651        let msg = MailMessage::new()
652            .to("user@example.com")
653            .subject("Report")
654            .text("Please find the attached report.")
655            .attach(attachment);
656
657        mailer.send(msg).unwrap();
658        let sent = mailer.last().unwrap();
659        assert_eq!(sent.attachments.len(), 1);
660        assert_eq!(sent.attachments[0].filename, "report.pdf");
661    }
662
663    /// 测试 MailMessage 默认值
664    #[test]
665    fn test_mail_message_default() {
666        let msg = MailMessage::default();
667        assert!(msg.from.is_none());
668        assert!(msg.reply_to.is_none());
669        assert!(msg.to.is_empty());
670        assert!(msg.cc.is_empty());
671        assert!(msg.bcc.is_empty());
672        assert!(msg.subject.is_empty());
673        assert!(msg.html_body.is_none());
674        assert!(msg.text_body.is_none());
675        assert!(msg.attachments.is_empty());
676    }
677
678    /// 测试纯文本邮件(无 HTML)
679    #[test]
680    fn test_text_only_email() {
681        let mailer = MemoryMailer::new();
682        let msg = MailMessage::new()
683            .to("user@example.com")
684            .subject("Plain Text")
685            .text("This is plain text content.");
686
687        mailer.send(msg).unwrap();
688        let sent = mailer.last().unwrap();
689        assert!(sent.html_body.is_none());
690        assert_eq!(
691            sent.text_body,
692            Some("This is plain text content.".to_string())
693        );
694    }
695
696    /// 测试 HTML 邮件(无纯文本)
697    #[test]
698    fn test_html_only_email() {
699        let mailer = MemoryMailer::new();
700        let msg = MailMessage::new()
701            .to("user@example.com")
702            .subject("HTML Only")
703            .html("<h1>HTML Content</h1>");
704
705        mailer.send(msg).unwrap();
706        let sent = mailer.last().unwrap();
707        assert_eq!(sent.html_body, Some("<h1>HTML Content</h1>".to_string()));
708        assert!(sent.text_body.is_none());
709    }
710}