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