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