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