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