Skip to main content

wae_email/
mime.rs

1//! MIME 消息构建模块
2//!
3//! 实现符合 RFC 5322 和 RFC 2047 标准的邮件消息构建功能。
4//! 支持纯文本邮件、HTML 邮件、附件以及 multipart 消息格式。
5
6use std::fmt;
7
8use base64::{Engine as _, engine::general_purpose::STANDARD};
9
10/// 邮件附件结构体
11#[derive(Debug, Clone)]
12pub struct Attachment {
13    /// 附件文件名
14    pub filename: String,
15    /// 附件 MIME 类型
16    pub content_type: String,
17    /// 附件内容(二进制数据)
18    pub data: Vec<u8>,
19}
20
21impl Attachment {
22    /// 创建新的附件
23    ///
24    /// # 参数
25    /// - `filename`: 附件文件名
26    /// - `content_type`: MIME 类型,如 "application/pdf"
27    /// - `data`: 附件的二进制内容
28    pub fn new(filename: impl Into<String>, content_type: impl Into<String>, data: Vec<u8>) -> Self {
29        Self { filename: filename.into(), content_type: content_type.into(), data }
30    }
31
32    /// 从文本创建附件
33    ///
34    /// # 参数
35    /// - `filename`: 附件文件名
36    /// - `content`: 文本内容
37    pub fn from_text(filename: impl Into<String>, content: impl Into<String>) -> Self {
38        Self {
39            filename: filename.into(),
40            content_type: "text/plain; charset=utf-8".to_string(),
41            data: content.into().into_bytes(),
42        }
43    }
44}
45
46/// 邮件消息结构体
47///
48/// 存储邮件的所有元数据和内容,支持生成符合 RFC 5322 标准的邮件内容。
49#[derive(Debug, Clone, Default)]
50pub struct EmailMessage {
51    /// 发件人地址
52    pub from: String,
53    /// 收件人地址列表
54    pub to: Vec<String>,
55    /// 抄送地址列表
56    pub cc: Vec<String>,
57    /// 密送地址列表
58    pub bcc: Vec<String>,
59    /// 邮件主题
60    pub subject: String,
61    /// 纯文本正文
62    pub body: Option<String>,
63    /// HTML 正文
64    pub html_body: Option<String>,
65    /// 附件列表
66    pub attachments: Vec<Attachment>,
67    /// 自定义头部字段
68    pub headers: Vec<(String, String)>,
69    /// 消息 ID
70    pub message_id: Option<String>,
71    /// 回复地址
72    pub reply_to: Option<String>,
73}
74
75impl EmailMessage {
76    /// 创建新的邮件消息
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// 使用构建器创建邮件
82    pub fn builder() -> EmailBuilder {
83        EmailBuilder::new()
84    }
85
86    /// 生成完整的邮件内容(字节形式)
87    ///
88    /// 返回符合 RFC 5322 标准的邮件字节数据
89    pub fn to_bytes(&self) -> Vec<u8> {
90        let mut output = Vec::new();
91
92        self.write_headers(&mut output);
93
94        let boundary = self.generate_boundary();
95
96        if !self.attachments.is_empty() {
97            self.write_multipart_mixed(&mut output, &boundary);
98        }
99        else if self.html_body.is_some() && self.body.is_some() {
100            self.write_multipart_alternative(&mut output, &boundary);
101        }
102        else if let Some(ref html) = self.html_body {
103            self.write_html_only(&mut output, html);
104        }
105        else if let Some(ref text) = self.body {
106            self.write_text_only(&mut output, text);
107        }
108        else {
109            output.extend_from_slice(b"\r\n");
110        }
111
112        output
113    }
114
115    /// 写入邮件头部
116    fn write_headers(&self, output: &mut Vec<u8>) {
117        if let Some(ref id) = self.message_id {
118            output.extend_from_slice(format!("Message-ID: <{}>\r\n", id).as_bytes());
119        }
120
121        output.extend_from_slice(format!("From: {}\r\n", self.encode_address(&self.from)).as_bytes());
122
123        if !self.to.is_empty() {
124            let to_encoded: Vec<String> = self.to.iter().map(|a| self.encode_address(a)).collect();
125            output.extend_from_slice(format!("To: {}\r\n", to_encoded.join(", ")).as_bytes());
126        }
127
128        if !self.cc.is_empty() {
129            let cc_encoded: Vec<String> = self.cc.iter().map(|a| self.encode_address(a)).collect();
130            output.extend_from_slice(format!("Cc: {}\r\n", cc_encoded.join(", ")).as_bytes());
131        }
132
133        if let Some(ref reply_to) = self.reply_to {
134            output.extend_from_slice(format!("Reply-To: {}\r\n", self.encode_address(reply_to)).as_bytes());
135        }
136
137        output.extend_from_slice(format!("Subject: {}\r\n", encode_subject(&self.subject)).as_bytes());
138
139        output.extend_from_slice(b"Date: ");
140        output.extend_from_slice(generate_date().as_bytes());
141        output.extend_from_slice(b"\r\n");
142
143        output.extend_from_slice(b"MIME-Version: 1.0\r\n");
144
145        for (name, value) in &self.headers {
146            output.extend_from_slice(format!("{}: {}\r\n", name, value).as_bytes());
147        }
148    }
149
150    /// 编码邮件地址(处理非 ASCII 字符)
151    fn encode_address(&self, addr: &str) -> String {
152        if addr.is_ascii() {
153            addr.to_string()
154        }
155        else {
156            if let Some(at_pos) = addr.rfind('@') {
157                let name_part = &addr[..at_pos];
158                let domain_part = &addr[at_pos..];
159                if name_part.is_ascii() { addr.to_string() } else { format!("{}{}", encode_subject(name_part), domain_part) }
160            }
161            else {
162                encode_subject(addr)
163            }
164        }
165    }
166
167    /// 生成 boundary 字符串
168    fn generate_boundary(&self) -> String {
169        let timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos();
170        format!("----=_Part_{}_{}", timestamp, rand_suffix())
171    }
172
173    /// 写入纯文本邮件
174    fn write_text_only(&self, output: &mut Vec<u8>, text: &str) {
175        if text.is_ascii() {
176            output.extend_from_slice(b"Content-Type: text/plain; charset=utf-8\r\n");
177            output.extend_from_slice(b"Content-Transfer-Encoding: 7bit\r\n");
178            output.extend_from_slice(b"\r\n");
179            output.extend_from_slice(text.as_bytes());
180        }
181        else {
182            output.extend_from_slice(b"Content-Type: text/plain; charset=utf-8\r\n");
183            output.extend_from_slice(b"Content-Transfer-Encoding: base64\r\n");
184            output.extend_from_slice(b"\r\n");
185            let encoded = STANDARD.encode(text.as_bytes());
186            for line in encoded.as_bytes().chunks(76) {
187                output.extend_from_slice(line);
188                output.extend_from_slice(b"\r\n");
189            }
190        }
191    }
192
193    /// 写入 HTML 邮件
194    fn write_html_only(&self, output: &mut Vec<u8>, html: &str) {
195        if html.is_ascii() {
196            output.extend_from_slice(b"Content-Type: text/html; charset=utf-8\r\n");
197            output.extend_from_slice(b"Content-Transfer-Encoding: 7bit\r\n");
198            output.extend_from_slice(b"\r\n");
199            output.extend_from_slice(html.as_bytes());
200        }
201        else {
202            output.extend_from_slice(b"Content-Type: text/html; charset=utf-8\r\n");
203            output.extend_from_slice(b"Content-Transfer-Encoding: base64\r\n");
204            output.extend_from_slice(b"\r\n");
205            let encoded = STANDARD.encode(html.as_bytes());
206            for line in encoded.as_bytes().chunks(76) {
207                output.extend_from_slice(line);
208                output.extend_from_slice(b"\r\n");
209            }
210        }
211    }
212
213    /// 写入 multipart/alternative 消息(纯文本 + HTML)
214    fn write_multipart_alternative(&self, output: &mut Vec<u8>, boundary: &str) {
215        output.extend_from_slice(format!("Content-Type: multipart/alternative; boundary=\"{}\"\r\n", boundary).as_bytes());
216        output.extend_from_slice(b"\r\n");
217
218        output.extend_from_slice(b"This is a multi-part message in MIME format.\r\n\r\n");
219
220        if let Some(ref text) = self.body {
221            output.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
222            self.write_text_only(output, text);
223            output.extend_from_slice(b"\r\n");
224        }
225
226        if let Some(ref html) = self.html_body {
227            output.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
228            self.write_html_only(output, html);
229            output.extend_from_slice(b"\r\n");
230        }
231
232        output.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
233    }
234
235    /// 写入 multipart/mixed 消息(包含附件)
236    fn write_multipart_mixed(&self, output: &mut Vec<u8>, boundary: &str) {
237        output.extend_from_slice(format!("Content-Type: multipart/mixed; boundary=\"{}\"\r\n", boundary).as_bytes());
238        output.extend_from_slice(b"\r\n");
239
240        output.extend_from_slice(b"This is a multi-part message in MIME format.\r\n\r\n");
241
242        let has_content = self.body.is_some() || self.html_body.is_some();
243
244        if has_content {
245            output.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
246
247            if self.html_body.is_some() && self.body.is_some() {
248                let alt_boundary = format!("{}_alt", boundary);
249                output.extend_from_slice(
250                    format!("Content-Type: multipart/alternative; boundary=\"{}\"\r\n", alt_boundary).as_bytes(),
251                );
252                output.extend_from_slice(b"\r\n");
253
254                if let Some(ref text) = self.body {
255                    output.extend_from_slice(format!("--{}\r\n", alt_boundary).as_bytes());
256                    self.write_text_only(output, text);
257                    output.extend_from_slice(b"\r\n");
258                }
259
260                if let Some(ref html) = self.html_body {
261                    output.extend_from_slice(format!("--{}\r\n", alt_boundary).as_bytes());
262                    self.write_html_only(output, html);
263                    output.extend_from_slice(b"\r\n");
264                }
265
266                output.extend_from_slice(format!("--{}--\r\n\r\n", alt_boundary).as_bytes());
267            }
268            else if let Some(ref html) = self.html_body {
269                self.write_html_only(output, html);
270                output.extend_from_slice(b"\r\n");
271            }
272            else if let Some(ref text) = self.body {
273                self.write_text_only(output, text);
274                output.extend_from_slice(b"\r\n");
275            }
276        }
277
278        for attachment in &self.attachments {
279            output.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
280            self.write_attachment(output, attachment);
281        }
282
283        output.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
284    }
285
286    /// 写入附件
287    fn write_attachment(&self, output: &mut Vec<u8>, attachment: &Attachment) {
288        let filename_encoded = encode_subject(&attachment.filename);
289        output.extend_from_slice(
290            format!("Content-Type: {}; name=\"{}\"\r\n", attachment.content_type, filename_encoded).as_bytes(),
291        );
292        output.extend_from_slice(b"Content-Transfer-Encoding: base64\r\n");
293        output.extend_from_slice(format!("Content-Disposition: attachment; filename=\"{}\"\r\n", filename_encoded).as_bytes());
294        output.extend_from_slice(b"\r\n");
295
296        let encoded = STANDARD.encode(&attachment.data);
297        for line in encoded.as_bytes().chunks(76) {
298            output.extend_from_slice(line);
299            output.extend_from_slice(b"\r\n");
300        }
301        output.extend_from_slice(b"\r\n");
302    }
303}
304
305impl fmt::Display for EmailMessage {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        write!(f, "{}", String::from_utf8_lossy(&self.to_bytes()))
308    }
309}
310
311/// 邮件构建器
312///
313/// 支持链式调用构建邮件消息。
314#[derive(Debug, Clone, Default)]
315pub struct EmailBuilder {
316    message: EmailMessage,
317}
318
319impl EmailBuilder {
320    /// 创建新的邮件构建器
321    pub fn new() -> Self {
322        Self::default()
323    }
324
325    /// 设置发件人
326    pub fn from(mut self, from: impl Into<String>) -> Self {
327        self.message.from = from.into();
328        self
329    }
330
331    /// 添加收件人
332    pub fn to(mut self, to: impl Into<String>) -> Self {
333        self.message.to.push(to.into());
334        self
335    }
336
337    /// 添加多个收件人
338    pub fn to_multiple(mut self, addresses: Vec<String>) -> Self {
339        self.message.to.extend(addresses);
340        self
341    }
342
343    /// 添加抄送
344    pub fn cc(mut self, cc: impl Into<String>) -> Self {
345        self.message.cc.push(cc.into());
346        self
347    }
348
349    /// 添加密送
350    pub fn bcc(mut self, bcc: impl Into<String>) -> Self {
351        self.message.bcc.push(bcc.into());
352        self
353    }
354
355    /// 设置邮件主题
356    pub fn subject(mut self, subject: impl Into<String>) -> Self {
357        self.message.subject = subject.into();
358        self
359    }
360
361    /// 设置纯文本正文
362    pub fn body(mut self, body: impl Into<String>) -> Self {
363        self.message.body = Some(body.into());
364        self
365    }
366
367    /// 设置 HTML 正文
368    pub fn html_body(mut self, html: impl Into<String>) -> Self {
369        self.message.html_body = Some(html.into());
370        self
371    }
372
373    /// 添加附件
374    pub fn attachment(mut self, attachment: Attachment) -> Self {
375        self.message.attachments.push(attachment);
376        self
377    }
378
379    /// 添加多个附件
380    pub fn attachments(mut self, attachments: Vec<Attachment>) -> Self {
381        self.message.attachments.extend(attachments);
382        self
383    }
384
385    /// 设置消息 ID
386    pub fn message_id(mut self, id: impl Into<String>) -> Self {
387        self.message.message_id = Some(id.into());
388        self
389    }
390
391    /// 设置回复地址
392    pub fn reply_to(mut self, reply_to: impl Into<String>) -> Self {
393        self.message.reply_to = Some(reply_to.into());
394        self
395    }
396
397    /// 添加自定义头部
398    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
399        self.message.headers.push((name.into(), value.into()));
400        self
401    }
402
403    /// 构建邮件消息
404    pub fn build(self) -> EmailMessage {
405        self.message
406    }
407}
408
409/// 使用 RFC 2047 编码主题(支持 UTF-8)
410///
411/// 格式: =?UTF-8?B?base64_encoded_text?=
412pub fn encode_subject(subject: &str) -> String {
413    if subject.is_ascii() {
414        subject.to_string()
415    }
416    else {
417        let encoded = STANDARD.encode(subject.as_bytes());
418        format!("=?UTF-8?B?{}?=", encoded)
419    }
420}
421
422/// 生成当前时间的 RFC 5322 格式日期字符串
423pub fn generate_date() -> String {
424    chrono_now()
425}
426
427/// 获取当前时间的格式化字符串
428fn chrono_now() -> String {
429    use std::time::{SystemTime, UNIX_EPOCH};
430
431    let now = SystemTime::now();
432    let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
433    let secs = duration.as_secs();
434
435    let days_since_epoch = secs / 86400;
436    let secs_of_day = secs % 86400;
437    let hours = secs_of_day / 3600;
438    let minutes = (secs_of_day % 3600) / 60;
439    let seconds = secs_of_day % 60;
440
441    let (year, month, day, weekday) = days_to_date(days_since_epoch as i64);
442
443    let weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
444    let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
445
446    format!(
447        "{}, {:02} {} {:04} {:02}:{:02}:{:02} +0800",
448        weekdays[weekday as usize],
449        day,
450        months[(month - 1) as usize],
451        year,
452        hours,
453        minutes,
454        seconds
455    )
456}
457
458/// 将 Unix 天数转换为日期
459fn days_to_date(days: i64) -> (i32, i32, i32, i32) {
460    let mut year = 1970;
461    let mut days_left = days;
462
463    loop {
464        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
465        if days_left < days_in_year {
466            break;
467        }
468        days_left -= days_in_year;
469        year += 1;
470    }
471
472    let days_in_months = if is_leap_year(year) {
473        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
474    }
475    else {
476        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
477    };
478
479    let mut month = 1;
480    for &days_in_month in &days_in_months {
481        if days_left < days_in_month as i64 {
482            break;
483        }
484        days_left -= days_in_month as i64;
485        month += 1;
486    }
487
488    let day = days_left + 1;
489
490    let days_since_epoch = days;
491    let weekday = ((days_since_epoch + 3) % 7) as i32;
492    let weekday = if weekday < 0 { weekday + 7 } else { weekday };
493
494    (year, month, day as i32, weekday)
495}
496
497/// 判断是否为闰年
498fn is_leap_year(year: i32) -> bool {
499    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
500}
501
502/// 生成随机后缀
503fn rand_suffix() -> String {
504    use std::time::{SystemTime, UNIX_EPOCH};
505
506    let now = SystemTime::now();
507    let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
508    let nanos = duration.subsec_nanos();
509
510    format!("{:08x}", nanos.wrapping_mul(2654435761))
511}