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