1use parking_lot::Mutex;
36use std::sync::Arc;
37use thiserror::Error;
38
39#[derive(Debug, Error)]
45pub enum MailError {
46 #[error("邮件字段缺失: {0}")]
48 MissingField(String),
49 #[error("邮件发送失败: {0}")]
51 SendFailed(String),
52 #[error("附件读取失败: {path} — {source}")]
54 AttachmentRead {
55 path: String,
57 #[source]
59 source: std::io::Error,
60 },
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct MailAddress {
72 pub email: String,
74 pub name: Option<String>,
76}
77
78impl MailAddress {
79 pub fn new(email: impl Into<String>) -> Self {
85 Self {
86 email: email.into(),
87 name: None,
88 }
89 }
90
91 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 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#[derive(Debug, Clone)]
136pub struct MailAttachment {
137 pub filename: String,
139 pub content: Vec<u8>,
141 pub mime_type: String,
143}
144
145impl MailAttachment {
146 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 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#[derive(Debug, Clone, Default)]
232pub struct MailMessage {
233 pub from: Option<MailAddress>,
235 pub reply_to: Option<MailAddress>,
237 pub to: Vec<MailAddress>,
239 pub cc: Vec<MailAddress>,
241 pub bcc: Vec<MailAddress>,
243 pub subject: String,
245 pub html_body: Option<String>,
247 pub text_body: Option<String>,
249 pub attachments: Vec<MailAttachment>,
251}
252
253impl MailMessage {
254 pub fn new() -> Self {
256 Self::default()
257 }
258
259 pub fn from(mut self, address: impl Into<MailAddress>) -> Self {
265 self.from = Some(address.into());
266 self
267 }
268
269 pub fn reply_to(mut self, address: impl Into<MailAddress>) -> Self {
271 self.reply_to = Some(address.into());
272 self
273 }
274
275 pub fn to(mut self, address: impl Into<MailAddress>) -> Self {
277 self.to.push(address.into());
278 self
279 }
280
281 pub fn cc(mut self, address: impl Into<MailAddress>) -> Self {
283 self.cc.push(address.into());
284 self
285 }
286
287 pub fn bcc(mut self, address: impl Into<MailAddress>) -> Self {
289 self.bcc.push(address.into());
290 self
291 }
292
293 pub fn subject(mut self, subject: impl Into<String>) -> Self {
295 self.subject = subject.into();
296 self
297 }
298
299 pub fn html(mut self, content: impl Into<String>) -> Self {
301 self.html_body = Some(content.into());
302 self
303 }
304
305 pub fn text(mut self, content: impl Into<String>) -> Self {
307 self.text_body = Some(content.into());
308 self
309 }
310
311 pub fn attach(mut self, attachment: MailAttachment) -> Self {
313 self.attachments.push(attachment);
314 self
315 }
316
317 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
338pub trait Mailer: Send + Sync {
356 fn send(&self, message: MailMessage) -> Result<(), MailError>;
366}
367
368#[derive(Debug, Clone, Default)]
380pub struct MemoryMailer {
381 sent: Arc<Mutex<Vec<MailMessage>>>,
383}
384
385impl MemoryMailer {
386 pub fn new() -> Self {
388 Self::default()
389 }
390
391 pub fn count(&self) -> usize {
393 self.sent.lock().len()
394 }
395
396 pub fn all(&self) -> Vec<MailMessage> {
398 self.sent.lock().clone()
399 }
400
401 pub fn last(&self) -> Option<MailMessage> {
403 self.sent.lock().last().cloned()
404 }
405
406 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 message.validate()?;
416
417 self.sent.lock().push(message);
419 Ok(())
420 }
421}
422
423#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}