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 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#[derive(Debug, Clone, Default)]
234pub struct MailMessage {
235 pub from: Option<MailAddress>,
237 pub reply_to: Option<MailAddress>,
239 pub to: Vec<MailAddress>,
241 pub cc: Vec<MailAddress>,
243 pub bcc: Vec<MailAddress>,
245 pub subject: String,
247 pub html_body: Option<String>,
249 pub text_body: Option<String>,
251 pub attachments: Vec<MailAttachment>,
253}
254
255impl MailMessage {
256 pub fn new() -> Self {
258 Self::default()
259 }
260
261 pub fn from(mut self, address: impl Into<MailAddress>) -> Self {
267 self.from = Some(address.into());
268 self
269 }
270
271 pub fn reply_to(mut self, address: impl Into<MailAddress>) -> Self {
273 self.reply_to = Some(address.into());
274 self
275 }
276
277 pub fn to(mut self, address: impl Into<MailAddress>) -> Self {
279 self.to.push(address.into());
280 self
281 }
282
283 pub fn cc(mut self, address: impl Into<MailAddress>) -> Self {
285 self.cc.push(address.into());
286 self
287 }
288
289 pub fn bcc(mut self, address: impl Into<MailAddress>) -> Self {
291 self.bcc.push(address.into());
292 self
293 }
294
295 pub fn subject(mut self, subject: impl Into<String>) -> Self {
297 self.subject = subject.into();
298 self
299 }
300
301 pub fn html(mut self, content: impl Into<String>) -> Self {
303 self.html_body = Some(content.into());
304 self
305 }
306
307 pub fn text(mut self, content: impl Into<String>) -> Self {
309 self.text_body = Some(content.into());
310 self
311 }
312
313 pub fn attach(mut self, attachment: MailAttachment) -> Self {
315 self.attachments.push(attachment);
316 self
317 }
318
319 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
340pub trait Mailer: Send + Sync {
358 fn send(&self, message: MailMessage) -> Result<(), MailError>;
368}
369
370#[derive(Debug, Clone, Default)]
382pub struct MemoryMailer {
383 sent: Arc<Mutex<Vec<MailMessage>>>,
385}
386
387impl MemoryMailer {
388 pub fn new() -> Self {
390 Self::default()
391 }
392
393 pub fn count(&self) -> usize {
395 self.sent.lock().len()
396 }
397
398 pub fn all(&self) -> Vec<MailMessage> {
400 self.sent.lock().clone()
401 }
402
403 pub fn last(&self) -> Option<MailMessage> {
405 self.sent.lock().last().cloned()
406 }
407
408 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 message.validate()?;
418
419 self.sent.lock().push(message);
421 Ok(())
422 }
423}
424
425#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}