use parking_lot::Mutex;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MailError {
#[error("邮件字段缺失: {0}")]
MissingField(String),
#[error("邮件发送失败: {0}")]
SendFailed(String),
#[error("附件读取失败: {path} — {source}")]
AttachmentRead {
path: String,
#[source]
source: std::io::Error,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MailAddress {
pub email: String,
pub name: Option<String>,
}
impl MailAddress {
pub fn new(email: impl Into<String>) -> Self {
Self {
email: email.into(),
name: None,
}
}
pub fn with_name(email: impl Into<String>, name: impl Into<String>) -> Self {
Self {
email: email.into(),
name: Some(name.into()),
}
}
pub fn to_rfc5322_string(&self) -> String {
match &self.name {
Some(name) => format!("\"{}\" <{}>", name, self.email),
None => self.email.clone(),
}
}
}
impl From<&str> for MailAddress {
fn from(email: &str) -> Self {
Self::new(email)
}
}
impl From<String> for MailAddress {
fn from(email: String) -> Self {
Self::new(email)
}
}
#[derive(Debug, Clone)]
pub struct MailAttachment {
pub filename: String,
pub content: Vec<u8>,
pub mime_type: String,
}
impl MailAttachment {
pub fn new(
filename: impl Into<String>,
content: Vec<u8>,
mime_type: impl Into<String>,
) -> Self {
Self {
filename: filename.into(),
content,
mime_type: mime_type.into(),
}
}
pub async fn from_file(
path: impl AsRef<std::path::Path>,
filename: Option<&str>,
mime_type: impl Into<String>,
) -> Result<Self, MailError> {
let path_ref = path.as_ref();
let content = tokio::fs::read(path_ref)
.await
.map_err(|e| MailError::AttachmentRead {
path: path_ref.display().to_string(),
source: e,
})?;
let filename = match filename {
Some(name) => name.to_string(),
None => path_ref
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("attachment")
.to_string(),
};
Ok(Self::new(filename, content, mime_type))
}
}
#[derive(Debug, Clone, Default)]
pub struct MailMessage {
pub from: Option<MailAddress>,
pub reply_to: Option<MailAddress>,
pub to: Vec<MailAddress>,
pub cc: Vec<MailAddress>,
pub bcc: Vec<MailAddress>,
pub subject: String,
pub html_body: Option<String>,
pub text_body: Option<String>,
pub attachments: Vec<MailAttachment>,
}
impl MailMessage {
pub fn new() -> Self {
Self::default()
}
pub fn from(mut self, address: impl Into<MailAddress>) -> Self {
self.from = Some(address.into());
self
}
pub fn reply_to(mut self, address: impl Into<MailAddress>) -> Self {
self.reply_to = Some(address.into());
self
}
pub fn to(mut self, address: impl Into<MailAddress>) -> Self {
self.to.push(address.into());
self
}
pub fn cc(mut self, address: impl Into<MailAddress>) -> Self {
self.cc.push(address.into());
self
}
pub fn bcc(mut self, address: impl Into<MailAddress>) -> Self {
self.bcc.push(address.into());
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = subject.into();
self
}
pub fn html(mut self, content: impl Into<String>) -> Self {
self.html_body = Some(content.into());
self
}
pub fn text(mut self, content: impl Into<String>) -> Self {
self.text_body = Some(content.into());
self
}
pub fn attach(mut self, attachment: MailAttachment) -> Self {
self.attachments.push(attachment);
self
}
pub fn validate(&self) -> Result<(), MailError> {
if self.to.is_empty() {
return Err(MailError::MissingField("收件人 (to) 不能为空".to_string()));
}
if self.subject.is_empty() {
return Err(MailError::MissingField(
"主题 (subject) 不能为空".to_string(),
));
}
if self.html_body.is_none() && self.text_body.is_none() {
return Err(MailError::MissingField(
"内容 (html 或 text) 不能同时为空".to_string(),
));
}
Ok(())
}
}
pub trait Mailer: Send + Sync {
fn send(&self, message: MailMessage) -> Result<(), MailError>;
}
#[derive(Debug, Clone, Default)]
pub struct MemoryMailer {
sent: Arc<Mutex<Vec<MailMessage>>>,
}
impl MemoryMailer {
pub fn new() -> Self {
Self::default()
}
pub fn count(&self) -> usize {
self.sent.lock().len()
}
pub fn all(&self) -> Vec<MailMessage> {
self.sent.lock().clone()
}
pub fn last(&self) -> Option<MailMessage> {
self.sent.lock().last().cloned()
}
pub fn clear(&self) {
self.sent.lock().clear();
}
}
impl Mailer for MemoryMailer {
fn send(&self, message: MailMessage) -> Result<(), MailError> {
message.validate()?;
self.sent.lock().push(message);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mail_address_new() {
let addr = MailAddress::new("user@example.com");
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name, None);
assert_eq!(addr.to_rfc5322_string(), "user@example.com");
}
#[test]
fn test_mail_address_with_name() {
let addr = MailAddress::with_name("user@example.com", "张三");
assert_eq!(addr.email, "user@example.com");
assert_eq!(addr.name, Some("张三".to_string()));
assert_eq!(addr.to_rfc5322_string(), "\"张三\" <user@example.com>");
}
#[test]
fn test_mail_address_from_str() {
let addr: MailAddress = "test@example.com".into();
assert_eq!(addr.email, "test@example.com");
assert_eq!(addr.name, None);
}
#[test]
fn test_mail_message_builder_chain() {
let msg = MailMessage::new()
.from("sender@example.com")
.to("user1@example.com")
.to("user2@example.com")
.cc("cc@example.com")
.bcc("bcc@example.com")
.subject("Test Subject")
.html("<h1>Hello</h1>")
.text("Hello");
assert_eq!(msg.from.unwrap().email, "sender@example.com");
assert_eq!(msg.to.len(), 2);
assert_eq!(msg.to[0].email, "user1@example.com");
assert_eq!(msg.to[1].email, "user2@example.com");
assert_eq!(msg.cc.len(), 1);
assert_eq!(msg.bcc.len(), 1);
assert_eq!(msg.subject, "Test Subject");
assert_eq!(msg.html_body, Some("<h1>Hello</h1>".to_string()));
assert_eq!(msg.text_body, Some("Hello".to_string()));
}
#[test]
fn test_validate_success() {
let msg = MailMessage::new()
.to("user@example.com")
.subject("Test")
.html("<p>Hello</p>");
assert!(msg.validate().is_ok());
}
#[test]
fn test_validate_missing_to() {
let msg = MailMessage::new().subject("Test").html("<p>Hello</p>");
let result = msg.validate();
assert!(result.is_err());
match result {
Err(MailError::MissingField(msg)) => {
assert!(msg.contains("收件人"));
}
_ => panic!("期望 MissingField 错误"),
}
}
#[test]
fn test_validate_missing_subject() {
let msg = MailMessage::new()
.to("user@example.com")
.html("<p>Hello</p>");
let result = msg.validate();
assert!(result.is_err());
match result {
Err(MailError::MissingField(msg)) => {
assert!(msg.contains("主题"));
}
_ => panic!("期望 MissingField 错误"),
}
}
#[test]
fn test_validate_missing_content() {
let msg = MailMessage::new().to("user@example.com").subject("Test");
let result = msg.validate();
assert!(result.is_err());
match result {
Err(MailError::MissingField(msg)) => {
assert!(msg.contains("内容"));
}
_ => panic!("期望 MissingField 错误"),
}
}
#[test]
fn test_memory_mailer_send() {
let mailer = MemoryMailer::new();
let msg = MailMessage::new()
.to("user@example.com")
.subject("Test")
.html("<p>Hello</p>");
let result = mailer.send(msg);
assert!(result.is_ok());
assert_eq!(mailer.count(), 1);
let sent = mailer.last().unwrap();
assert_eq!(sent.subject, "Test");
assert_eq!(sent.to.len(), 1);
assert_eq!(sent.to[0].email, "user@example.com");
}
#[test]
fn test_memory_mailer_send_multiple() {
let mailer = MemoryMailer::new();
for i in 0..3 {
let msg = MailMessage::new()
.to(format!("user{}@example.com", i))
.subject(format!("Subject {}", i))
.text(format!("Body {}", i));
mailer.send(msg).unwrap();
}
assert_eq!(mailer.count(), 3);
let all = mailer.all();
assert_eq!(all[0].subject, "Subject 0");
assert_eq!(all[2].subject, "Subject 2");
}
#[test]
fn test_memory_mailer_send_invalid_errors() {
let mailer = MemoryMailer::new();
let msg = MailMessage::new().subject("No recipient").html("<p>Hi</p>");
let result = mailer.send(msg);
assert!(result.is_err());
assert_eq!(mailer.count(), 0);
}
#[test]
fn test_memory_mailer_clear() {
let mailer = MemoryMailer::new();
let msg = MailMessage::new()
.to("user@example.com")
.subject("Test")
.text("Hi");
mailer.send(msg).unwrap();
assert_eq!(mailer.count(), 1);
mailer.clear();
assert_eq!(mailer.count(), 0);
assert!(mailer.last().is_none());
}
#[test]
fn test_attachment_from_bytes() {
let attachment = MailAttachment::new("doc.pdf", vec![1, 2, 3], "application/pdf");
assert_eq!(attachment.filename, "doc.pdf");
assert_eq!(attachment.content, vec![1, 2, 3]);
assert_eq!(attachment.mime_type, "application/pdf");
}
#[tokio::test]
async fn test_attachment_from_file() {
let temp_dir = std::env::temp_dir().join("sz_rust_mail_test");
let _ = std::fs::create_dir_all(&temp_dir);
let file_path = temp_dir.join("test.txt");
std::fs::write(&file_path, b"hello attachment").unwrap();
let attachment = MailAttachment::from_file(&file_path, None, "text/plain")
.await
.unwrap();
assert_eq!(attachment.filename, "test.txt");
assert_eq!(attachment.content, b"hello attachment");
assert_eq!(attachment.mime_type, "text/plain");
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[tokio::test]
async fn test_attachment_from_nonexistent_file_errors() {
let result = MailAttachment::from_file("/nonexistent/file.txt", None, "text/plain").await;
assert!(result.is_err());
match result {
Err(MailError::AttachmentRead { .. }) => {}
_ => panic!("期望 AttachmentRead 错误"),
}
}
#[test]
fn test_send_mail_with_attachment() {
let mailer = MemoryMailer::new();
let attachment = MailAttachment::new("report.pdf", vec![1, 2, 3, 4], "application/pdf");
let msg = MailMessage::new()
.to("user@example.com")
.subject("Report")
.text("Please find the attached report.")
.attach(attachment);
mailer.send(msg).unwrap();
let sent = mailer.last().unwrap();
assert_eq!(sent.attachments.len(), 1);
assert_eq!(sent.attachments[0].filename, "report.pdf");
}
#[test]
fn test_mail_message_default() {
let msg = MailMessage::default();
assert!(msg.from.is_none());
assert!(msg.reply_to.is_none());
assert!(msg.to.is_empty());
assert!(msg.cc.is_empty());
assert!(msg.bcc.is_empty());
assert!(msg.subject.is_empty());
assert!(msg.html_body.is_none());
assert!(msg.text_body.is_none());
assert!(msg.attachments.is_empty());
}
#[test]
fn test_text_only_email() {
let mailer = MemoryMailer::new();
let msg = MailMessage::new()
.to("user@example.com")
.subject("Plain Text")
.text("This is plain text content.");
mailer.send(msg).unwrap();
let sent = mailer.last().unwrap();
assert!(sent.html_body.is_none());
assert_eq!(
sent.text_body,
Some("This is plain text content.".to_string())
);
}
#[test]
fn test_html_only_email() {
let mailer = MemoryMailer::new();
let msg = MailMessage::new()
.to("user@example.com")
.subject("HTML Only")
.html("<h1>HTML Content</h1>");
mailer.send(msg).unwrap();
let sent = mailer.last().unwrap();
assert_eq!(sent.html_body, Some("<h1>HTML Content</h1>".to_string()));
assert!(sent.text_body.is_none());
}
}