Skip to main content

eml_parser/
parser.rs

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,     // Found a line feed (without first seeing a carriage return). EOLs are messed up.
13    CR,     // Found a carriage return
14    CRLF,   // Found a carriage return followed by a line feed
15    CRLFCR, // Found a CRLF followed by a new CR
16    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
45/// For matching: `"John Smith" <jsmith@example.com>` or `John Smith <jsmith@example.com>`
46///
47/// According to [RFC 5322 Section 3.2.5](https://www.rfc-editor.org/info/rfc5322/#section-3.2.5), it appears that
48/// the display name may be quoted or unquoted.
49///
50/// See [Issue 17](https://github.com/aeshirey/EmlParser/issues/17) / [PR 18](https://github.com/aeshirey/EmlParser/pull/18/)
51static NAME_ADDR_RE: LazyLock<Regex> =
52    LazyLock::new(|| Regex::new(r#"^(?:"([^"]+?)"|([^"]+?)) <\s*([^>]+)\s*>[ ,]*"#).unwrap());
53
54/// for matching the email in brackets without name: <jsmith@example.com>
55static ADDR_RE1: LazyLock<Regex> =
56    LazyLock::new(|| Regex::new(r#"^\s*<\s*([^>]+)\s*>[ ,]*"#).unwrap());
57
58/// for matching the email without brackets: jsmith@example.com
59static ADDR_RE2: LazyLock<Regex> =
60    LazyLock::new(|| Regex::new(r#"^\s*([^"<>@]+@[^"<>@\s,]+)[ ,]*"#).unwrap());
61
62impl EmlParser {
63    /// Read an .eml file from disk, parsing its contents.
64    /// Note that the current implementation loads the entire file to memory since `std::fs::File`
65    /// doesn't provide an iterator over `char` that could give a `Peekable`.
66    // One possible TODO is rolling something like https://github.com/C4K3/peekable-reader-rs into
67    // this project.
68    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    // Builder pattern methods
87    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(); // making a copy so we can have a mutable reference
108        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            // Attempt to structure this header value
149            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        // Email address header values can span multiple lines. Clean those up first
173        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                // Something weird
201                return HeaderFieldValue::Unstructured(value);
202            }
203        }
204
205        //match found_addresses {
206        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), // finding a CR or LF when looking for a header means the body is about to start
219            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    // 1*<any CHAR, excluding CTLs, SPACE, and ":">
270    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                // we shouldn't see CR or LF in a field name; if we do, it's likely the end of the
280                // header
281                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    /// Read until we've found a CRLF that does NOT have white whitespace after it.
303    ///
304    /// On success, this returns the body and a bool indicating end-of-header.
305    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                    // While reading input, anything not CR or LF gets included
321                    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                    // A newline followed by whitespace is the definition of linear whitespace.
341                    // This is an input that spans multiple lines; for example:
342                    //       X-Received: by 0000:111:222e:: with SMTP id abcdef;
343                    //               Mon, 13 Apr 2020 14:04:07 -0700 (PDT)
344                    // In this case, we return to the ReadingContent state
345                    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                    // A newline followed by non-whitespace means we're at the end of this
354                    // header item.
355                    break;
356                }
357
358                (LwspState::LF, InputType::LF) => {
359                    // Found the end of the header in the form of LF + LF
360                    state = LwspState::EndOfHeader_LFLF;
361                    char_input.next();
362                    end_position += len;
363                    break;
364                }
365                (LwspState::CR, InputType::CR) => {
366                    // Found the end of the header in the form of CR + CR
367                    state = LwspState::EndOfHeader_CRCR;
368                    char_input.next();
369                    end_position += len;
370                    break;
371                }
372                (LwspState::CRLFCR, InputType::LF) => {
373                    // Found the end of the header in the form of CRLF + CRLF
374                    state = LwspState::EndOfHeader_CRLFCRLF;
375                    char_input.next();
376                    end_position += len;
377                    break;
378                }
379
380                (LwspState::CR, InputType::LF) => {
381                    // CR+LF will probably lead to CRLF+CRLF
382                    state = LwspState::CRLF;
383                    char_input.next();
384                    end_position += len;
385                }
386
387                (LwspState::CRLF, InputType::CR) => {
388                    // Approaching CRLF+CRLF
389                    state = LwspState::CRLFCR;
390                    char_input.next();
391                    end_position += len;
392                }
393
394                // Rather strict handling of line endings when we're at the border of the header
395                // and body. According to RFC0822, the body "is separated from the headers by a
396                // null line (i.e., a line with nothing preceding the CRLF)."
397                // In reality, we'll see "\n\n" or possibly even "\r\r" separating lines.
398                // It's not unreasonable to think that we'd see other unusual input such as
399                // "\r\n\n" separating the header from the body, but for now, I'm only accepting
400                // "\r\r", "\n\n", and "\r\n\r\n". The following situations are all erroneous:
401                // We should really just see
402                (LwspState::CRLFCR, _) => {
403                    // CRLF + CR shouldn't be followed by anything but a line feed.
404                    return Err(EmlError::UnexpectedContent(String::from(
405                        "Found CRLF+CR in header without expected LF",
406                    )));
407                }
408
409                (LwspState::CRLF, InputType::LF) => {
410                    // CRLF should have had an additional CR before the LF.
411                    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                    // LF after non-breaking character should be followed by another LF, not CR.
418                    return Err(EmlError::UnexpectedContent(String::from(
419                        "Found LF+CR in header as line delimeter",
420                    )));
421                }
422
423                // These match arms won't be hit because we only set the state above before breaking from the loop
424                (LwspState::EndOfHeader_LFLF, _)
425                | (LwspState::EndOfHeader_CRCR, _)
426                | (LwspState::EndOfHeader_CRLFCRLF, _) => unreachable!(),
427            }
428        }
429
430        self.position = end_position;
431
432        // Depending on the state (end of individual header value or the full thing, type of line ending), the return value
433        // has a different end position
434        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            // According to RFC0822, linear whitespace is CRLF + (space or tab).
464            // There's no clear indication how a form feed (0xC, dec 12) should play into this.
465            // Hopefully this isn't an issue, but I am explicitly defining non-CR, non-LF
466            // whitespace as being no different than space and tab.
467            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(); // Result
571
572        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(); // Result
583
584        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        // The same email twice, but once uses the quoted version ("John Q. Public")
618        // and the other uses unquoted (John Q. Public), both of which are allowed.
619        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        // The same email twice, but once uses the quoted version ("John Q. Public")
645        // and the other uses unquoted (John Q. Public), both of which are allowed.
646        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    /// See https://github.com/aeshirey/EmlParser/issues/14
715    #[test]
716    fn nonascii() {
717        // This previously gave a incorrect results of "tés" and " ba"
718        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        // This previously crashed due to the letter + diacritic being at the end of a header value
735        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        // This previously crashed due to the letter + diacritic being at the end of a header name
752        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}