1use std::{fs, iter::Peekable, path::Path, sync::LazyLock};
2
3use regex::Regex;
4
5use crate::eml::*;
6use crate::errors::EmlError;
7
8#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
9#[derive(Debug)]
10enum LwspState {
11 ReadingContent,
12 LF, CR, CRLF, CRLFCR, EndOfHeader_LFLF,
17 EndOfHeader_CRCR,
18 EndOfHeader_CRLFCRLF,
19}
20
21#[allow(clippy::upper_case_acronyms)]
22#[derive(Debug)]
23enum InputType {
24 CR,
25 LF,
26 WSP,
27 NonWsp,
28}
29
30#[derive(Debug)]
31enum BodyHandling {
32 None,
33 Preview(usize),
34 All,
35}
36
37#[derive(Debug)]
38pub struct EmlParser {
39 content: String,
40 position: usize,
41
42 body_handling: BodyHandling,
43}
44
45static NAME_ADDR_RE: LazyLock<Regex> =
52 LazyLock::new(|| Regex::new(r#"^(?:"([^"]+?)"|([^"]+?)) <\s*([^>]+)\s*>[ ,]*"#).unwrap());
53
54static ADDR_RE1: LazyLock<Regex> =
56 LazyLock::new(|| Regex::new(r#"^\s*<\s*([^>]+)\s*>[ ,]*"#).unwrap());
57
58static ADDR_RE2: LazyLock<Regex> =
60 LazyLock::new(|| Regex::new(r#"^\s*([^"<>@]+@[^"<>@\s,]+)[ ,]*"#).unwrap());
61
62impl EmlParser {
63 pub fn from_file(filename: impl AsRef<Path>) -> Result<Self, EmlError> {
69 let content = fs::read_to_string(filename)?;
70
71 Ok(EmlParser {
72 content,
73 position: 0,
74 body_handling: BodyHandling::All,
75 })
76 }
77
78 pub fn from_string(content: String) -> Self {
79 EmlParser {
80 content,
81 position: 0,
82 body_handling: BodyHandling::All,
83 }
84 }
85
86 pub fn ignore_body(mut self) -> Self {
88 self.body_handling = BodyHandling::None;
89 self
90 }
91
92 pub fn with_body(mut self) -> Self {
93 self.body_handling = BodyHandling::All;
94 self
95 }
96
97 pub fn with_body_preview(mut self, bytes: usize) -> Self {
98 self.body_handling = BodyHandling::Preview(bytes);
99 self
100 }
101
102 pub fn parse(&mut self) -> Result<Eml, EmlError> {
103 if self.content.is_empty() {
104 return Err(EmlError::UnexpectedEndOfStream(String::from("Empty input")));
105 }
106
107 let content = self.content.to_string(); let chars = content.chars();
109 let mut char_input = chars.peekable();
110 let eml = self.parse_email(&mut char_input)?;
111
112 Ok(eml)
113 }
114
115 fn parse_email<T: Iterator<Item = char>>(
116 &mut self,
117 char_input: &mut Peekable<T>,
118 ) -> Result<Eml, EmlError> {
119 let headers = self.parse_header_fields(char_input)?;
120
121 let mut result = Eml {
122 body: self.parse_body(),
123 ..Default::default()
124 };
125
126 for header in headers {
127 match (&header.name[..], &header.value) {
128 ("To", _) => result.to = Some(header.value),
129 ("From", _) => result.from = Some(header.value),
130 ("Subject", HeaderFieldValue::Unstructured(subj)) => {
131 result.subject = Some((*subj).to_string())
132 }
133 _ => result.headers.push(header),
134 }
135 }
136
137 Ok(result)
138 }
139
140 fn parse_header_fields<T: Iterator<Item = char>>(
141 &mut self,
142 char_input: &mut Peekable<T>,
143 ) -> Result<Vec<HeaderField>, EmlError> {
144 use HeaderFieldValue::*;
145 let mut headers = Vec::new();
146
147 while let Some((name, value, eoh)) = self.read_raw_header_field(char_input)? {
148 let value = match (&name[..], value) {
150 ("From", v)
151 | ("To", v)
152 | ("Reply-To", v)
153 | ("Delivered-To", v)
154 | ("X-Original-To", v)
155 | ("Return-Path", v) => EmlParser::parse_email_address(v),
156 (_, v) if v.is_empty() => Empty,
157 (_, v) => match rfc2047_decoder::decode(&v) {
158 Ok(decoded) => Unstructured(decoded),
159 Err(_) => Unstructured(v),
160 },
161 };
162 headers.push(HeaderField { name, value });
163
164 if eoh {
165 break;
166 }
167 }
168 Ok(headers)
169 }
170
171 fn parse_email_address(value: String) -> HeaderFieldValue {
172 let mut remaining = value.replace(['\n', '\r'], "");
174
175 let mut found_addresses = Vec::new();
176
177 while !remaining.is_empty() {
178 if let Some(cap) = NAME_ADDR_RE.captures(&remaining) {
179 let name_match = cap.get(1).unwrap_or_else(|| cap.get(2).unwrap());
180 let name = name_match.as_str().to_string();
181
182 let address = cap.get(3).unwrap().as_str().to_string();
183 found_addresses.push(EmailAddress::NameAndEmailAddress { name, address });
184
185 let entire_match = cap.get(0).unwrap();
186 remaining = remaining[entire_match.end()..].to_string();
187 } else if let Some(cap) = ADDR_RE1.captures(&remaining) {
188 let address = cap.get(1).unwrap().as_str().to_string();
189 found_addresses.push(EmailAddress::AddressOnly { address });
190
191 let entire_match = cap.get(0).unwrap();
192 remaining = remaining[entire_match.end()..].to_string();
193 } else if let Some(cap) = ADDR_RE2.captures(&remaining) {
194 let address = cap.get(1).unwrap().as_str().to_string();
195 found_addresses.push(EmailAddress::AddressOnly { address });
196
197 let entire_match = cap.get(0).unwrap();
198 remaining = remaining[entire_match.end()..].to_string();
199 } else {
200 return HeaderFieldValue::Unstructured(value);
202 }
203 }
204
205 if found_addresses.len() == 1 {
207 HeaderFieldValue::SingleEmailAddress(found_addresses.into_iter().next().unwrap())
208 } else {
209 HeaderFieldValue::MultipleEmailAddresses(found_addresses)
210 }
211 }
212
213 fn read_raw_header_field<T: Iterator<Item = char>>(
214 &mut self,
215 char_input: &mut Peekable<T>,
216 ) -> Result<Option<(String, String, bool)>, EmlError> {
217 match char_input.peek() {
218 Some('\n') | Some('\r') => return Ok(None), Some(_) => {}
220 None => {
221 return Err(EmlError::UnexpectedEndOfStream(String::from(
222 "Expected the beginning of a header field name",
223 )))
224 }
225 };
226
227 if let Some(name) = self.read_field_name(char_input)? {
228 match char_input.peek() {
229 Some(':') => {
230 self.position += 1;
231 char_input.next();
232 }
233 Some(c) => {
234 return Err(EmlError::UnexpectedContent(format!(
235 "Expected ':' to terminate header field '{}'; got '{}' (byte value {})",
236 name, c, *c as u8
237 )))
238 }
239 None => {
240 return Err(EmlError::UnexpectedEndOfStream(format!(
241 "Expected ':' to terminate header field '{}'",
242 name
243 )))
244 }
245 };
246
247 match char_input.peek() {
248 Some(' ') => {
249 self.position += 1;
250 char_input.next();
251 }
252 Some(_) => {}
253 None => {
254 return Err(EmlError::UnexpectedEndOfStream(format!(
255 "Expected non-empty content for header field '{}'",
256 name
257 )))
258 }
259 };
260
261 let (value, eoh) = self.read_field_body(char_input)?;
262
263 Ok(Some((name, value, eoh)))
264 } else {
265 Ok(None)
266 }
267 }
268
269 fn read_field_name<T: Iterator<Item = char>>(
271 &mut self,
272 char_input: &mut Peekable<T>,
273 ) -> Result<Option<String>, EmlError> {
274 let start_position = self.position;
275 let mut end_position = self.position;
276
277 while let Some(c) = char_input.peek() {
278 if c == &'\n' || c == &'\r' {
279 return Ok(None);
282 } else if c != &' ' && c != &':' && !c.is_control() {
283 end_position += c.len_utf8();
284 char_input.next();
285 } else {
286 break;
287 }
288 }
289
290 if end_position == self.content.len() {
291 Err(EmlError::UnexpectedEndOfStream(String::from(
292 "Expected content for header field",
293 )))
294 } else {
295 self.position = end_position;
296 Ok(Some(String::from(
297 &self.content[start_position..end_position],
298 )))
299 }
300 }
301
302 fn read_field_body<T: Iterator<Item = char>>(
306 &mut self,
307 char_input: &mut Peekable<T>,
308 ) -> Result<(String, bool), EmlError> {
309 let start_position = self.position;
310 let mut end_position = self.position;
311 let mut state = LwspState::ReadingContent;
312
313 while let Some(next_char) = char_input.peek() {
314 let ws = EmlParser::next_char_type(*next_char);
315 let len = next_char.len_utf8();
316
317 match (&state, ws) {
318 (LwspState::ReadingContent, InputType::WSP)
319 | (LwspState::ReadingContent, InputType::NonWsp) => {
320 char_input.next();
322 end_position += len;
323 }
324
325 (LwspState::ReadingContent, InputType::CR) => {
326 state = LwspState::CR;
327 char_input.next();
328 end_position += len;
329 }
330
331 (LwspState::ReadingContent, InputType::LF) => {
332 state = LwspState::LF;
333 char_input.next();
334 end_position += len;
335 }
336
337 (LwspState::LF, InputType::WSP)
338 | (LwspState::CR, InputType::WSP)
339 | (LwspState::CRLF, InputType::WSP) => {
340 state = LwspState::ReadingContent;
346 char_input.next();
347 end_position += len;
348 }
349
350 (LwspState::LF, InputType::NonWsp)
351 | (LwspState::CR, InputType::NonWsp)
352 | (LwspState::CRLF, InputType::NonWsp) => {
353 break;
356 }
357
358 (LwspState::LF, InputType::LF) => {
359 state = LwspState::EndOfHeader_LFLF;
361 char_input.next();
362 end_position += len;
363 break;
364 }
365 (LwspState::CR, InputType::CR) => {
366 state = LwspState::EndOfHeader_CRCR;
368 char_input.next();
369 end_position += len;
370 break;
371 }
372 (LwspState::CRLFCR, InputType::LF) => {
373 state = LwspState::EndOfHeader_CRLFCRLF;
375 char_input.next();
376 end_position += len;
377 break;
378 }
379
380 (LwspState::CR, InputType::LF) => {
381 state = LwspState::CRLF;
383 char_input.next();
384 end_position += len;
385 }
386
387 (LwspState::CRLF, InputType::CR) => {
388 state = LwspState::CRLFCR;
390 char_input.next();
391 end_position += len;
392 }
393
394 (LwspState::CRLFCR, _) => {
403 return Err(EmlError::UnexpectedContent(String::from(
405 "Found CRLF+CR in header without expected LF",
406 )));
407 }
408
409 (LwspState::CRLF, InputType::LF) => {
410 return Err(EmlError::UnexpectedContent(String::from(
412 "Found CRLF+LF in header without expected CR first",
413 )));
414 }
415
416 (LwspState::LF, InputType::CR) => {
417 return Err(EmlError::UnexpectedContent(String::from(
419 "Found LF+CR in header as line delimeter",
420 )));
421 }
422
423 (LwspState::EndOfHeader_LFLF, _)
425 | (LwspState::EndOfHeader_CRCR, _)
426 | (LwspState::EndOfHeader_CRLFCRLF, _) => unreachable!(),
427 }
428 }
429
430 self.position = end_position;
431
432 let value_end = end_position
435 - match state {
436 LwspState::LF => 1,
437 LwspState::CR => 1,
438 LwspState::CRLF => 2,
439 LwspState::EndOfHeader_LFLF => 2,
440 LwspState::EndOfHeader_CRCR => 2,
441 LwspState::EndOfHeader_CRLFCRLF => 4,
442 LwspState::ReadingContent | LwspState::CRLFCR => unreachable!(),
443 };
444
445 let end_of_header = matches!(
446 state,
447 LwspState::EndOfHeader_LFLF
448 | LwspState::EndOfHeader_CRCR
449 | LwspState::EndOfHeader_CRLFCRLF
450 );
451
452 Ok((
453 String::from(&self.content[start_position..value_end]),
454 end_of_header,
455 ))
456 }
457
458 fn next_char_type(c: char) -> InputType {
459 match c {
460 '\n' => InputType::LF,
461 '\r' => InputType::CR,
462 ' ' | '\t' => InputType::WSP,
463 c if c.is_ascii_whitespace() => InputType::WSP,
468 _ => InputType::NonWsp,
469 }
470 }
471
472 fn parse_body(&mut self) -> Option<String> {
473 match self.body_handling {
474 BodyHandling::None => None,
475 BodyHandling::Preview(bytes) => {
476 let bytes_remaining = self.content.len() - self.position;
477 let bytes = bytes.min(bytes_remaining);
478
479 Some(String::from(
480 &self.content
481 [self.position..self.content.floor_char_boundary(self.position + bytes)],
482 ))
483 }
484 BodyHandling::All => Some(String::from(&self.content[self.position..])),
485 }
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492
493 const TEST_HEADER: &str = r#"Delivered-To: john.public@example.com
494Received: by 2002:ac9:700e:0:0:0:0:0 with SMTP id w14csp4493771ocr;
495 Mon, 13 Apr 2020 14:04:07 -0700 (PDT)
496X-Google-Smtp-Source: APiQypIbRnWumT0t4TOJHlvDOVkxfqZ8A8HBzdR39kgdjVQQfKUsY/DkKFeZI53Ux1Z3reMRqaCl
497X-Received: by 2002:a37:aa8e:: with SMTP id t136mr9744838qke.175.1586811847065;
498 Mon, 13 Apr 2020 14:04:07 -0700 (PDT)
499foo: bar
500
501This is the start of the body
502"#;
503
504 #[test]
505 fn basic_test() {
506 let eml = EmlParser::from_string(TEST_HEADER.to_string())
507 .with_body()
508 .parse();
509
510 assert!(eml.is_ok());
511 let eml = eml.unwrap();
512
513 assert_eq!(5, eml.headers.len());
514
515 let delivered_to: &HeaderField = &eml.headers[0];
516 assert_eq!("Delivered-To", delivered_to.name);
517 assert_eq!(
518 HeaderFieldValue::SingleEmailAddress(EmailAddress::AddressOnly {
519 address: ("john.public@example.com".to_string())
520 }),
521 delivered_to.value
522 );
523
524 let received: &HeaderField = &eml.headers[1];
525 assert_eq!("Received", received.name);
526 assert_eq!(
527 HeaderFieldValue::Unstructured(
528 r#"by 2002:ac9:700e:0:0:0:0:0 with SMTP id w14csp4493771ocr;
529 Mon, 13 Apr 2020 14:04:07 -0700 (PDT)"#
530 .to_string()
531 ),
532 received.value
533 );
534
535 assert_eq!("X-Google-Smtp-Source".to_string(), eml.headers[2].name);
536 assert_eq!(
537 HeaderFieldValue::Unstructured(
538 "APiQypIbRnWumT0t4TOJHlvDOVkxfqZ8A8HBzdR39kgdjVQQfKUsY/DkKFeZI53Ux1Z3reMRqaCl"
539 .to_string()
540 ),
541 eml.headers[2].value
542 );
543
544 assert_eq!("X-Received".to_string(), eml.headers[3].name);
545 assert_eq!(
546 HeaderFieldValue::Unstructured(
547 r#"by 2002:a37:aa8e:: with SMTP id t136mr9744838qke.175.1586811847065;
548 Mon, 13 Apr 2020 14:04:07 -0700 (PDT)"#
549 .to_string()
550 ),
551 eml.headers[3].value
552 );
553
554 assert_eq!("foo".to_string(), eml.headers[4].name);
555 assert_eq!(
556 HeaderFieldValue::Unstructured("bar".to_string()),
557 eml.headers[4].value
558 );
559
560 assert!(eml.body.is_some());
561 let body = eml.body.unwrap();
562 assert_eq!("This is the start of the body\n", body);
563 }
564
565 #[test]
566 fn basic_test_with_truncated_body() {
567 let eml: Eml = EmlParser::from_string(TEST_HEADER.to_string())
568 .with_body_preview(15)
569 .parse()
570 .unwrap(); let body = eml.body.unwrap();
573 let expected = &"This is the start of the body\n"[0..15];
574 assert_eq!(expected, body);
575 }
576
577 #[test]
578 fn basic_test_with_truncation_gt_body_length() {
579 let eml: Eml = EmlParser::from_string(TEST_HEADER.to_string())
580 .with_body_preview(150)
581 .parse()
582 .unwrap(); assert_eq!(5, eml.headers.len());
585
586 let body = eml.body.unwrap();
587 assert_eq!("This is the start of the body\n", body);
588 }
589
590 #[test]
591 fn body_truncated_in_multibyte_char() {
592 let result = EmlParser::from_string("Foo: ok\n\nBá".to_string())
593 .with_body_preview(2)
594 .parse()
595 .unwrap();
596
597 let body = result.body.unwrap();
598 assert_eq!("B", body);
599 }
600
601 #[test]
602 fn parse_emails() {
603 let parsed =
604 EmlParser::parse_email_address(r#""John Smith" <jsmith@example.com>"#.to_string());
605
606 let jsmith = EmailAddress::NameAndEmailAddress {
607 name: "John Smith".to_string(),
608 address: "jsmith@example.com".to_string(),
609 };
610 let expected = HeaderFieldValue::SingleEmailAddress(jsmith);
611
612 assert_eq!(parsed, expected);
613 }
614
615 #[test]
616 fn parse_and_display_emails() {
617 let singles = [
620 r#""John Q. Public" < john@example.com>, "#.to_string(),
621 r#"John Q. Public < john@example.com>, "#.to_string(),
622 ];
623
624 for single in singles {
625 let parsed = EmlParser::parse_email_address(single);
626
627 match &parsed {
628 HeaderFieldValue::SingleEmailAddress(EmailAddress::NameAndEmailAddress {
629 name,
630 address,
631 }) => {
632 assert_eq!(name, "John Q. Public");
633 assert_eq!(address, "john@example.com");
634 }
635 _ => panic!("Expected SingleEmailAddress, got something else"),
636 };
637
638 assert_eq!(parsed.to_string(), r#""John Q. Public" <john@example.com>"#);
639 }
640 }
641
642 #[test]
643 fn quoted_display_must_be_balanced() {
644 let singles = [
647 r#"John Q. Public" < john@example.com>, "#.to_string(),
648 r#""John Q. Public < john@example.com>, "#.to_string(),
649 ];
650
651 for single in singles {
652 let parsed = EmlParser::parse_email_address(single);
653
654 if !matches!(parsed, HeaderFieldValue::Unstructured(_)) {
655 panic!("Expected Unstructured, got something else");
656 }
657 }
658 }
659
660 #[test]
661 fn test_errors() {
662 let filename = "nonexistent.eml";
663 let parsed = EmlParser::from_file(filename);
664 assert!(parsed.is_err());
665
666 let errval = parsed.unwrap_err();
667 assert!(matches!(errval, EmlError::IoError(_inner)));
668 }
669
670 #[test]
671 fn last_header_empty() {
672 let eml: Eml = EmlParser::from_string("Foo: ok\nBar: \n\nHello".to_string())
673 .with_body()
674 .parse()
675 .unwrap();
676
677 assert_eq!(2, eml.headers.len());
678
679 let foo = &eml.headers[0];
680 let HeaderField { name, value } = foo;
681 assert_eq!("Foo", name);
682 assert_eq!(&HeaderFieldValue::Unstructured("ok".to_string()), value);
683
684 let bar = &eml.headers[1];
685 let HeaderField { name, value } = bar;
686 assert_eq!("Bar", name);
687 assert_eq!(&HeaderFieldValue::Empty, value);
688
689 assert_eq!(Some("Hello".to_string()), eml.body);
690 }
691
692 #[test]
693 fn last_header_get_full_value() {
694 let eml: Eml = EmlParser::from_string("Foo: ok\nBar: super\n\nHello".to_string())
695 .with_body()
696 .parse()
697 .unwrap();
698
699 assert_eq!(2, eml.headers.len());
700
701 let foo = &eml.headers[0];
702 let HeaderField { name, value } = foo;
703 assert_eq!("Foo", name);
704 assert_eq!(&HeaderFieldValue::Unstructured("ok".to_string()), value);
705
706 let bar = &eml.headers[1];
707 let HeaderField { name, value } = bar;
708 assert_eq!("Bar", name);
709 assert_eq!(&HeaderFieldValue::Unstructured("super".to_string()), value);
710
711 assert_eq!(Some("Hello".to_string()), eml.body);
712 }
713
714 #[test]
716 fn nonascii() {
717 let result = EmlParser::from_string("Foo: tést\nBar: bar\n\nHello".to_string())
719 .ignore_body()
720 .parse()
721 .expect("Should parse");
722
723 let headers = result.headers;
724 assert_eq!(2, headers.len());
725
726 let HeaderField { name, value } = &headers[0];
727 assert_eq!("Foo", name);
728 assert_eq!("tést", value.to_string());
729
730 let HeaderField { name, value } = &headers[1];
731 assert_eq!("Bar", name);
732 assert_eq!("bar", value.to_string());
733
734 let result = EmlParser::from_string("Foo: testé\nBar: bar\n\nHello".to_string())
736 .ignore_body()
737 .parse()
738 .expect("Should parse");
739
740 let headers = result.headers;
741 assert_eq!(2, headers.len());
742
743 let HeaderField { name, value } = &headers[0];
744 assert_eq!("Foo", name);
745 assert_eq!("testé", value.to_string());
746
747 let HeaderField { name, value } = &headers[1];
748 assert_eq!("Bar", name);
749 assert_eq!("bar", value.to_string());
750
751 let result = EmlParser::from_string("ō: test\n\n".to_string())
753 .ignore_body()
754 .parse()
755 .unwrap();
756
757 let headers = result.headers;
758 assert_eq!(1, headers.len());
759
760 let HeaderField { name, value } = &headers[0];
761 assert_eq!("ō", name);
762 assert_eq!("test", value.to_string());
763 }
764
765 #[test]
766 fn test_parse_phishing_emails() {
767 for n in 0..10 {
768 let filename = format!("test_emails/{n}.eml");
769
770 let mut e = EmlParser::from_file(&filename).expect("Load file");
771 let _parsed = e.parse().expect("Parse file");
772 }
773 }
774
775 #[test]
776 fn test_parse_rfc2047() {
777 let mut e = EmlParser::from_file("test_emails/rfc2047.eml").unwrap();
778 let parsed = e.parse().expect("Parse rfc2047.eml");
779 let schöne = HeaderFieldValue::Unstructured("Schöne Grüße".to_string());
780
781 for h in parsed.headers {
782 if h.name == "Salutation" {
783 assert_eq!(h.value, schöne);
784 }
785 }
786 }
787}