Skip to main content

br_email/
analyze.rs

1use br_crypto::encoding::code_to_utf8;
2use chrono::{DateTime, Local, TimeZone};
3use json::{object, JsonValue};
4use regex::Regex;
5use std::collections::HashMap;
6use std::ffi::OsStr;
7use std::io::{Error, ErrorKind, Write};
8use std::{env, fs, io};
9
10/// 解析邮件
11#[derive(Debug)]
12pub struct AnalyzeEmails {
13    pub debug: bool,
14    pub header: HashMap<String, String>,
15    pub mime_version: String,
16    boundary: String,
17    pub md5: String,
18    pub size: usize,
19    /// 时间戳
20    pub timestamp: i64,
21    /// 本地时间
22    pub datetime: String,
23    /// 主题
24    pub subject: String,
25    /// 发件人
26    pub from: HashMap<String, String>,
27    /// 收件人
28    pub to: HashMap<String, String>,
29    /// 抄送人
30    pub cc: HashMap<String, String>,
31    /// 用于指定收件人回复邮件时应该使用的电子邮件地址
32    pub replyto: HashMap<String, String>,
33    /// 内容类型
34    pub content_type: String,
35    /// 编码规则
36    pub content_transfer_encoding: ContentTransferEncoding,
37    /// 实际发件人
38    pub sender: String,
39    pub body_text: String,
40    pub body_html: String,
41    pub files: JsonValue,
42    pub charset: String,
43    /// 顶层附件文件名(来自 Content-Type name= 或 Content-Disposition filename=)
44    content_filename: String,
45}
46
47impl AnalyzeEmails {
48    pub fn new(mut data: Vec<u8>, debug: bool) -> io::Result<AnalyzeEmails> {
49        let md5 = br_crypto::md5::encrypt_hex(&data.clone()).to_string();
50        let size = data.len();
51        let data_string = String::from_utf8_lossy(&data).to_string();
52        if data_string.contains("\n\n") {
53            let updated_string = data_string.replace("\n", "\r\n");
54            data = updated_string.as_bytes().to_vec();
55        }
56
57        let subsequence = "\r\n\r\n".as_bytes();
58
59        let (header, body) = match data
60            .windows(subsequence.len())
61            .position(|window| window == subsequence)
62        {
63            None => {
64                if debug {
65                    fs::write(
66                        format!(
67                            "{}/xygs-{}.eml",
68                            env::current_dir()
69                                .unwrap_or_default()
70                                .to_str()
71                                .unwrap_or("."),
72                            md5
73                        ),
74                        data.clone(),
75                    )?;
76                }
77                return Err(Error::other(format!("协议格式错误: {md5}")));
78            }
79            Some(e) => (data[..e].to_vec(), data[e + 4..].to_vec()),
80        };
81        let mut that = Self {
82            debug,
83            header: Default::default(),
84            mime_version: "".to_string(),
85            boundary: "".to_string(),
86            md5,
87            size,
88            timestamp: 0,
89            subject: "".to_string(),
90            from: Default::default(),
91            to: Default::default(),
92            cc: Default::default(),
93            replyto: Default::default(),
94            datetime: "".to_string(),
95            content_type: "".to_string(),
96            content_transfer_encoding: ContentTransferEncoding::Bit7,
97            sender: "".to_string(),
98            body_text: "".to_string(),
99            body_html: "".to_string(),
100            files: object! {},
101            charset: "utf-8".to_string(),
102            content_filename: "".to_string(),
103        };
104        that.header(header)?;
105        that.body(body, data_string)?;
106        Ok(that)
107    }
108
109    fn header(&mut self, data: Vec<u8>) -> io::Result<()> {
110        let data = String::from_utf8_lossy(&data).to_string();
111        let data = data.replace("\r\n\t", "").replace("\r\n ", " ");
112        for item in data.lines() {
113            let (key, value) = match item.find(": ") {
114                Some(e) => (item[..e].to_string(), item[e + 2..].to_string()),
115                None => match item.find(":") {
116                    Some(e) => (item[..e].to_string(), item[e + 1..].to_string()),
117                    None => continue,
118                },
119            };
120            let name = key.to_lowercase();
121            if value.is_empty() {
122                continue;
123            }
124            match key.to_lowercase().as_str() {
125                "mime-version" => self.mime_version = value.to_string(),
126                "from" => {
127                    self.from = self.from(&value);
128                }
129                "sender" => {
130                    self.sender = value.to_string();
131                }
132                "to" => {
133                    self.to = self.email_encoded(&value);
134                }
135                "cc" => {
136                    self.cc = self.email_encoded(&value);
137                }
138                "reply-to" => {
139                    self.replyto = self.email_encoded(&value);
140                }
141                "subject" => {
142                    self.subject = self.subject(value.to_string());
143                }
144                "content-type" => {
145                    let types = value.split(";").collect::<Vec<&str>>();
146                    self.content_type = types[0].trim().to_lowercase().to_string();
147                    match self.content_type.as_str() {
148                        "multipart/mixed"
149                        | "multipart/alternative"
150                        | "multipart/related"
151                        | "multipart/report" => match types[1].find("boundary=") {
152                            None => {}
153                            Some(e) => {
154                                let boundary = &types[1][e..];
155                                self.boundary = boundary
156                                    .trim()
157                                    .trim_start_matches("boundary=")
158                                    .trim_start_matches("\"")
159                                    .trim_end_matches("\"")
160                                    .to_string();
161                            }
162                        },
163                        _ => {}
164                    }
165                    if types.len() > 1 {
166                        for item in types.iter() {
167                            let trimmed = item.trim();
168                            if trimmed.contains("charset=") {
169                                self.charset = trimmed
170                                    .split("charset=")
171                                    .last()
172                                    .unwrap_or("")
173                                    .trim_start_matches('"')
174                                    .trim_end_matches('"')
175                                    .to_string();
176                            }
177                            if trimmed.starts_with("name=") {
178                                self.content_filename =
179                                    self.encoded(trimmed.trim_start_matches("name="));
180                            }
181                        }
182                    }
183                }
184                "content-transfer-encoding" => {
185                    self.content_transfer_encoding = ContentTransferEncoding::from(&value);
186                }
187                "date" => self.datetime(&value)?,
188                "content-disposition" => {
189                    if self.content_filename.is_empty() && value.contains("filename=") {
190                        self.content_filename = value.split("filename=").collect::<Vec<&str>>()[1]
191                            .trim_start_matches('"')
192                            .trim_end_matches('"')
193                            .to_string();
194                    }
195                    if self.content_filename.is_empty() && value.contains("filename*=utf-8''") {
196                        self.content_filename =
197                            value.split("filename*=utf-8''").collect::<Vec<&str>>()[1]
198                                .trim_start_matches('"')
199                                .trim_end_matches('"')
200                                .to_string();
201                        self.content_filename =
202                            br_crypto::encoding::urlencoding_decode(self.content_filename.as_str());
203                    }
204                }
205                _ => {
206                    self.header
207                        .insert(name.trim().to_string(), value.to_string());
208                }
209            }
210        }
211        Ok(())
212    }
213    fn body(&mut self, data: Vec<u8>, old_data: String) -> io::Result<()> {
214        match self.content_type.to_lowercase().as_str() {
215            "text/html" => {
216                let data = self.content_transfer_encoding.decode(data)?;
217                let res = code_to_utf8(self.charset.as_str(), data.clone());
218                self.body_html = res;
219            }
220            "text/plain" => {
221                let data = self.content_transfer_encoding.decode(data)?;
222                let res = code_to_utf8(self.charset.as_str(), data.clone());
223                self.body_text = res;
224            }
225            "multipart/mixed"
226            | "multipart/alternative"
227            | "multipart/related"
228            | "multipart/report" => {
229                let data = self.content_transfer_encoding.decode(data.clone())?;
230                let mut parts = code_to_utf8(self.charset.as_str(), data.clone());
231                let mut parts_list = vec![];
232                let mut text = String::new();
233
234                parts = match parts.find(self.boundary.as_str()) {
235                    None => parts,
236                    Some(e) => parts[e..].to_string(),
237                };
238                for item in parts.lines() {
239                    if item.contains(self.boundary.as_str()) && text.is_empty() {
240                        continue;
241                    }
242                    if item.contains(self.boundary.as_str()) && !text.is_empty() {
243                        parts_list.push(text.clone());
244                        text = String::new();
245                        continue;
246                    }
247                    text.push_str(item);
248                    text.push_str("\r\n");
249                }
250                for part in parts_list {
251                    if part.trim().is_empty() {
252                        continue;
253                    }
254                    self.parts(part.to_string(), old_data.clone())?;
255                }
256            }
257            _ => {
258                if !self.content_filename.is_empty() {
259                    let data_str = String::from_utf8_lossy(&data).to_string();
260                    let encoding = self.content_transfer_encoding.clone();
261                    let filename = self.content_filename.clone();
262                    let ct = self.content_type.clone();
263                    self.set_files(encoding, &data_str, &filename, ct)?;
264                } else {
265                    log::warn!("未知body类型: {}, 已跳过", self.content_type);
266                }
267            }
268        }
269        Ok(())
270    }
271    /// 部分内容处理
272    fn parts(&mut self, data: String, old_data: String) -> io::Result<()> {
273        let (header_str, body) = match data.find("\r\n\r\n") {
274            None => {
275                if self.debug {
276                    fs::write(
277                        format!(
278                            "{}/head-{}.eml",
279                            env::current_dir()
280                                .unwrap_or_default()
281                                .to_str()
282                                .unwrap_or("."),
283                            self.md5
284                        ),
285                        old_data.clone(),
286                    )?;
287                }
288                return Err(Error::other("解析附件头失败"));
289            }
290            Some(e) => (
291                data[..e].replace("\r\n\t", " ").replace("\r\n ", " "),
292                &data[e + 4..],
293            ),
294        };
295
296        let mut filename = "".to_string();
297        let mut content_type = String::new();
298        let mut boundary = String::new();
299        let mut content_transfer_encoding = ContentTransferEncoding::None;
300        for item in header_str.lines() {
301            let (key, value) = match item.find(": ") {
302                Some(e) => (&item[..e], &item[e + 2..]),
303                None => match item.find(":") {
304                    Some(e) => (&item[..e], &item[e + 1..]),
305                    None => continue,
306                },
307            };
308
309            let name = key.to_lowercase();
310
311            match name.trim() {
312                "content-transfer-encoding" => {
313                    content_transfer_encoding = ContentTransferEncoding::from(value)
314                }
315                "content-type" => {
316                    let types = value.trim().split(";").collect::<Vec<&str>>();
317                    content_type = types[0].trim().to_string();
318                    let name = types
319                        .iter()
320                        .filter(|&x| x.trim().starts_with("name="))
321                        .map(|&x| x.trim().to_string())
322                        .collect::<Vec<String>>();
323                    if !name.is_empty() {
324                        let name = name[0].trim_start_matches("name=");
325                        filename = self.encoded(name);
326                    }
327                    match value.find("boundary=") {
328                        None => {}
329                        Some(i) => {
330                            let mut b = &value[i + 9..];
331                            b = match b.find(";") {
332                                None => b,
333                                Some(i) => &b[..i],
334                            };
335                            boundary = b
336                                .trim_start_matches("\"")
337                                .trim_end_matches("\"")
338                                .to_string();
339                        }
340                    }
341                }
342                "content-id"
343                | "content-length"
344                | "mime-version"
345                | "content-description"
346                | "date"
347                | "x-attachment-id" => {}
348                "content-disposition" => {
349                    if filename.is_empty() && value.contains("filename=") {
350                        filename = value.split("filename=").collect::<Vec<&str>>()[1]
351                            .trim_start_matches("\"")
352                            .trim_end_matches("\"")
353                            .to_string();
354                    }
355                    if filename.is_empty() && value.contains("filename*=utf-8''") {
356                        filename = value.split("filename*=utf-8''").collect::<Vec<&str>>()[1]
357                            .trim_start_matches("\"")
358                            .trim_end_matches("\"")
359                            .to_string();
360                        filename = br_crypto::encoding::urlencoding_decode(filename.as_str());
361                    }
362                }
363                _ => {
364                    return Err(Error::new(
365                        ErrorKind::NotFound,
366                        format!("parts 未知 header 类型: {name} [{item}]"),
367                    ));
368                }
369            }
370        }
371
372        match content_type.as_str() {
373            "text/plain" => {
374                if filename.is_empty() {
375                    let res = content_transfer_encoding.decode(body.as_bytes().to_vec())?;
376                    let text = code_to_utf8(self.charset.as_str(), res.clone());
377                    self.body_text = text;
378                } else {
379                    self.set_files(
380                        content_transfer_encoding,
381                        body,
382                        filename.as_str(),
383                        "".to_string(),
384                    )?;
385                }
386            }
387            "text/html" | "text/x-amp-html" => {
388                if filename.is_empty() {
389                    let res = content_transfer_encoding.decode(body.as_bytes().to_vec())?;
390                    self.body_html = code_to_utf8(self.charset.as_str(), res.clone());
391                } else {
392                    self.set_files(
393                        content_transfer_encoding,
394                        body,
395                        filename.as_str(),
396                        "".to_string(),
397                    )?;
398                }
399            }
400            "multipart/mixed" | "multipart/alternative" | "multipart/related" => {
401                let data = self
402                    .content_transfer_encoding
403                    .decode(body.as_bytes().to_vec())?;
404                let mut parts = code_to_utf8(self.charset.as_str(), data.clone());
405
406                parts = match parts.find(self.boundary.as_str()) {
407                    None => parts,
408                    Some(e) => parts[e..].to_string(),
409                };
410
411                let mut parts_list = vec![];
412                let mut text = String::new();
413                for item in parts.lines() {
414                    if item.contains(&boundary) && text.is_empty() {
415                        continue;
416                    }
417                    if item.contains(&boundary) && !text.is_empty() {
418                        parts_list.push(text);
419                        text = String::new();
420                        continue;
421                    }
422                    text.push_str(item);
423                    text.push_str("\r\n");
424                }
425                for part in parts_list {
426                    if part.trim().is_empty() {
427                        continue;
428                    }
429                    self.parts(part.to_string(), old_data.clone())?;
430                }
431            }
432            "text/calendar" => {}
433            "application/octet-stream"
434            | "application/zip"
435            | "application/pdf"
436            | "image/jpeg"
437            | "image/png"
438            | "image/gif"
439            | "application/ics"
440            | "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
441            | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
442            | "application/vnd.ms-excel" => {
443                if !filename.is_empty() {
444                    self.set_files(
445                        content_transfer_encoding,
446                        body,
447                        filename.as_str(),
448                        content_type.to_string(),
449                    )?;
450                }
451            }
452            _ => {
453                if !filename.is_empty() {
454                    self.set_files(
455                        content_transfer_encoding,
456                        body,
457                        filename.as_str(),
458                        content_type.to_string(),
459                    )?;
460                } else {
461                    if self.debug {
462                        fs::write(
463                            format!(
464                                "{}/content_type-{}.eml",
465                                env::current_dir()
466                                    .unwrap_or_default()
467                                    .to_str()
468                                    .unwrap_or("."),
469                                self.md5
470                            ),
471                            old_data.clone(),
472                        )?;
473                    }
474                    log::warn!(
475                        "未知 parts content_type 类型: {}, 无文件名已跳过",
476                        content_type
477                    );
478                }
479            }
480        }
481        Ok(())
482    }
483    pub fn from(&mut self, value: &str) -> HashMap<String, String> {
484        let mut r = value
485            .split("<")
486            .filter(|x| !x.trim().is_empty())
487            .map(|x| x.trim())
488            .collect::<Vec<&str>>();
489        if r[0].starts_with("\"") && r[0].ends_with("\"") {
490            r[0] = r[0].trim_start_matches("\"").trim_end_matches("\"").trim();
491        }
492        let mut emails = HashMap::new();
493        if r.len() == 1 {
494            let name = r[0].trim_end_matches(">").to_string();
495            emails.insert(name.clone(), name);
496        } else {
497            let name = self.encoded(r[0].trim());
498            let email = r[1].trim_end_matches(">").to_string();
499            emails.insert(email, name);
500        }
501        emails
502    }
503    fn subject(&mut self, value: String) -> String {
504        let value = value.replace("?==?", "?=\r\n\t=?");
505        if !value.contains("=?") && !value.contains("?=") {
506            return value.to_string();
507        }
508        let list = value.split("\r\n\t").collect::<Vec<&str>>();
509        let mut txt = vec![];
510        for item in list {
511            txt.push(self.encoded(item));
512        }
513        txt.join("")
514    }
515
516    fn encoded(&mut self, value: &str) -> String {
517        let t = value.trim_start_matches("\"").trim_end_matches("\"");
518        if t.contains("=?") && t.contains("?=") {
519            let l = t.split(" ").collect::<Vec<&str>>();
520            let mut txt = vec![];
521            for item in l {
522                txt.push(self.encoded_line(item));
523            }
524            txt.join("")
525        } else {
526            t.to_string()
527        }
528    }
529    /// 段落解码
530    fn encoded_line(&mut self, value: &str) -> String {
531        let line = value.split("?").collect::<Vec<&str>>();
532        if line.len() < 4 {
533            return value.to_string();
534        }
535        let charset = line[1].to_lowercase();
536        let code = line[2].to_uppercase();
537        let data = line[3];
538
539        let strs = match code.as_str() {
540            "B" => br_crypto::base64::decode_u8(data),
541            "Q" => br_crypto::qp::decode(data).unwrap_or(vec![]),
542            _ => data.as_bytes().to_vec(),
543        };
544        let text = code_to_utf8(&charset, strs.clone());
545        text.chars().filter(|&x| x != '\u{200b}').collect()
546    }
547
548    /// 时间处理
549    fn datetime(&mut self, value: &str) -> io::Result<()> {
550        let re =
551            Regex::new(r"\s*\(.*\)$").map_err(|e| Error::other(format!("正则表达式错误: {e}")))?;
552        let datetime = re.replace(value, "").to_string();
553        let datetime = datetime.replace("GMT", "+0000").to_string();
554        let datetime = match datetime.find(",") {
555            None => datetime,
556            Some(i) => datetime[i + 1..].trim().to_string(),
557        };
558        // 归一化月份缩写大小写 ("NOV" -> "Nov", "nov" -> "Nov")
559        let datetime = datetime
560            .split_whitespace()
561            .map(|word| {
562                if word.len() == 3 && word.chars().all(|c| c.is_ascii_alphabetic()) {
563                    let mut chars = word.chars();
564                    match chars.next() {
565                        Some(f) => {
566                            format!("{}{}", f.to_uppercase(), chars.as_str().to_lowercase())
567                        }
568                        None => word.to_string(),
569                    }
570                } else {
571                    word.to_string()
572                }
573            })
574            .collect::<Vec<_>>()
575            .join(" ");
576        // 尝试多种日期格式:标准 RFC 2822 (dd Mon YYYY) 和美式 (Mon dd YYYY)
577        let formats = ["%d %b %Y %H:%M:%S %z", "%b %d %Y %H:%M:%S %z"];
578        let datetime = formats
579            .iter()
580            .find_map(|fmt| DateTime::parse_from_str(datetime.as_str(), fmt).ok())
581            .ok_or_else(|| {
582                Error::other(format!("时间解析失败: 所有格式均不匹配 [{datetime:?}]"))
583            })?;
584
585        self.timestamp = datetime.timestamp();
586        self.datetime = Local
587            .timestamp_opt(self.timestamp, 0)
588            .single()
589            .map(|dt| {
590                dt.with_timezone(&Local)
591                    .format("%Y-%m-%d %H:%M:%S")
592                    .to_string()
593            })
594            .unwrap_or_default();
595        Ok(())
596    }
597    pub fn email_encoded(&mut self, value: &str) -> HashMap<String, String> {
598        let list = value.split(",").map(|x| x.trim()).collect::<Vec<&str>>();
599        let mut emails = HashMap::new();
600        for item in list {
601            let mut r = item.split(" <").collect::<Vec<&str>>();
602            if r[0].starts_with("\"") && r[0].ends_with("\"") {
603                r[0] = r[0].trim_start_matches("\"").trim_end_matches("\"");
604            }
605            if r.len() == 1 {
606                let name = r[0]
607                    .trim_start_matches("<")
608                    .trim_end_matches(">")
609                    .to_string();
610                emails.insert(name.clone(), name);
611            } else {
612                let name = self.encoded(r[0].trim());
613                let email = r[1].trim_end_matches(">").to_string();
614                emails.insert(email, name);
615            }
616        }
617        emails
618    }
619    fn set_files(
620        &mut self,
621        mut content_transfer_encoding: ContentTransferEncoding,
622        body: &str,
623        filename: &str,
624        mut content_type: String,
625    ) -> io::Result<()> {
626        let mut data_str = String::new();
627        if let ContentTransferEncoding::Base64 = content_transfer_encoding {
628            let mut text = "".to_string();
629            for line in body.lines() {
630                text += line;
631            }
632            data_str = text;
633        }
634
635        let body = content_transfer_encoding.decode(data_str.as_bytes().to_vec())?;
636        let md5 = br_crypto::md5::encrypt_hex(&body.clone());
637        let size = body.len();
638        let mut temp_dir = env::temp_dir();
639        temp_dir.push(filename);
640        let path_temp_dir = temp_dir.clone();
641
642        let mut temp_file = match fs::File::create(temp_dir.clone()) {
643            Ok(e) => e,
644            Err(e) => {
645                return Err(Error::other(format!(
646                    "打开(创建)临时文件: {e} [{filename}]"
647                )))
648            }
649        };
650
651        if temp_file.write(body.as_slice()).is_ok() {
652            if content_type.is_empty() {
653                content_type = path_temp_dir
654                    .extension()
655                    .unwrap_or(OsStr::new("unknown"))
656                    .to_str()
657                    .unwrap_or("unknown")
658                    .to_string();
659            }
660
661            self.files[md5.as_str()] = object! {
662                name:filename,
663                md5:md5.clone(),
664                size:size,
665                "content-type":content_type.clone(),
666                file:temp_dir.to_str()
667            };
668        };
669        Ok(())
670    }
671}
672
673impl Default for AnalyzeEmails {
674    fn default() -> Self {
675        Self {
676            debug: false,
677            header: Default::default(),
678            mime_version: "".to_string(),
679            boundary: "".to_string(),
680            md5: "".to_string(),
681            size: 0,
682            timestamp: 0,
683            datetime: "".to_string(),
684            subject: "".to_string(),
685            from: Default::default(),
686            to: Default::default(),
687            cc: Default::default(),
688            replyto: Default::default(),
689            content_type: "".to_string(),
690            content_transfer_encoding: ContentTransferEncoding::None,
691            sender: "".to_string(),
692            body_text: "".to_string(),
693            body_html: "".to_string(),
694            files: JsonValue::Null,
695            charset: "".to_string(),
696            content_filename: "".to_string(),
697        }
698    }
699}
700
701/// 编码规则
702/// 选择 Content-Transfer-Encoding 的原则
703///
704/// 纯文本: 如果内容是纯文本且只包含 ASCII 字符,通常使用 7bit。
705/// 非 ASCII 文本: 如果内容包含非 ASCII 字符,可以使用 quoted-printable 或 8bit,具体取决于内容和兼容性要求。
706/// 二进制数据: 对于图像、视频、音频等二进制数据,通常使用 base64 编码。
707#[derive(Debug, Clone)]
708pub enum ContentTransferEncoding {
709    /// 这种编码方式主要用于编码文本数据,它保持大部分文本的可读性,但会对非 ASCII 字符和特殊字符(如 =, ?, & 等)进行编码,以确保兼容性。
710    /// 适用于包含大量特殊字符或非 ASCII 文本的邮件内容。
711    QuotedPrintable,
712    ///    将二进制数据编码为 ASCII 字符串,使用 64 个字符的字母表(A-Z, a-z, 0-9, +, /)表示二进制数据。每 3 个字节的二进制数据编码为 4 个字符,便于在邮件中传输。
713    /// 常用于编码附件、图像、音频、视频等二进制数据。
714    Base64,
715    /// 表示内容是二进制数据,不能被转义或编码,必须保持原始的二进制格式进行传输。这种编码方式通常用于图像、音频等二进制文件。
716    /// 这种编码要求邮件传输代理能够处理所有可能的字节值,几乎不做任何转换,因此也不是所有系统都支持。
717    Binary,
718    /// 表示内容包含 8 位字符,这意味着它可能包含非 ASCII 字符(如带有音标的字母)。尽管这样编码的邮件可以包含更多字符,但并非所有邮件传输代理都支持 8bit 传输。
719    /// 适用于非 ASCII 的文本数据,但需要确保邮件传输链路支持 8bit 数据传输。
720    Bit8,
721    /// 表示内容是 ASCII 文本,仅包含 7 位字符(即标准 ASCII 字符集),每个字符的最高位是 0。这种编码方式是最常用的,因为它适合绝大多数邮件传输系统。
722    /// 适用于纯文本邮件,不包含任何特殊字符或二进制数据。
723    Bit7,
724    None,
725}
726
727impl ContentTransferEncoding {
728    fn from(value: &str) -> Self {
729        match value.to_lowercase().as_str() {
730            "7bit" => Self::Bit7,
731            "8bit" => Self::Bit8,
732            "binary" => Self::Binary,
733            "base64" => Self::Base64,
734            "quoted-printable" => Self::QuotedPrintable,
735            _ => Self::None,
736        }
737    }
738    fn decode(&mut self, mut data: Vec<u8>) -> io::Result<Vec<u8>> {
739        let res = match self {
740            ContentTransferEncoding::QuotedPrintable => br_crypto::qp::decode(data)?,
741            ContentTransferEncoding::Base64 => {
742                let str = String::from_utf8_lossy(&data).to_string();
743                let mut text = "".to_string();
744                for line in str.lines() {
745                    text += line;
746                }
747                data = text.as_bytes().to_vec();
748                br_crypto::base64::decode_u8(data)
749            }
750            ContentTransferEncoding::Binary => data,
751            ContentTransferEncoding::Bit8 => data,
752            ContentTransferEncoding::Bit7 => data,
753            ContentTransferEncoding::None => data,
754        };
755        Ok(res)
756    }
757}
758
759#[cfg(test)]
760#[allow(clippy::field_reassign_with_default)]
761mod tests {
762    use super::*;
763    use std::time::{SystemTime, UNIX_EPOCH};
764    use std::{env, fs};
765
766    fn unique_token(prefix: &str) -> String {
767        let nanos = SystemTime::now()
768            .duration_since(UNIX_EPOCH)
769            .unwrap()
770            .as_nanos();
771        format!("{prefix}-{nanos}-{}", std::process::id())
772    }
773
774    fn multipart_email(content_type: &str, boundary: &str, part: &str) -> Vec<u8> {
775        format!(
776            "From: sender@example.com\r\n\
777To: receiver@example.com\r\n\
778Subject: Multipart Test\r\n\
779Content-Type: {content_type};boundary=\"{boundary}\";charset=\"utf-8\"\r\n\
780Content-Transfer-Encoding: 7bit\r\n\
781Date: Mon, 01 Jan 2024 12:00:00 GMT (UTC)\r\n\
782\r\n\
783--{boundary}\r\n\
784{part}\r\n\
785--{boundary}--\r\n"
786        )
787        .into_bytes()
788    }
789
790    #[test]
791    fn test_content_transfer_encoding_from() {
792        assert!(matches!(
793            ContentTransferEncoding::from("7bit"),
794            ContentTransferEncoding::Bit7
795        ));
796        assert!(matches!(
797            ContentTransferEncoding::from("8bit"),
798            ContentTransferEncoding::Bit8
799        ));
800        assert!(matches!(
801            ContentTransferEncoding::from("base64"),
802            ContentTransferEncoding::Base64
803        ));
804        assert!(matches!(
805            ContentTransferEncoding::from("BASE64"),
806            ContentTransferEncoding::Base64
807        ));
808        assert!(matches!(
809            ContentTransferEncoding::from("quoted-printable"),
810            ContentTransferEncoding::QuotedPrintable
811        ));
812        assert!(matches!(
813            ContentTransferEncoding::from("binary"),
814            ContentTransferEncoding::Binary
815        ));
816        assert!(matches!(
817            ContentTransferEncoding::from("unknown"),
818            ContentTransferEncoding::None
819        ));
820    }
821
822    #[test]
823    fn test_content_transfer_encoding_decode_7bit() {
824        let mut enc = ContentTransferEncoding::Bit7;
825        let data = b"Hello World".to_vec();
826        let result = enc.decode(data.clone()).unwrap();
827        assert_eq!(result, data);
828    }
829
830    #[test]
831    fn test_content_transfer_encoding_decode_8bit() {
832        let mut enc = ContentTransferEncoding::Bit8;
833        let data = "你好世界".as_bytes().to_vec();
834        let result = enc.decode(data.clone()).unwrap();
835        assert_eq!(result, data);
836    }
837
838    #[test]
839    fn test_content_transfer_encoding_decode_base64() {
840        let mut enc = ContentTransferEncoding::Base64;
841        let data = b"SGVsbG8gV29ybGQ=".to_vec();
842        let result = enc.decode(data).unwrap();
843        assert_eq!(result, b"Hello World");
844    }
845
846    #[test]
847    fn test_analyze_emails_default() {
848        let email = AnalyzeEmails::default();
849        assert!(!email.debug);
850        assert_eq!(email.size, 0);
851        assert_eq!(email.timestamp, 0);
852        assert!(email.subject.is_empty());
853        assert!(email.from.is_empty());
854        assert!(email.to.is_empty());
855    }
856
857    #[test]
858    fn test_analyze_simple_email() {
859        let email_data = b"From: sender@example.com\r\n\
860To: receiver@example.com\r\n\
861Subject: Test Subject\r\n\
862Content-Type: text/plain\r\n\
863Date: 01 Jan 2024 12:00:00 +0000\r\n\
864\r\n\
865Hello, this is a test email body."
866            .to_vec();
867
868        let result = AnalyzeEmails::new(email_data, false).unwrap();
869        assert_eq!(result.subject, "Test Subject");
870        assert_eq!(result.content_type, "text/plain");
871        assert!(result.from.contains_key("sender@example.com"));
872        assert!(result.to.contains_key("receiver@example.com"));
873        assert_eq!(result.body_text, "Hello, this is a test email body.");
874    }
875
876    #[test]
877    fn test_analyze_email_with_encoded_subject() {
878        let email_data = b"From: test@example.com\r\n\
879To: receiver@example.com\r\n\
880Subject: =?UTF-8?B?5rWL6K+V5Li76aKY?=\r\n\
881Content-Type: text/plain\r\n\
882Date: 01 Jan 2024 12:00:00 +0000\r\n\
883\r\n\
884Test body"
885            .to_vec();
886
887        let result = AnalyzeEmails::new(email_data, false).unwrap();
888        assert!(result.subject.contains("测试主题"));
889    }
890
891    #[test]
892    fn test_analyze_email_html() {
893        let email_data = b"From: sender@example.com\r\n\
894To: receiver@example.com\r\n\
895Subject: HTML Test\r\n\
896Content-Type: text/html\r\n\
897Date: 01 Jan 2024 12:00:00 +0000\r\n\
898\r\n\
899<html><body><h1>Hello</h1></body></html>"
900            .to_vec();
901
902        let result = AnalyzeEmails::new(email_data, false).unwrap();
903        assert_eq!(result.content_type, "text/html");
904        assert!(result.body_html.contains("<h1>Hello</h1>"));
905    }
906
907    #[test]
908    fn test_analyze_email_invalid_format() {
909        let invalid_data = b"This is not a valid email".to_vec();
910        let result = AnalyzeEmails::new(invalid_data, false);
911        assert!(result.is_err());
912    }
913
914    #[test]
915    fn test_from_parsing() {
916        let mut email = AnalyzeEmails::default();
917
918        let result = email.from(r#""John Doe" <john@example.com>"#);
919        assert_eq!(result.get("john@example.com").unwrap(), "John Doe");
920
921        let result = email.from(r#"<simple@example.com>"#);
922        assert_eq!(
923            result.get("simple@example.com").unwrap(),
924            "simple@example.com"
925        );
926    }
927
928    #[test]
929    fn test_email_encoded_parsing() {
930        let mut email = AnalyzeEmails::default();
931
932        let result = email.email_encoded(r#"<a@test.com>, <b@test.com>"#);
933        assert!(result.contains_key("a@test.com"));
934        assert!(result.contains_key("b@test.com"));
935    }
936
937    #[test]
938    fn test_analyze_email_with_cc() {
939        let email_data = b"From: sender@example.com\r\n\
940To: receiver@example.com\r\n\
941Cc: cc1@example.com, cc2@example.com\r\n\
942Subject: CC Test\r\n\
943Content-Type: text/plain\r\n\
944Date: 01 Jan 2024 12:00:00 +0000\r\n\
945\r\n\
946Test body"
947            .to_vec();
948
949        let result = AnalyzeEmails::new(email_data, false).unwrap();
950        assert!(result.cc.contains_key("cc1@example.com"));
951        assert!(result.cc.contains_key("cc2@example.com"));
952    }
953
954    #[test]
955    fn test_content_transfer_encoding_decode_binary() {
956        let mut enc = ContentTransferEncoding::Binary;
957        let data = vec![0x00, 0x01, 0x02, 0xFF, 0xFE];
958        let result = enc.decode(data.clone()).unwrap();
959        assert_eq!(result, data);
960    }
961
962    #[test]
963    fn test_content_transfer_encoding_decode_none() {
964        let mut enc = ContentTransferEncoding::None;
965        let data = b"raw data".to_vec();
966        let result = enc.decode(data.clone()).unwrap();
967        assert_eq!(result, data);
968    }
969
970    #[test]
971    fn test_analyze_email_with_reply_to() {
972        let email_data = b"From: sender@example.com\r\n\
973To: receiver@example.com\r\n\
974Reply-To: reply@example.com\r\n\
975Subject: Reply-To Test\r\n\
976Content-Type: text/plain\r\n\
977Date: 01 Jan 2024 12:00:00 +0000\r\n\
978\r\n\
979Test body"
980            .to_vec();
981
982        let result = AnalyzeEmails::new(email_data, false).unwrap();
983        assert!(result.replyto.contains_key("reply@example.com"));
984    }
985
986    #[test]
987    fn test_analyze_email_with_sender() {
988        let email_data = b"From: sender@example.com\r\n\
989Sender: actual-sender@example.com\r\n\
990To: receiver@example.com\r\n\
991Subject: Sender Test\r\n\
992Content-Type: text/plain\r\n\
993Date: 01 Jan 2024 12:00:00 +0000\r\n\
994\r\n\
995Test body"
996            .to_vec();
997
998        let result = AnalyzeEmails::new(email_data, false).unwrap();
999        assert_eq!(result.sender, "actual-sender@example.com");
1000    }
1001
1002    #[test]
1003    fn test_analyze_email_with_mion() {
1004        let email_data = b"From: sender@example.com\r\n\
1005To: receiver@example.com\r\n\
1006MIME-Version: 1.0\r\n\
1007Subject: MIME Test\r\n\
1008Content-Type: text/plain\r\n\
1009Date: 01 Jan 2024 12:00:00 +0000\r\n\
1010\r\n\
1011Test body"
1012            .to_vec();
1013
1014        let result = AnalyzeEmails::new(email_data, false).unwrap();
1015        assert_eq!(result.mime_version, "1.0");
1016    }
1017
1018    #[test]
1019    fn test_analyze_email_lf_only() {
1020        let email_data = b"From: sender@example.com\n\
1021To: receiver@example.com\n\
1022Subject: LF Only Test\n\
1023Content-Type: text/plain\n\
1024Date: 01 Jan 2024 12:00:00 +0000\n\
1025\n\
1026Test body with LF only"
1027            .to_vec();
1028
1029        let result = AnalyzeEmails::new(email_data, false).unwrap();
1030        assert_eq!(result.subject, "LF Only Test");
1031    }
1032
1033    #[test]
1034    fn test_analyze_email_with_custom_header() {
1035        let email_data = b"From: sender@example.com\r\n\
1036To: receiver@example.com\r\n\
1037X-Custom-Header: custom-value\r\n\
1038Subject: Custom Header Test\r\n\
1039Content-Type: text/plain\r\n\
1040Date: 01 Jan 2024 12:00:00 +0000\r\n\
1041\r\n\
1042Test body"
1043            .to_vec();
1044
1045        let result = AnalyzeEmails::new(email_data, false).unwrap();
1046        assert_eq!(
1047            result.header.get("x-custom-header").unwrap(),
1048            "custom-value"
1049        );
1050    }
1051
1052    #[test]
1053    fn test_analyze_email_base64_body() {
1054        let email_data = b"From: sender@example.com\r\n\
1055To: receiver@example.com\r\n\
1056Subject: Base64 Test\r\n\
1057Content-Type: text/plain\r\n\
1058Content-Transfer-Encoding: base64\r\n\
1059Date: 01 Jan 2024 12:00:00 +0000\r\n\
1060\r\n\
1061SGVsbG8gV29ybGQ="
1062            .to_vec();
1063
1064        let result = AnalyzeEmails::new(email_data, false).unwrap();
1065        assert_eq!(result.body_text, "Hello World");
1066    }
1067
1068    #[test]
1069    fn test_from_parsing_simple_email() {
1070        let mut email = AnalyzeEmails::default();
1071        let result = email.from("user@example.com");
1072        assert!(result.contains_key("user@example.com"));
1073    }
1074
1075    #[test]
1076    fn test_email_encoded_with_name() {
1077        let mut email = AnalyzeEmails::default();
1078        let result = email.email_encoded(r#"John Doe <john@example.com>"#);
1079        assert_eq!(result.get("john@example.com").unwrap(), "John Doe");
1080    }
1081
1082    #[test]
1083    fn test_analyze_invalid_email_debug_writes_eml() {
1084        let invalid_data = b"invalid-email-without-separator".to_vec();
1085        let md5 = br_crypto::md5::encrypt_hex(&invalid_data);
1086        let path = env::current_dir().unwrap().join(format!("xygs-{md5}.eml"));
1087        let _ = fs::remove_file(&path);
1088
1089        let result = AnalyzeEmails::new(invalid_data.clone(), true);
1090        assert!(result.is_err());
1091        assert!(path.exists());
1092        assert_eq!(fs::read(&path).unwrap(), invalid_data);
1093
1094        let _ = fs::remove_file(path);
1095    }
1096
1097    #[test]
1098    fn test_header_colon_only_and_empty_value_skip() {
1099        let email_data = b"From: sender@example.com\r\n\
1100To: receiver@example.com\r\n\
1101Subject:Colon Header\r\n\
1102Content-Type:text/plain;charset=\"utf-8\"\r\n\
1103X-No-Space:value-without-space\r\n\
1104X-Empty:\r\n\
1105Date:Mon, 01 Jan 2024 12:00:00 GMT\r\n\
1106\r\n\
1107Body"
1108            .to_vec();
1109
1110        let result = AnalyzeEmails::new(email_data, false).unwrap();
1111        assert_eq!(result.subject, "Colon Header");
1112        assert_eq!(result.charset, "utf-8");
1113        assert_eq!(
1114            result.header.get("x-no-space").unwrap(),
1115            "value-without-space"
1116        );
1117        assert!(!result.header.contains_key("x-empty"));
1118    }
1119
1120    #[test]
1121    fn test_header_line_without_any_colon_is_skipped() {
1122        let email_data = b"From: sender@example.com\r\n\
1123To: receiver@example.com\r\n\
1124no-colon-line-here\r\n\
1125Subject: Test\r\n\
1126Content-Type: text/plain\r\n\
1127Date: 01 Jan 2024 12:00:00 +0000\r\n\
1128\r\n\
1129body"
1130            .to_vec();
1131
1132        let result = AnalyzeEmails::new(email_data, false).unwrap();
1133        assert_eq!(result.subject, "Test");
1134    }
1135
1136    #[test]
1137    fn test_multipart_body_parsing_for_all_supported_types() {
1138        let content_types = [
1139            "multipart/mixed",
1140            "multipart/alternative",
1141            "multipart/related",
1142            "multipart/report",
1143        ];
1144
1145        for content_type in content_types {
1146            let boundary = unique_token("boundary");
1147            let email_data = multipart_email(
1148                content_type,
1149                boundary.as_str(),
1150                "Content-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello multipart body",
1151            );
1152            let result = AnalyzeEmails::new(email_data, false).unwrap();
1153
1154            assert_eq!(result.content_type, content_type);
1155            assert_eq!(result.charset, "utf-8");
1156            assert!(result.body_text.contains("Hello multipart body"));
1157        }
1158    }
1159
1160    #[test]
1161    fn test_parts_header_parse_failure_debug_writes_file() {
1162        let mut email = AnalyzeEmails::default();
1163        email.debug = true;
1164        email.md5 = unique_token("head");
1165        email.files = object! {};
1166
1167        let path = env::current_dir()
1168            .unwrap()
1169            .join(format!("head-{}.eml", email.md5));
1170        let _ = fs::remove_file(&path);
1171
1172        let result = email.parts(
1173            "invalid-part-content".to_string(),
1174            "raw-email-data".to_string(),
1175        );
1176        assert!(result.is_err());
1177        assert!(path.exists());
1178        assert_eq!(fs::read_to_string(&path).unwrap(), "raw-email-data");
1179
1180        let _ = fs::remove_file(path);
1181    }
1182
1183    #[test]
1184    fn test_parts_unknown_header_returns_error() {
1185        let mut email = AnalyzeEmails::default();
1186        email.files = object! {};
1187
1188        let result = email.parts(
1189            "X-Unknown: value\r\n\r\nbody".to_string(),
1190            "raw".to_string(),
1191        );
1192        assert!(result.is_err());
1193        assert!(result
1194            .unwrap_err()
1195            .to_string()
1196            .contains("parts 未知 header 类型"));
1197    }
1198
1199    #[test]
1200    fn test_parts_text_plain_with_name_as_attachment() {
1201        let mut email = AnalyzeEmails::default();
1202        email.charset = "utf-8".to_string();
1203        email.files = object! {};
1204
1205        let filename = format!("{}.txt", unique_token("plain-attachment"));
1206        let part = format!(
1207            "Content-Type:text/plain; name=\"{filename}\"\r\nContent-Transfer-Encoding:base64\r\n\r\nSGVsbG8gQXR0YWNobWVudA=="
1208        );
1209
1210        email.parts(part, "raw".to_string()).unwrap();
1211
1212        let body = b"Hello Attachment".to_vec();
1213        let md5 = br_crypto::md5::encrypt_hex(&body);
1214        let entry = &email.files[md5.as_str()];
1215
1216        assert_eq!(entry["name"].as_str().unwrap(), filename);
1217        assert_eq!(entry["content-type"].as_str().unwrap(), "txt");
1218        let path = entry["file"].as_str().unwrap();
1219        assert_eq!(fs::read(path).unwrap(), body);
1220
1221        let _ = fs::remove_file(path);
1222    }
1223
1224    #[test]
1225    fn test_parts_text_html_with_name_as_attachment() {
1226        let mut email = AnalyzeEmails::default();
1227        email.charset = "utf-8".to_string();
1228        email.files = object! {};
1229
1230        let filename = format!("{}.html", unique_token("html-attachment"));
1231        let part = format!(
1232            "Content-Type: text/html; name=\"{filename}\"\r\nContent-Transfer-Encoding: base64\r\n\r\nPGgxPkhlbGxvIEhUTUwgQXR0YWNobWVudDwvaDE+"
1233        );
1234
1235        email.parts(part, "raw".to_string()).unwrap();
1236
1237        let body = b"<h1>Hello HTML Attachment</h1>".to_vec();
1238        let md5 = br_crypto::md5::encrypt_hex(&body);
1239        let entry = &email.files[md5.as_str()];
1240
1241        assert_eq!(entry["name"].as_str().unwrap(), filename);
1242        assert_eq!(entry["content-type"].as_str().unwrap(), "html");
1243        let path = entry["file"].as_str().unwrap();
1244        assert_eq!(fs::read(path).unwrap(), body);
1245
1246        let _ = fs::remove_file(path);
1247    }
1248
1249    #[test]
1250    fn test_parts_content_disposition_filename() {
1251        let mut email = AnalyzeEmails::default();
1252        email.charset = "utf-8".to_string();
1253        email.files = object! {};
1254
1255        let filename = format!("{}.pdf", unique_token("filename"));
1256        let part = format!(
1257            "Content-Type: application/pdf\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"{filename}\"\r\n\r\nSGVsbG8gUERG"
1258        );
1259
1260        email.parts(part, "raw".to_string()).unwrap();
1261
1262        let body = b"Hello PDF".to_vec();
1263        let md5 = br_crypto::md5::encrypt_hex(&body);
1264        let entry = &email.files[md5.as_str()];
1265
1266        assert_eq!(entry["name"].as_str().unwrap(), filename);
1267        assert_eq!(entry["content-type"].as_str().unwrap(), "application/pdf");
1268        let path = entry["file"].as_str().unwrap();
1269        assert_eq!(fs::read(path).unwrap(), body);
1270
1271        let _ = fs::remove_file(path);
1272    }
1273
1274    #[test]
1275    fn test_parts_content_disposition_filename_utf8_star() {
1276        let mut email = AnalyzeEmails::default();
1277        email.charset = "utf-8".to_string();
1278        email.files = object! {};
1279
1280        let part = "Content-Type: application/octet-stream\r\n\
1281Content-Transfer-Encoding: base64\r\n\
1282Content-Disposition: attachment; filename*=utf-8''hello%20world.txt\r\n\
1283\r\n\
1284SGVsbG8gVVJMIEZpbGU="
1285            .to_string();
1286
1287        email.parts(part, "raw".to_string()).unwrap();
1288
1289        let body = b"Hello URL File".to_vec();
1290        let md5 = br_crypto::md5::encrypt_hex(&body);
1291        let entry = &email.files[md5.as_str()];
1292
1293        assert_eq!(entry["name"].as_str().unwrap(), "hello world.txt");
1294        assert_eq!(
1295            entry["content-type"].as_str().unwrap(),
1296            "application/octet-stream"
1297        );
1298        let path = entry["file"].as_str().unwrap();
1299        assert_eq!(fs::read(path).unwrap(), body);
1300
1301        let _ = fs::remove_file(path);
1302    }
1303
1304    #[test]
1305    fn test_parts_nested_multipart() {
1306        let mut email = AnalyzeEmails::default();
1307        email.charset = "utf-8".to_string();
1308        email.files = object! {};
1309
1310        let boundary = unique_token("inner-boundary");
1311        let part = format!(
1312            "Content-Type: multipart/alternative; boundary=\"{boundary}\"\r\n\
1313Content-Transfer-Encoding: 7bit\r\n\
1314\r\n\
1315--{boundary}\r\n\
1316Content-Type: text/plain\r\n\
1317Content-Transfer-Encoding: 7bit\r\n\
1318\r\n\
1319Nested text body\r\n\
1320--{boundary}--\r\n"
1321        );
1322
1323        email.parts(part, "raw".to_string()).unwrap();
1324        assert!(email.body_text.contains("Nested text body"));
1325    }
1326
1327    #[test]
1328    fn test_parts_text_calendar_is_skipped() {
1329        let mut email = AnalyzeEmails::default();
1330        email.charset = "utf-8".to_string();
1331        email.files = object! {};
1332        email.body_text = "keep-me".to_string();
1333
1334        let part = "Content-Type: text/calendar\r\n\
1335Content-Transfer-Encoding: 7bit\r\n\
1336\r\n\
1337BEGIN:VCALENDAR"
1338            .to_string();
1339
1340        email.parts(part, "raw".to_string()).unwrap();
1341        assert_eq!(email.body_text, "keep-me");
1342    }
1343
1344    #[test]
1345    fn test_parts_application_content_types_are_saved() {
1346        let mut email = AnalyzeEmails::default();
1347        email.charset = "utf-8".to_string();
1348        email.files = object! {};
1349
1350        let content_types = [
1351            "application/octet-stream",
1352            "application/zip",
1353            "application/pdf",
1354            "image/jpeg",
1355            "image/png",
1356            "image/gif",
1357            "application/ics",
1358            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1359            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1360            "application/vnd.ms-excel",
1361        ];
1362        let body = b"Hello File".to_vec();
1363        let md5 = br_crypto::md5::encrypt_hex(&body);
1364
1365        for (idx, content_type) in content_types.iter().enumerate() {
1366            let filename = format!("{}-{idx}.bin", unique_token("app-attachment"));
1367            let part = format!(
1368                "Content-Type: {content_type}; name=\"{filename}\"\r\nContent-Transfer-Encoding: base64\r\n\r\nSGVsbG8gRmlsZQ=="
1369            );
1370
1371            email.parts(part, "raw".to_string()).unwrap();
1372
1373            let entry = &email.files[md5.as_str()];
1374            assert_eq!(entry["name"].as_str().unwrap(), filename);
1375            assert_eq!(entry["content-type"].as_str().unwrap(), *content_type);
1376
1377            let path = entry["file"].as_str().unwrap();
1378            assert_eq!(fs::read(path).unwrap(), body);
1379            let _ = fs::remove_file(path);
1380        }
1381    }
1382
1383    #[test]
1384    fn test_parts_unknown_content_type_debug_writes_file() {
1385        let mut email = AnalyzeEmails::default();
1386        email.debug = true;
1387        email.md5 = unique_token("content-type");
1388        email.charset = "utf-8".to_string();
1389        email.files = object! {};
1390
1391        let path = env::current_dir()
1392            .unwrap()
1393            .join(format!("content_type-{}.eml", email.md5));
1394        let _ = fs::remove_file(&path);
1395
1396        let part = "Content-Type: application/x-custom\r\n\
1397Content-Transfer-Encoding: 7bit\r\n\
1398\r\n\
1399custom body"
1400            .to_string();
1401
1402        email
1403            .parts(part, "raw-unknown-content-type".to_string())
1404            .unwrap();
1405        assert!(path.exists());
1406        assert_eq!(
1407            fs::read_to_string(&path).unwrap(),
1408            "raw-unknown-content-type"
1409        );
1410
1411        let _ = fs::remove_file(path);
1412    }
1413
1414    #[test]
1415    fn test_encoded_line_paths() {
1416        let mut email = AnalyzeEmails::default();
1417
1418        assert_eq!(email.encoded_line("plain text"), "plain text");
1419        assert_eq!(email.encoded_line("=?UTF-8?Q?=48=65=6C=6C=6F?="), "Hello");
1420        assert_eq!(
1421            email.encoded_line("=?UTF-8?X?UnknownEncoding?="),
1422            "UnknownEncoding"
1423        );
1424    }
1425
1426    #[test]
1427    fn test_datetime_parsing_variants_and_error() {
1428        let mut email = AnalyzeEmails::default();
1429
1430        email
1431            .datetime("Mon, 01 Jan 2024 12:00:00 GMT (UTC)")
1432            .unwrap();
1433        assert!(email.timestamp > 0);
1434        assert!(!email.datetime.is_empty());
1435
1436        let err = email.datetime("invalid datetime format").unwrap_err();
1437        assert!(err.to_string().contains("时间解析失败"));
1438    }
1439
1440
1441    #[test]
1442    fn test_datetime_us_style_month_first() {
1443        let mut email = AnalyzeEmails::default();
1444        // 美式日期格式: Mon dd YYYY (如 "Nov 4 2025 22:19:26 +0800")
1445        email
1446            .datetime("Mon, Nov 4 2025 22:19:26 +0800")
1447            .unwrap();
1448        assert!(email.timestamp > 0);
1449        assert!(!email.datetime.is_empty());
1450        // 标准格式仍然正常工作
1451        email
1452            .datetime("Tue, 15 Oct 2024 08:30:00 +0000")
1453            .unwrap();
1454        assert!(email.timestamp > 0);
1455    }
1456    #[test]
1457    fn test_email_encoded_with_encoded_and_quoted_names() {
1458        let mut email = AnalyzeEmails::default();
1459        let result = email.email_encoded(
1460            "\"Quoted User\" <quoted@example.com>, =?UTF-8?B?5rWL6K+V?= <encoded@example.com>",
1461        );
1462
1463        assert_eq!(result.get("quoted@example.com").unwrap(), "Quoted User");
1464        assert_eq!(result.get("encoded@example.com").unwrap(), "测试");
1465    }
1466
1467    #[test]
1468    fn test_set_files_base64_decodes_and_detects_extension() {
1469        let mut email = AnalyzeEmails::default();
1470        email.files = object! {};
1471
1472        let filename = format!("{}.txt", unique_token("set-files"));
1473        email
1474            .set_files(
1475                ContentTransferEncoding::Base64,
1476                "c2V0IGZpbGVzIGJvZHk=\r\n",
1477                filename.as_str(),
1478                "".to_string(),
1479            )
1480            .unwrap();
1481
1482        let body = b"set files body".to_vec();
1483        let md5 = br_crypto::md5::encrypt_hex(&body);
1484        let entry = &email.files[md5.as_str()];
1485
1486        assert_eq!(entry["name"].as_str().unwrap(), filename);
1487        assert_eq!(entry["content-type"].as_str().unwrap(), "txt");
1488        let path = entry["file"].as_str().unwrap();
1489        assert_eq!(fs::read(path).unwrap(), body);
1490
1491        let _ = fs::remove_file(path);
1492    }
1493
1494    #[test]
1495    fn test_header_parses_colon_without_space_separator() {
1496        let email_data = b"From:sender@example.com\r\n\
1497To:receiver@example.com\r\n\
1498X-Test:nospaceval\r\n\
1499Content-Type:text/plain\r\n\
1500Date: 01 Jan 2024 12:00:00 +0000\r\n\
1501\r\n\
1502body"
1503            .to_vec();
1504
1505        let result = AnalyzeEmails::new(email_data, false).unwrap();
1506        assert_eq!(result.header.get("x-test").unwrap(), "nospaceval");
1507    }
1508
1509    #[test]
1510    fn test_multipart_header_without_boundary_keeps_boundary_empty() {
1511        let email_data = b"From: sender@example.com\r\n\
1512To: receiver@example.com\r\n\
1513Subject: Multipart Without Boundary\r\n\
1514Content-Type: multipart/mixed; charset=utf-8\r\n\
1515Date: 01 Jan 2024 12:00:00 +0000\r\n\
1516\r\n\
1517body without multipart markers"
1518            .to_vec();
1519
1520        let result = AnalyzeEmails::new(email_data, false).unwrap();
1521        assert_eq!(result.content_type, "multipart/mixed");
1522        assert!(result.boundary.is_empty());
1523    }
1524
1525    #[test]
1526    fn test_body_multipart_boundary_not_found_uses_original_body() {
1527        let boundary = unique_token("missing-boundary");
1528        let email_data = format!(
1529            "From: sender@example.com\r\n\
1530To: receiver@example.com\r\n\
1531Subject: Boundary Missing In Body\r\n\
1532Content-Type: multipart/mixed;boundary=\"{boundary}\";charset=\"utf-8\"\r\n\
1533Content-Transfer-Encoding: 7bit\r\n\
1534Date: 01 Jan 2024 12:00:00 +0000\r\n\
1535\r\n\
1536this body intentionally has no boundary lines"
1537        )
1538        .into_bytes();
1539
1540        let result = AnalyzeEmails::new(email_data, false).unwrap();
1541        assert_eq!(result.content_type, "multipart/mixed");
1542        assert_eq!(result.boundary, boundary);
1543        assert!(result.body_text.is_empty());
1544        assert!(result.body_html.is_empty());
1545    }
1546
1547    #[test]
1548    fn test_body_multipart_skips_empty_part_segments() {
1549        let boundary = unique_token("empty-part");
1550        let email_data = format!(
1551            "From: sender@example.com\r\n\
1552To: receiver@example.com\r\n\
1553Subject: Empty Multipart Segment\r\n\
1554Content-Type: multipart/mixed; boundary=\"{boundary}\"\r\n\
1555Date: 01 Jan 2024 12:00:00 +0000\r\n\
1556\r\n\
1557--{boundary}\r\n\
1558Content-Type: text/plain\r\n\
1559Content-Transfer-Encoding: 7bit\r\n\
1560\r\n\
1561first text\r\n\
1562--{boundary}\r\n\
1563\r\n\
1564--{boundary}--\r\n"
1565        )
1566        .into_bytes();
1567
1568        let result = AnalyzeEmails::new(email_data, false).unwrap();
1569        assert_eq!(result.content_type, "multipart/mixed");
1570        assert!(result.body_text.contains("first text"));
1571    }
1572
1573    #[test]
1574    fn test_body_unknown_content_type_skips_gracefully() {
1575        let email_data = b"From: a@b.com\r\n\
1576Content-Type: application/json\r\n\
1577Date: 01 Jan 2024 12:00:00 +0000\r\n\
1578\r\n\
1579{\"key\":\"value\"}"
1580            .to_vec();
1581        let result = AnalyzeEmails::new(email_data, false);
1582        assert!(result.is_ok());
1583        let email = result.unwrap();
1584        assert!(email.body_text.is_empty());
1585        assert!(email.body_html.is_empty());
1586    }
1587
1588    #[test]
1589    fn test_parts_ignores_header_line_without_colon() {
1590        let mut email = AnalyzeEmails::default();
1591        email.charset = "utf-8".to_string();
1592        email.files = object! {};
1593
1594        let part = "NoColonHeader\r\n\
1595Content-Type: text/plain\r\n\
1596Content-Transfer-Encoding: 7bit\r\n\
1597\r\n\
1598plain body"
1599            .to_string();
1600
1601        email.parts(part, "raw".to_string()).unwrap();
1602        assert_eq!(email.body_text, "plain body");
1603    }
1604
1605    #[test]
1606    fn test_parts_content_type_boundary_with_semicolon_suffix() {
1607        let mut email = AnalyzeEmails::default();
1608        email.charset = "utf-8".to_string();
1609        email.files = object! {};
1610
1611        let boundary = unique_token("inner-semi");
1612        let part = format!(
1613            "Content-Type: multipart/alternative; boundary=\"{boundary}\"; charset=\"utf-8\"\r\n\
1614Content-Transfer-Encoding: 7bit\r\n\
1615\r\n\
1616--{boundary}\r\n\
1617Content-Type: text/plain\r\n\
1618Content-Transfer-Encoding: 7bit\r\n\
1619\r\n\
1620nested plain body\r\n\
1621--{boundary}--\r\n"
1622        );
1623
1624        email.parts(part, "raw".to_string()).unwrap();
1625        assert!(email.body_text.contains("nested plain body"));
1626    }
1627
1628    #[test]
1629    fn test_parts_text_html_without_filename_sets_body_html() {
1630        let mut email = AnalyzeEmails::default();
1631        email.charset = "utf-8".to_string();
1632        email.files = object! {};
1633
1634        let part = "Content-Type: text/html\r\n\
1635Content-Transfer-Encoding: 7bit\r\n\
1636\r\n\
1637<p>inline html body</p>"
1638            .to_string();
1639
1640        email.parts(part, "raw".to_string()).unwrap();
1641        assert!(email.body_html.contains("<p>inline html body</p>"));
1642    }
1643
1644    #[test]
1645    fn test_parts_nested_multipart_outer_boundary_not_found_skips_empty_part() {
1646        let mut email = AnalyzeEmails::default();
1647        email.charset = "utf-8".to_string();
1648        email.files = object! {};
1649        email.boundary = unique_token("outer-boundary");
1650
1651        let inner_boundary = unique_token("inner-boundary");
1652        let part = format!(
1653            "Content-Type: multipart/alternative; boundary=\"{inner_boundary}\"\r\n\
1654Content-Transfer-Encoding: 7bit\r\n\
1655\r\n\
1656--{inner_boundary}\r\n\
1657Content-Type: text/plain\r\n\
1658Content-Transfer-Encoding: 7bit\r\n\
1659\r\n\
1660nested plain text\r\n\
1661--{inner_boundary}\r\n\
1662\r\n\
1663--{inner_boundary}--\r\n"
1664        );
1665
1666        email.parts(part, "raw".to_string()).unwrap();
1667        assert!(email.body_text.contains("nested plain text"));
1668    }
1669
1670    #[test]
1671    fn test_set_files_returns_error_when_create_fails() {
1672        let mut email = AnalyzeEmails::default();
1673        email.files = object! {};
1674
1675        let missing_parent = unique_token("missing-parent");
1676        let filename = format!("{missing_parent}/file.txt");
1677
1678        let err = email
1679            .set_files(
1680                ContentTransferEncoding::Bit7,
1681                "ignored",
1682                filename.as_str(),
1683                "application/octet-stream".to_string(),
1684            )
1685            .unwrap_err();
1686
1687        assert!(err.to_string().contains("打开(创建)临时文件"));
1688        assert!(err.to_string().contains(filename.as_str()));
1689    }
1690
1691    #[test]
1692    fn test_content_transfer_encoding_decode_quoted_printable() {
1693        let mut enc = ContentTransferEncoding::QuotedPrintable;
1694        let data = b"Hello=20World=21".to_vec();
1695        let result = enc.decode(data).unwrap();
1696        assert_eq!(result, b"Hello World!");
1697    }
1698}