mailparse/
lib.rs

1#![forbid(unsafe_code)]
2
3extern crate charset;
4extern crate data_encoding;
5extern crate quoted_printable;
6
7use std::borrow::Cow;
8use std::collections::{BTreeMap, HashMap};
9use std::error;
10use std::fmt;
11
12use charset::{decode_latin1, Charset};
13
14mod addrparse;
15pub mod body;
16mod dateparse;
17mod header;
18pub mod headers;
19mod msgidparse;
20
21pub use crate::addrparse::{
22    addrparse, addrparse_header, GroupInfo, MailAddr, MailAddrList, SingleInfo,
23};
24use crate::body::Body;
25pub use crate::dateparse::dateparse;
26use crate::header::HeaderToken;
27use crate::headers::Headers;
28pub use crate::msgidparse::{msgidparse, MessageIdList};
29
30/// An error type that represents the different kinds of errors that may be
31/// encountered during message parsing.
32#[derive(Debug)]
33pub enum MailParseError {
34    /// Data that was specified as being in the quoted-printable transfer-encoding
35    /// could not be successfully decoded as quoted-printable data.
36    QuotedPrintableDecodeError(quoted_printable::QuotedPrintableError),
37    /// Data that was specified as being in the base64 transfer-encoding could
38    /// not be successfully decoded as base64 data.
39    Base64DecodeError(data_encoding::DecodeError),
40    /// An error occurred when converting the raw byte data to Rust UTF-8 string
41    /// format using the charset specified in the message.
42    EncodingError(std::borrow::Cow<'static, str>),
43    /// Some other error occurred while parsing the message; the description string
44    /// provides additional details.
45    Generic(&'static str),
46}
47
48impl fmt::Display for MailParseError {
49    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50        match *self {
51            MailParseError::QuotedPrintableDecodeError(ref err) => {
52                write!(f, "QuotedPrintable decode error: {}", err)
53            }
54            MailParseError::Base64DecodeError(ref err) => write!(f, "Base64 decode error: {}", err),
55            MailParseError::EncodingError(ref err) => write!(f, "Encoding error: {}", err),
56            MailParseError::Generic(ref description) => write!(f, "{}", description),
57        }
58    }
59}
60
61impl error::Error for MailParseError {
62    fn cause(&self) -> Option<&dyn error::Error> {
63        match *self {
64            MailParseError::QuotedPrintableDecodeError(ref err) => Some(err),
65            MailParseError::Base64DecodeError(ref err) => Some(err),
66            _ => None,
67        }
68    }
69
70    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
71        match *self {
72            MailParseError::QuotedPrintableDecodeError(ref err) => Some(err),
73            MailParseError::Base64DecodeError(ref err) => Some(err),
74            _ => None,
75        }
76    }
77}
78
79impl From<quoted_printable::QuotedPrintableError> for MailParseError {
80    fn from(err: quoted_printable::QuotedPrintableError) -> MailParseError {
81        MailParseError::QuotedPrintableDecodeError(err)
82    }
83}
84
85impl From<data_encoding::DecodeError> for MailParseError {
86    fn from(err: data_encoding::DecodeError) -> MailParseError {
87        MailParseError::Base64DecodeError(err)
88    }
89}
90
91impl From<std::borrow::Cow<'static, str>> for MailParseError {
92    fn from(err: std::borrow::Cow<'static, str>) -> MailParseError {
93        MailParseError::EncodingError(err)
94    }
95}
96
97/// A struct that represents a single header in the message.
98/// It holds slices into the raw byte array passed to parse_mail, and so the
99/// lifetime of this struct must be contained within the lifetime of the raw
100/// input. There are additional accessor functions on this struct to extract
101/// the data as Rust strings.
102pub struct MailHeader<'a> {
103    key: &'a [u8],
104    value: &'a [u8],
105}
106
107/// Custom Debug trait for better formatting and printing of MailHeader items.
108impl<'a> fmt::Debug for MailHeader<'a> {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.debug_struct("MailHeader")
111            .field("key", &String::from_utf8_lossy(self.key))
112            .field("value", &String::from_utf8_lossy(self.value))
113            .finish()
114    }
115}
116
117pub(crate) fn find_from(line: &str, ix_start: usize, key: &str) -> Option<usize> {
118    line[ix_start..].find(key).map(|v| ix_start + v)
119}
120
121fn find_from_u8(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
122    assert!(!key.is_empty());
123    assert!(ix_start <= line.len());
124    if line.len() < key.len() {
125        return None;
126    }
127    let ix_end = line.len() - key.len();
128    if ix_start <= ix_end {
129        for i in ix_start..=ix_end {
130            if line[i] == key[0] {
131                let mut success = true;
132                for j in 1..key.len() {
133                    if line[i + j] != key[j] {
134                        success = false;
135                        break;
136                    }
137                }
138                if success {
139                    return Some(i);
140                }
141            }
142        }
143    }
144    None
145}
146
147#[test]
148fn test_find_from_u8() {
149    assert_eq!(find_from_u8(b"hello world", 0, b"hell"), Some(0));
150    assert_eq!(find_from_u8(b"hello world", 0, b"o"), Some(4));
151    assert_eq!(find_from_u8(b"hello world", 4, b"o"), Some(4));
152    assert_eq!(find_from_u8(b"hello world", 5, b"o"), Some(7));
153    assert_eq!(find_from_u8(b"hello world", 8, b"o"), None);
154    assert_eq!(find_from_u8(b"hello world", 10, b"d"), Some(10));
155    assert_eq!(find_from_u8(b"hello world", 0, b"world"), Some(6));
156}
157
158// Like find_from_u8, but additionally filters such that `key` is at the start
159// of a line (preceded by `\n`) or at the start of the search space.
160fn find_from_u8_line_prefix(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
161    let mut start = ix_start;
162    while let Some(ix) = find_from_u8(line, start, key) {
163        if ix == ix_start || line[ix - 1] == b'\n' {
164            return Some(ix);
165        }
166        start = ix + 1;
167    }
168    None
169}
170
171#[test]
172fn test_find_from_u8_line_prefix() {
173    assert_eq!(find_from_u8_line_prefix(b"hello world", 0, b"he"), Some(0));
174    assert_eq!(find_from_u8_line_prefix(b"hello\nhello", 0, b"he"), Some(0));
175    assert_eq!(find_from_u8_line_prefix(b"hello\nhello", 1, b"he"), Some(6));
176    assert_eq!(find_from_u8_line_prefix(b"hello world", 0, b"wo"), None);
177    assert_eq!(find_from_u8_line_prefix(b"hello\nworld", 0, b"wo"), Some(6));
178    assert_eq!(find_from_u8_line_prefix(b"hello\nworld", 6, b"wo"), Some(6));
179    assert_eq!(find_from_u8_line_prefix(b"hello\nworld", 7, b"wo"), None);
180    assert_eq!(
181        find_from_u8_line_prefix(b"hello\nworld", 0, b"world"),
182        Some(6)
183    );
184}
185
186impl<'a> MailHeader<'a> {
187    /// Get the name of the header. Note that header names are case-insensitive.
188    /// Prefer using get_key_ref where possible for better performance.
189    pub fn get_key(&self) -> String {
190        decode_latin1(self.key).into_owned()
191    }
192
193    /// Get the name of the header, borrowing if it's ASCII-only.
194    /// Note that header names are case-insensitive.
195    pub fn get_key_ref(&self) -> Cow<str> {
196        decode_latin1(self.key)
197    }
198
199    pub(crate) fn decode_utf8_or_latin1(&'a self) -> Cow<'a, str> {
200        // RFC 6532 says that header values can be UTF-8. Let's try that first, and
201        // fall back to latin1 if that fails, for better backwards-compatibility with
202        // older versions of this library that didn't try UTF-8.
203        match std::str::from_utf8(self.value) {
204            Ok(s) => Cow::Borrowed(s),
205            Err(_) => decode_latin1(self.value),
206        }
207    }
208
209    /// Get the value of the header. Any sequences of newlines characters followed
210    /// by whitespace are collapsed into a single space. In effect, header values
211    /// wrapped across multiple lines are compacted back into one line, while
212    /// discarding the extra whitespace required by the MIME format. Additionally,
213    /// any quoted-printable words in the value are decoded.
214    /// Note that this function attempts to decode the header value bytes as UTF-8
215    /// first, and falls back to Latin-1 if the UTF-8 decoding fails. This attempts
216    /// to be compliant with both RFC 6532 as well as older versions of this library.
217    /// To avoid the Latin-1 fallback decoding, which may end up returning "garbage",
218    /// prefer using the get_value_utf8 function instead, which will fail and return
219    /// an error instead of falling back to Latin-1.
220    ///
221    /// # Examples
222    /// ```
223    ///     use mailparse::parse_header;
224    ///     let (parsed, _) = parse_header(b"Subject: =?iso-8859-1?Q?=A1Hola,_se=F1or!?=").unwrap();
225    ///     assert_eq!(parsed.get_key(), "Subject");
226    ///     assert_eq!(parsed.get_value(), "\u{a1}Hola, se\u{f1}or!");
227    /// ```
228    pub fn get_value(&self) -> String {
229        let chars = self.decode_utf8_or_latin1();
230        self.normalize_header(chars)
231    }
232
233    fn normalize_header(&'a self, chars: Cow<'a, str>) -> String {
234        let mut result = String::new();
235
236        for tok in header::normalized_tokens(&chars) {
237            match tok {
238                HeaderToken::Text(t) => {
239                    result.push_str(t);
240                }
241                HeaderToken::Whitespace(ws) => {
242                    result.push_str(ws);
243                }
244                HeaderToken::Newline(Some(ws)) => {
245                    result.push_str(&ws);
246                }
247                HeaderToken::Newline(None) => {}
248                HeaderToken::DecodedWord(dw) => {
249                    result.push_str(&dw);
250                }
251            }
252        }
253
254        result
255    }
256
257    /// Get the value of the header. Any sequences of newlines characters followed
258    /// by whitespace are collapsed into a single space. In effect, header values
259    /// wrapped across multiple lines are compacted back into one line, while
260    /// discarding the extra whitespace required by the MIME format. Additionally,
261    /// any quoted-printable words in the value are decoded. As per RFC 6532, this
262    /// function assumes the raw header value is encoded as UTF-8, and does that
263    /// decoding prior to tokenization and other processing. An EncodingError is
264    /// returned if the raw header value cannot be decoded as UTF-8.
265    ///
266    /// # Examples
267    /// ```
268    ///     use mailparse::parse_header;
269    ///     let (parsed, _) = parse_header(b"Subject: \xC2\xA1Hola, se\xC3\xB1or!").unwrap();
270    ///     assert_eq!(parsed.get_key(), "Subject");
271    ///     assert_eq!(parsed.get_value(), "\u{a1}Hola, se\u{f1}or!");
272    /// ```
273    pub fn get_value_utf8(&self) -> Result<String, MailParseError> {
274        let chars = std::str::from_utf8(self.value).map_err(|_| {
275            MailParseError::EncodingError(Cow::Borrowed("Invalid UTF-8 in header value"))
276        })?;
277        Ok(self.normalize_header(Cow::Borrowed(chars)))
278    }
279
280    /// Get the raw, unparsed value of the header key.
281    ///
282    /// # Examples
283    /// ```
284    ///     use mailparse::parse_header;
285    ///     let (parsed, _) = parse_header(b"SuBJect : =?iso-8859-1?Q?=A1Hola,_se=F1or!?=").unwrap();
286    ///     assert_eq!(parsed.get_key_raw(), "SuBJect ".as_bytes());
287    /// ```
288    pub fn get_key_raw(&self) -> &[u8] {
289        self.key
290    }
291
292    /// Get the raw, unparsed value of the header value.
293    ///
294    /// # Examples
295    /// ```
296    ///     use mailparse::parse_header;
297    ///     let (parsed, _) = parse_header(b"Subject: =?iso-8859-1?Q?=A1Hola,_se=F1or!?=").unwrap();
298    ///     assert_eq!(parsed.get_key(), "Subject");
299    ///     assert_eq!(parsed.get_value_raw(), "=?iso-8859-1?Q?=A1Hola,_se=F1or!?=".as_bytes());
300    /// ```
301    pub fn get_value_raw(&self) -> &[u8] {
302        self.value
303    }
304}
305
306#[derive(Debug)]
307enum HeaderParseState {
308    Initial,
309    Key,
310    PreValue,
311    Value,
312    ValueNewline,
313}
314
315/// Parse a single header from the raw data given.
316/// This function takes raw byte data, and starts parsing it, expecting there
317/// to be a MIME header key-value pair right at the beginning. It parses that
318/// header and returns it, along with the index at which the next header is
319/// expected to start. If you just want to parse a single header, you can ignore
320/// the second component of the tuple, which is the index of the next header.
321/// Error values are returned if the data could not be successfully interpreted
322/// as a MIME key-value pair.
323///
324/// # Examples
325/// ```
326///     use mailparse::parse_header;
327///     let (parsed, _) = parse_header(concat!(
328///             "Subject: Hello, sir,\n",
329///             "   I am multiline\n",
330///             "Next:Header").as_bytes())
331///         .unwrap();
332///     assert_eq!(parsed.get_key(), "Subject");
333///     assert_eq!(parsed.get_value(), "Hello, sir, I am multiline");
334/// ```
335pub fn parse_header(raw_data: &[u8]) -> Result<(MailHeader, usize), MailParseError> {
336    let mut it = raw_data.iter();
337    let mut ix = 0;
338    let mut c = match it.next() {
339        None => return Err(MailParseError::Generic("Empty string provided")),
340        Some(v) => *v,
341    };
342
343    let mut ix_key_end = None;
344    let mut ix_value_start = 0;
345    let mut ix_value_end = 0;
346
347    let mut state = HeaderParseState::Initial;
348    loop {
349        match state {
350            HeaderParseState::Initial => {
351                if c == b' ' {
352                    return Err(MailParseError::Generic(
353                        "Header cannot start with a space; it is \
354                         likely an overhanging line from a \
355                         previous header",
356                    ));
357                };
358                state = HeaderParseState::Key;
359                continue;
360            }
361            HeaderParseState::Key => {
362                if c == b':' {
363                    ix_key_end = Some(ix);
364                    state = HeaderParseState::PreValue;
365                } else if c == b'\n' {
366                    // Technically this is invalid. We'll handle it gracefully
367                    // since it does appear to happen in the wild and other
368                    // MTAs deal with it. Our handling is to just treat everything
369                    // encountered so far on this line as the header key, and
370                    // leave the value empty.
371                    ix_key_end = Some(ix);
372                    ix_value_start = ix;
373                    ix_value_end = ix;
374                    ix += 1;
375                    break;
376                }
377            }
378            HeaderParseState::PreValue => {
379                if c != b' ' {
380                    ix_value_start = ix;
381                    ix_value_end = ix;
382                    state = HeaderParseState::Value;
383                    continue;
384                }
385            }
386            HeaderParseState::Value => {
387                if c == b'\n' {
388                    state = HeaderParseState::ValueNewline;
389                } else if c != b'\r' {
390                    ix_value_end = ix + 1;
391                }
392            }
393            HeaderParseState::ValueNewline => {
394                if c == b' ' || c == b'\t' {
395                    state = HeaderParseState::Value;
396                    continue;
397                } else {
398                    break;
399                }
400            }
401        }
402        ix += 1;
403        c = match it.next() {
404            None => break,
405            Some(v) => *v,
406        };
407    }
408    match ix_key_end {
409        Some(v) => Ok((
410            MailHeader {
411                key: &raw_data[0..v],
412                value: &raw_data[ix_value_start..ix_value_end],
413            },
414            ix,
415        )),
416
417        None => Ok((
418            // Technically this is invalid. We'll handle it gracefully
419            // since we handle the analogous situation above. Our handling
420            // is to just treat everything encountered on this line as
421            // the header key, and leave the value empty.
422            MailHeader {
423                key: &raw_data[0..ix],
424                value: &raw_data[ix..ix],
425            },
426            ix,
427        )),
428    }
429}
430
431/// A trait that is implemented by the [MailHeader] slice. These functions are
432/// also available on Vec<MailHeader> which is returned by the parse_headers
433/// function. It provides a map-like interface to look up header values by their
434/// name.
435pub trait MailHeaderMap {
436    /// Look through the list of headers and return the value of the first one
437    /// that matches the provided key. It returns Ok(None) if the no matching
438    /// header was found. Header names are matched case-insensitively.
439    ///
440    /// # Examples
441    /// ```
442    ///     use mailparse::{parse_mail, MailHeaderMap};
443    ///     let headers = parse_mail(concat!(
444    ///             "Subject: Test\n",
445    ///             "\n",
446    ///             "This is a test message").as_bytes())
447    ///         .unwrap().headers;
448    ///     assert_eq!(headers.get_first_value("Subject"), Some("Test".to_string()));
449    /// ```
450    fn get_first_value(&self, key: &str) -> Option<String>;
451
452    /// Similar to `get_first_value`, except it returns a reference to the
453    /// MailHeader struct instead of just extracting the value.
454    fn get_first_header(&self, key: &str) -> Option<&MailHeader>;
455
456    /// Look through the list of headers and return the values of all headers
457    /// matching the provided key. Returns an empty vector if no matching headers
458    /// were found. The order of the returned values is the same as the order
459    /// of the matching headers in the message. Header names are matched
460    /// case-insensitively.
461    ///
462    /// # Examples
463    /// ```
464    ///     use mailparse::{parse_mail, MailHeaderMap};
465    ///     let headers = parse_mail(concat!(
466    ///             "Key: Value1\n",
467    ///             "Key: Value2").as_bytes())
468    ///         .unwrap().headers;
469    ///     assert_eq!(headers.get_all_values("Key"),
470    ///         vec!["Value1".to_string(), "Value2".to_string()]);
471    /// ```
472    fn get_all_values(&self, key: &str) -> Vec<String>;
473
474    /// Similar to `get_all_values`, except it returns references to the
475    /// MailHeader structs instead of just extracting the values.
476    fn get_all_headers(&self, key: &str) -> Vec<&MailHeader>;
477}
478
479impl<'a> MailHeaderMap for [MailHeader<'a>] {
480    fn get_first_value(&self, key: &str) -> Option<String> {
481        for x in self {
482            if x.get_key_ref().eq_ignore_ascii_case(key) {
483                return Some(x.get_value());
484            }
485        }
486        None
487    }
488
489    fn get_first_header(&self, key: &str) -> Option<&MailHeader> {
490        self.iter()
491            .find(|&x| x.get_key_ref().eq_ignore_ascii_case(key))
492    }
493
494    fn get_all_values(&self, key: &str) -> Vec<String> {
495        let mut values: Vec<String> = Vec::new();
496        for x in self {
497            if x.get_key_ref().eq_ignore_ascii_case(key) {
498                values.push(x.get_value());
499            }
500        }
501        values
502    }
503
504    fn get_all_headers(&self, key: &str) -> Vec<&MailHeader> {
505        let mut headers: Vec<&MailHeader> = Vec::new();
506        for x in self {
507            if x.get_key_ref().eq_ignore_ascii_case(key) {
508                headers.push(x);
509            }
510        }
511        headers
512    }
513}
514
515/// Parses all the headers from the raw data given.
516/// This function takes raw byte data, and starts parsing it, expecting there
517/// to be zero or more MIME header key-value pair right at the beginning,
518/// followed by two consecutive newlines (i.e. a blank line). It parses those
519/// headers and returns them in a vector. The normal vector functions can be
520/// used to access the headers linearly, or the MailHeaderMap trait can be used
521/// to access them in a map-like fashion. Along with this vector, the function
522/// returns the index at which the message body is expected to start. If you
523/// just care about the headers, you can ignore the second component of the
524/// returned tuple.
525/// Error values are returned if there was some sort of parsing error.
526///
527/// # Examples
528/// ```
529///     use mailparse::{parse_headers, MailHeaderMap};
530///     let (headers, _) = parse_headers(concat!(
531///             "Subject: Test\n",
532///             "From: me@myself.com\n",
533///             "To: you@yourself.com").as_bytes())
534///         .unwrap();
535///     assert_eq!(headers[1].get_key(), "From");
536///     assert_eq!(headers.get_first_value("To"), Some("you@yourself.com".to_string()));
537/// ```
538pub fn parse_headers(raw_data: &[u8]) -> Result<(Vec<MailHeader>, usize), MailParseError> {
539    let mut headers: Vec<MailHeader> = Vec::new();
540    let mut ix = 0;
541    loop {
542        if ix >= raw_data.len() {
543            break;
544        } else if raw_data[ix] == b'\n' {
545            ix += 1;
546            break;
547        } else if raw_data[ix] == b'\r' {
548            if ix + 1 < raw_data.len() && raw_data[ix + 1] == b'\n' {
549                ix += 2;
550                break;
551            } else {
552                return Err(MailParseError::Generic(
553                    "Headers were followed by an unexpected lone \
554                     CR character!",
555                ));
556            }
557        }
558        let (header, ix_next) = parse_header(&raw_data[ix..])?;
559        headers.push(header);
560        ix += ix_next;
561    }
562    Ok((headers, ix))
563}
564
565/// A struct to hold a more structured representation of the Content-Type header.
566/// This is provided mostly as a convenience since this metadata is usually
567/// needed to interpret the message body properly.
568#[derive(Debug)]
569pub struct ParsedContentType {
570    /// The type of the data, for example "text/plain" or "application/pdf".
571    pub mimetype: String,
572    /// The charset used to decode the raw byte data, for example "iso-8859-1"
573    /// or "utf-8".
574    pub charset: String,
575    /// The additional params of Content-Type, e.g. filename and boundary. The
576    /// keys in the map will be lowercased, and the values will have any
577    /// enclosing quotes stripped.
578    pub params: BTreeMap<String, String>,
579}
580
581impl Default for ParsedContentType {
582    fn default() -> Self {
583        ParsedContentType {
584            mimetype: "text/plain".to_string(),
585            charset: "us-ascii".to_string(),
586            params: BTreeMap::new(),
587        }
588    }
589}
590
591impl ParsedContentType {
592    fn default_conditional(in_multipart_digest: bool) -> Self {
593        let mut default = Self::default();
594        if in_multipart_digest {
595            default.mimetype = "message/rfc822".to_string();
596        }
597        default
598    }
599}
600
601/// Helper method to parse a header value as a Content-Type header. Note that
602/// the returned object's `params` map will contain a charset key if a charset
603/// was explicitly specified in the header; otherwise the `params` map will not
604/// contain a charset key. Regardless, the `charset` field will contain a
605/// charset - either the one explicitly specified or the default of "us-ascii".
606///
607/// # Examples
608/// ```
609///     use mailparse::{parse_header, parse_content_type};
610///     let (parsed, _) = parse_header(
611///             b"Content-Type: text/html; charset=foo; boundary=\"quotes_are_removed\"")
612///         .unwrap();
613///     let ctype = parse_content_type(&parsed.get_value());
614///     assert_eq!(ctype.mimetype, "text/html");
615///     assert_eq!(ctype.charset, "foo");
616///     assert_eq!(ctype.params.get("boundary"), Some(&"quotes_are_removed".to_string()));
617///     assert_eq!(ctype.params.get("charset"), Some(&"foo".to_string()));
618/// ```
619/// ```
620///     use mailparse::{parse_header, parse_content_type};
621///     let (parsed, _) = parse_header(b"Content-Type: bogus").unwrap();
622///     let ctype = parse_content_type(&parsed.get_value());
623///     assert_eq!(ctype.mimetype, "bogus");
624///     assert_eq!(ctype.charset, "us-ascii");
625///     assert_eq!(ctype.params.get("boundary"), None);
626///     assert_eq!(ctype.params.get("charset"), None);
627/// ```
628/// ```
629///     use mailparse::{parse_header, parse_content_type};
630///     let (parsed, _) = parse_header(br#"Content-Type: application/octet-stream;name="=?utf8?B?6L+O5ai255m95a+M576O?=";charset="utf8""#).unwrap();
631///     let ctype = parse_content_type(&parsed.get_value());
632///     assert_eq!(ctype.mimetype, "application/octet-stream");
633///     assert_eq!(ctype.charset, "utf8");
634///     assert_eq!(ctype.params.get("boundary"), None);
635///     assert_eq!(ctype.params.get("name"), Some(&"迎娶白富美".to_string()));
636/// ```
637pub fn parse_content_type(header: &str) -> ParsedContentType {
638    let params = parse_param_content(header);
639    let mimetype = params.value.to_lowercase();
640    let charset = params
641        .params
642        .get("charset")
643        .cloned()
644        .unwrap_or_else(|| "us-ascii".to_string());
645
646    ParsedContentType {
647        mimetype,
648        charset,
649        params: params.params,
650    }
651}
652
653/// The possible disposition types in a Content-Disposition header. A more
654/// comprehensive list of IANA-recognized types can be found at
655/// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml. This library
656/// only enumerates the types most commonly found in email messages, and
657/// provides the `Extension` value for holding all other types.
658#[derive(Debug, Clone, PartialEq)]
659pub enum DispositionType {
660    /// Default value, indicating the content is to be displayed inline as
661    /// part of the enclosing document.
662    Inline,
663    /// A disposition indicating the content is not meant for inline display,
664    /// but whose content can be accessed for use.
665    Attachment,
666    /// A disposition indicating the content contains a form submission.
667    FormData,
668    /// Extension type to hold any disposition not explicitly enumerated.
669    Extension(String),
670}
671
672impl Default for DispositionType {
673    fn default() -> Self {
674        DispositionType::Inline
675    }
676}
677
678/// Convert the string represented disposition type to enum.
679fn parse_disposition_type(disposition: &str) -> DispositionType {
680    match &disposition.to_lowercase()[..] {
681        "inline" => DispositionType::Inline,
682        "attachment" => DispositionType::Attachment,
683        "form-data" => DispositionType::FormData,
684        extension => DispositionType::Extension(extension.to_string()),
685    }
686}
687
688/// A struct to hold a more structured representation of the Content-Disposition header.
689/// This is provided mostly as a convenience since this metadata is usually
690/// needed to interpret the message body properly.
691#[derive(Debug, Default)]
692pub struct ParsedContentDisposition {
693    /// The disposition type of the Content-Disposition header. If this
694    /// is an extension type, the string will be lowercased.
695    pub disposition: DispositionType,
696    /// The additional params of Content-Disposition, e.g. filename. The
697    /// keys in the map will be lowercased, and the values will have any
698    /// enclosing quotes stripped.
699    pub params: BTreeMap<String, String>,
700}
701
702/// Helper method to parse a header value as a Content-Disposition header. The disposition
703/// defaults to "inline" if no disposition parameter is provided in the header
704/// value.
705///
706/// # Examples
707/// ```
708///     use mailparse::{parse_header, parse_content_disposition, DispositionType};
709///     let (parsed, _) = parse_header(
710///             b"Content-Disposition: attachment; filename=\"yummy dummy\"")
711///         .unwrap();
712///     let dis = parse_content_disposition(&parsed.get_value());
713///     assert_eq!(dis.disposition, DispositionType::Attachment);
714///     assert_eq!(dis.params.get("name"), None);
715///     assert_eq!(dis.params.get("filename"), Some(&"yummy dummy".to_string()));
716/// ```
717pub fn parse_content_disposition(header: &str) -> ParsedContentDisposition {
718    let params = parse_param_content(header);
719    let disposition = parse_disposition_type(&params.value);
720    ParsedContentDisposition {
721        disposition,
722        params: params.params,
723    }
724}
725
726/// Struct that holds the structured representation of the message. Note that
727/// since MIME allows for nested multipart messages, a tree-like structure is
728/// necessary to represent it properly. This struct accomplishes that by holding
729/// a vector of other ParsedMail structures for the subparts.
730#[derive(Debug)]
731pub struct ParsedMail<'a> {
732    /// The raw bytes that make up this message (or subpart).
733    pub raw_bytes: &'a [u8],
734    /// The raw bytes that make up the header block for this message (or subpart).
735    header_bytes: &'a [u8],
736    /// The headers for the message (or message subpart).
737    pub headers: Vec<MailHeader<'a>>,
738    /// The Content-Type information for the message (or message subpart).
739    pub ctype: ParsedContentType,
740    /// The raw bytes that make up the body of the message (or message subpart).
741    body_bytes: &'a [u8],
742    /// The subparts of this message or subpart. This vector is only non-empty
743    /// if ctype.mimetype starts with "multipart/".
744    pub subparts: Vec<ParsedMail<'a>>,
745}
746
747impl<'a> ParsedMail<'a> {
748    /// Get the body of the message as a Rust string. This function tries to
749    /// unapply the Content-Transfer-Encoding if there is one, and then converts
750    /// the result into a Rust UTF-8 string using the charset in the Content-Type
751    /// (or "us-ascii" if the charset was missing or not recognized). Note that
752    /// in some cases the body may be binary data that doesn't make sense as a
753    /// Rust string - it is up to the caller to handle those cases gracefully.
754    /// These cases may occur in particular when the body is of a "binary"
755    /// Content-Transfer-Encoding (i.e. where `get_body_encoded()` returns a
756    /// `Body::Binary` variant) but may also occur in other cases because of the
757    /// messiness of the real world and non-compliant mail implementations.
758    ///
759    /// # Examples
760    /// ```
761    ///     use mailparse::parse_mail;
762    ///     let p = parse_mail(concat!(
763    ///             "Subject: test\n",
764    ///             "\n",
765    ///             "This is the body").as_bytes())
766    ///         .unwrap();
767    ///     assert_eq!(p.get_body().unwrap(), "This is the body");
768    /// ```
769    pub fn get_body(&self) -> Result<String, MailParseError> {
770        match self.get_body_encoded() {
771            Body::Base64(body) | Body::QuotedPrintable(body) => body.get_decoded_as_string(),
772            Body::SevenBit(body) | Body::EightBit(body) => body.get_as_string(),
773            Body::Binary(body) => body.get_as_string(),
774        }
775    }
776
777    /// Get the body of the message as a Rust Vec<u8>. This function tries to
778    /// unapply the Content-Transfer-Encoding if there is one, but won't do
779    /// any charset decoding.
780    ///
781    /// # Examples
782    /// ```
783    ///     use mailparse::parse_mail;
784    ///     let p = parse_mail(concat!(
785    ///             "Subject: test\n",
786    ///             "\n",
787    ///             "This is the body").as_bytes())
788    ///         .unwrap();
789    ///     assert_eq!(p.get_body_raw().unwrap(), b"This is the body");
790    /// ```
791    pub fn get_body_raw(&self) -> Result<Vec<u8>, MailParseError> {
792        match self.get_body_encoded() {
793            Body::Base64(body) | Body::QuotedPrintable(body) => body.get_decoded(),
794            Body::SevenBit(body) | Body::EightBit(body) => Ok(Vec::<u8>::from(body.get_raw())),
795            Body::Binary(body) => Ok(Vec::<u8>::from(body.get_raw())),
796        }
797    }
798
799    /// Get the body of the message.
800    /// This function returns the original body without attempting to
801    /// unapply the Content-Transfer-Encoding. The returned object
802    /// contains information that allows the caller to control decoding
803    /// as desired.
804    ///
805    /// # Examples
806    /// ```
807    ///     use mailparse::parse_mail;
808    ///     use mailparse::body::Body;
809    ///
810    ///     let mail = parse_mail(b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=").unwrap();
811    ///
812    ///     match mail.get_body_encoded() {
813    ///         Body::Base64(body) => {
814    ///             assert_eq!(body.get_raw(), b"aGVsbG 8gd\r\n29ybGQ=");
815    ///             assert_eq!(body.get_decoded().unwrap(), b"hello world");
816    ///             assert_eq!(body.get_decoded_as_string().unwrap(), "hello world");
817    ///         },
818    ///         _ => assert!(false),
819    ///     };
820    ///
821    ///
822    ///     // An email whose body encoding is not known upfront
823    ///     let another_mail = parse_mail(b"").unwrap();
824    ///
825    ///     match another_mail.get_body_encoded() {
826    ///         Body::Base64(body) | Body::QuotedPrintable(body) => {
827    ///             println!("mail body encoded: {:?}", body.get_raw());
828    ///             println!("mail body decoded: {:?}", body.get_decoded().unwrap());
829    ///             println!("mail body decoded as string: {}", body.get_decoded_as_string().unwrap());
830    ///         },
831    ///         Body::SevenBit(body) | Body::EightBit(body) => {
832    ///             println!("mail body: {:?}", body.get_raw());
833    ///             println!("mail body as string: {}", body.get_as_string().unwrap());
834    ///         },
835    ///         Body::Binary(body) => {
836    ///             println!("mail body binary: {:?}", body.get_raw());
837    ///         }
838    ///     }
839    /// ```
840    pub fn get_body_encoded(&'a self) -> Body<'a> {
841        let transfer_encoding = self
842            .headers
843            .get_first_value("Content-Transfer-Encoding")
844            .map(|s| s.to_lowercase());
845
846        Body::new(self.body_bytes, &self.ctype, &transfer_encoding)
847    }
848
849    /// Returns a struct that wraps the headers for this message.
850    /// The struct provides utility methods to read the individual headers.
851    pub fn get_headers(&'a self) -> Headers<'a> {
852        Headers::new(self.header_bytes, &self.headers)
853    }
854
855    /// Returns a struct containing a parsed representation of the
856    /// Content-Disposition header. The first header with this name
857    /// is used, if there are multiple. See the `parse_content_disposition`
858    /// method documentation for more details on the semantics of the
859    /// returned object.
860    pub fn get_content_disposition(&self) -> ParsedContentDisposition {
861        self.headers
862            .get_first_value("Content-Disposition")
863            .map(|s| parse_content_disposition(&s))
864            .unwrap_or_default()
865    }
866
867    /// Returns a depth-first pre-order traversal of the subparts of
868    /// this ParsedMail instance. The first item returned will be this
869    /// ParsedMail itself.
870    pub fn parts(&'a self) -> PartsIterator<'a> {
871        PartsIterator {
872            parts: vec![self],
873            index: 0,
874        }
875    }
876}
877
878pub struct PartsIterator<'a> {
879    parts: Vec<&'a ParsedMail<'a>>,
880    index: usize,
881}
882
883impl<'a> Iterator for PartsIterator<'a> {
884    type Item = &'a ParsedMail<'a>;
885
886    fn next(&mut self) -> Option<Self::Item> {
887        if self.index >= self.parts.len() {
888            return None;
889        }
890
891        let cur = self.parts[self.index];
892        self.index += 1;
893        self.parts
894            .splice(self.index..self.index, cur.subparts.iter());
895        Some(cur)
896    }
897}
898
899/// The main mail-parsing entry point.
900/// This function takes the raw data making up the message body and returns a
901/// structured version of it, which allows easily accessing the header and body
902/// information as needed.
903///
904/// # Examples
905/// ```
906///     use mailparse::*;
907///     let parsed = parse_mail(concat!(
908///             "Subject: This is a test email\n",
909///             "Content-Type: multipart/alternative; boundary=foobar\n",
910///             "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\n",
911///             "\n",
912///             "--foobar\n",
913///             "Content-Type: text/plain; charset=utf-8\n",
914///             "Content-Transfer-Encoding: quoted-printable\n",
915///             "\n",
916///             "This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\n",
917///             "--foobar\n",
918///             "Content-Type: text/html\n",
919///             "Content-Transfer-Encoding: base64\n",
920///             "\n",
921///             "PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \n",
922///             "dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \n",
923///             "--foobar--\n",
924///             "After the final boundary stuff gets ignored.\n").as_bytes())
925///         .unwrap();
926///     assert_eq!(parsed.headers.get_first_value("Subject"),
927///         Some("This is a test email".to_string()));
928///     assert_eq!(parsed.subparts.len(), 2);
929///     assert_eq!(parsed.subparts[0].get_body().unwrap(),
930///         "This is the plaintext version, in utf-8. Proof by Euro: \u{20AC}");
931///     assert_eq!(parsed.subparts[1].headers[1].get_value(), "base64");
932///     assert_eq!(parsed.subparts[1].ctype.mimetype, "text/html");
933///     assert!(parsed.subparts[1].get_body().unwrap().starts_with("<html>"));
934///     assert_eq!(dateparse(parsed.headers.get_first_value("Date").unwrap().as_str()).unwrap(), 1475417182);
935/// ```
936pub fn parse_mail(raw_data: &[u8]) -> Result<ParsedMail, MailParseError> {
937    parse_mail_recursive(raw_data, false)
938}
939
940/// Strips LF or CRLF if there is one at the end of the string raw_data[ix_start..ix].
941/// This is used to ensure that CRLF just before a boundary is treated as part of the
942/// boundary, not the body part that was before the boundary. See discussion in
943/// https://github.com/staktrace/mailparse/issues/127.
944fn strip_trailing_crlf(raw_data: &[u8], ix_start: usize, mut ix: usize) -> usize {
945    if ix > ix_start && raw_data[ix - 1] == b'\n' {
946        ix -= 1;
947        if ix > ix_start && raw_data[ix - 1] == b'\r' {
948            ix -= 1;
949        }
950    }
951    ix
952}
953
954fn parse_mail_recursive(
955    raw_data: &[u8],
956    in_multipart_digest: bool,
957) -> Result<ParsedMail, MailParseError> {
958    let (headers, ix_body) = parse_headers(raw_data)?;
959    let ctype = headers
960        .get_first_value("Content-Type")
961        .map(|s| parse_content_type(&s))
962        .unwrap_or_else(|| ParsedContentType::default_conditional(in_multipart_digest));
963
964    let mut result = ParsedMail {
965        raw_bytes: raw_data,
966        header_bytes: &raw_data[0..ix_body],
967        headers,
968        ctype,
969        body_bytes: &raw_data[ix_body..],
970        subparts: Vec::<ParsedMail>::new(),
971    };
972    if result.ctype.mimetype.starts_with("multipart/")
973        && result.ctype.params.get("boundary").is_some()
974        && raw_data.len() > ix_body
975    {
976        let in_multipart_digest = result.ctype.mimetype == "multipart/digest";
977        let boundary = String::from("--") + &result.ctype.params["boundary"];
978        if let Some(ix_boundary_start) =
979            find_from_u8_line_prefix(raw_data, ix_body, boundary.as_bytes())
980        {
981            let ix_body_end = strip_trailing_crlf(raw_data, ix_body, ix_boundary_start);
982            result.body_bytes = &raw_data[ix_body..ix_body_end];
983            let mut ix_boundary_end = ix_boundary_start + boundary.len();
984            while let Some(ix_part_start) =
985                find_from_u8(raw_data, ix_boundary_end, b"\n").map(|v| v + 1)
986            {
987                let ix_part_boundary_start =
988                    find_from_u8_line_prefix(raw_data, ix_part_start, boundary.as_bytes());
989                let ix_part_end = ix_part_boundary_start
990                    .map(|x| strip_trailing_crlf(raw_data, ix_part_start, x))
991                    // if there is no terminating boundary, assume the part end is the end of the email
992                    .unwrap_or(raw_data.len());
993
994                result.subparts.push(parse_mail_recursive(
995                    &raw_data[ix_part_start..ix_part_end],
996                    in_multipart_digest,
997                )?);
998                ix_boundary_end = ix_part_boundary_start
999                    .map(|x| x + boundary.len())
1000                    .unwrap_or(raw_data.len());
1001                if ix_boundary_end + 2 > raw_data.len()
1002                    || (raw_data[ix_boundary_end] == b'-' && raw_data[ix_boundary_end + 1] == b'-')
1003                {
1004                    break;
1005                }
1006            }
1007        }
1008    }
1009    Ok(result)
1010}
1011
1012/// Used to store params for content-type and content-disposition
1013struct ParamContent {
1014    value: String,
1015    params: BTreeMap<String, String>,
1016}
1017
1018/// Parse parameterized header values such as that for Content-Type
1019/// e.g. `multipart/alternative; boundary=foobar`
1020/// Note: this function is not made public as it may require
1021/// significant changes to be fully correct. For instance,
1022/// it does not handle quoted parameter values containing the
1023/// semicolon (';') character. It also produces a BTreeMap,
1024/// which implicitly does not support multiple parameters with
1025/// the same key. Also, the parameter values may contain language
1026/// information in a format specified by RFC 2184 which is thrown
1027/// away. The format for parameterized header values doesn't
1028/// appear to be strongly specified anywhere.
1029fn parse_param_content(content: &str) -> ParamContent {
1030    let mut tokens = content.split(';');
1031    // There must be at least one token produced by split, even if it's empty.
1032    let value = tokens.next().unwrap().trim();
1033    let mut map: BTreeMap<String, String> = tokens
1034        .filter_map(|kv| {
1035            kv.find('=').map(|idx| {
1036                let key = kv[0..idx].trim().to_lowercase();
1037                let mut value = kv[idx + 1..].trim();
1038                if value.starts_with('"') && value.ends_with('"') && value.len() > 1 {
1039                    value = &value[1..value.len() - 1];
1040                }
1041                (key, value.to_string())
1042            })
1043        })
1044        .collect();
1045
1046    // Decode charset encoding, as described in RFC 2184, Section 4.
1047    let decode_key_list: Vec<String> = map
1048        .keys()
1049        .filter_map(|k| k.strip_suffix('*'))
1050        .map(String::from)
1051        // Skip encoded keys where there is already an equivalent decoded key in the map
1052        .filter(|k| !map.contains_key(k))
1053        .collect();
1054    let encodings = compute_parameter_encodings(&map, &decode_key_list);
1055    // Note that when we get here, we might still have entries in `encodings` for continuation segments
1056    // that didn't have a *0 segment at all. These shouldn't exist per spec so we can do whatever we want,
1057    // as long as we don't panic.
1058    for (k, (e, strip)) in encodings {
1059        if let Some(charset) = Charset::for_label_no_replacement(e.as_bytes()) {
1060            let key = format!("{}*", k);
1061            let percent_encoded_value = map.remove(&key).unwrap();
1062            let encoded_value = if strip {
1063                percent_decode(percent_encoded_value.splitn(3, '\'').nth(2).unwrap_or(""))
1064            } else {
1065                percent_decode(&percent_encoded_value)
1066            };
1067            let decoded_value = charset.decode_without_bom_handling(&encoded_value).0;
1068            map.insert(k, decoded_value.to_string());
1069        }
1070    }
1071
1072    // Unwrap parameter value continuations, as described in RFC 2184, Section 3.
1073    let unwrap_key_list: Vec<String> = map
1074        .keys()
1075        .filter_map(|k| k.strip_suffix("*0"))
1076        .map(String::from)
1077        // Skip wrapped keys where there is already an unwrapped equivalent in the map
1078        .filter(|k| !map.contains_key(k))
1079        .collect();
1080    for unwrap_key in unwrap_key_list {
1081        let mut unwrapped_value = String::new();
1082        let mut index = 0;
1083        while let Some(wrapped_value_part) = map.remove(&format!("{}*{}", &unwrap_key, index)) {
1084            index += 1;
1085            unwrapped_value.push_str(&wrapped_value_part);
1086        }
1087        let old_value = map.insert(unwrap_key, unwrapped_value);
1088        assert!(old_value.is_none());
1089    }
1090
1091    ParamContent {
1092        value: value.into(),
1093        params: map,
1094    }
1095}
1096
1097/// In the returned map, the key is one of the entries from the decode_key_list,
1098/// (i.e. the parameter key with the trailing '*' stripped). The value is a tuple
1099/// containing the encoding (or empty string for no encoding found) and a flag
1100/// that indicates if the encoding needs to be stripped from the value. This is
1101/// set to true for non-continuation parameter values.
1102fn compute_parameter_encodings(
1103    map: &BTreeMap<String, String>,
1104    decode_key_list: &Vec<String>,
1105) -> HashMap<String, (String, bool)> {
1106    // To handle section 4.1 (combining encodings with continuations), we first
1107    // compute the encoding for each parameter value or parameter value segment
1108    // that is encoded. For continuation segments the encoding from the *0 segment
1109    // overwrites the continuation segment's encoding, if there is one.
1110    let mut encodings: HashMap<String, (String, bool)> = HashMap::new();
1111    for decode_key in decode_key_list {
1112        if let Some(unwrap_key) = decode_key.strip_suffix("*0") {
1113            // Per spec, there should always be an encoding. If it's missing, handle that case gracefully
1114            // by setting it to an empty string that we handle specially later.
1115            let encoding = map
1116                .get(&format!("{}*", decode_key))
1117                .unwrap()
1118                .split('\'')
1119                .next()
1120                .unwrap_or("");
1121            let continuation_prefix = format!("{}*", unwrap_key);
1122            for continuation_key in decode_key_list {
1123                if continuation_key.starts_with(&continuation_prefix) {
1124                    // This may (intentionally) overwite encodings previously found for the
1125                    // continuation segments (which are bogus). In those cases, the flag
1126                    // in the tuple should get updated from true to false.
1127                    encodings.insert(
1128                        continuation_key.clone(),
1129                        (encoding.to_string(), continuation_key == decode_key),
1130                    );
1131                }
1132            }
1133        } else if !encodings.contains_key(decode_key) {
1134            let encoding = map
1135                .get(&format!("{}*", decode_key))
1136                .unwrap()
1137                .split('\'')
1138                .next()
1139                .unwrap_or("")
1140                .to_string();
1141            let old_value = encodings.insert(decode_key.clone(), (encoding, true));
1142            assert!(old_value.is_none());
1143        }
1144        // else this is a continuation segment and the encoding has already been populated
1145        // by the initial *0 segment, so we can ignore it.
1146    }
1147    encodings
1148}
1149
1150fn percent_decode(encoded: &str) -> Vec<u8> {
1151    let mut decoded = Vec::with_capacity(encoded.len());
1152    let mut bytes = encoded.bytes();
1153    let mut next = bytes.next();
1154    while next.is_some() {
1155        let b = next.unwrap();
1156        if b != b'%' {
1157            decoded.push(b);
1158            next = bytes.next();
1159            continue;
1160        }
1161
1162        let top = match bytes.next() {
1163            Some(n) if n.is_ascii_hexdigit() => n,
1164            n => {
1165                decoded.push(b);
1166                next = n;
1167                continue;
1168            }
1169        };
1170        let bottom = match bytes.next() {
1171            Some(n) if n.is_ascii_hexdigit() => n,
1172            n => {
1173                decoded.push(b);
1174                decoded.push(top);
1175                next = n;
1176                continue;
1177            }
1178        };
1179        let decoded_byte = (hex_to_nybble(top) << 4) | hex_to_nybble(bottom);
1180        decoded.push(decoded_byte);
1181
1182        next = bytes.next();
1183    }
1184    decoded
1185}
1186
1187fn hex_to_nybble(byte: u8) -> u8 {
1188    match byte {
1189        b'0'..=b'9' => byte - b'0',
1190        b'a'..=b'f' => byte - b'a' + 10,
1191        b'A'..=b'F' => byte - b'A' + 10,
1192        _ => panic!("Not a hex character!"),
1193    }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    use super::*;
1199
1200    #[test]
1201    fn parse_basic_header() {
1202        let (parsed, _) = parse_header(b"Key: Value").unwrap();
1203        assert_eq!(parsed.key, b"Key");
1204        assert_eq!(parsed.get_key(), "Key");
1205        assert_eq!(parsed.get_key_ref(), "Key");
1206        assert_eq!(parsed.value, b"Value");
1207        assert_eq!(parsed.get_value(), "Value");
1208        assert_eq!(parsed.get_value_raw(), "Value".as_bytes());
1209
1210        let (parsed, _) = parse_header(b"Key :  Value ").unwrap();
1211        assert_eq!(parsed.key, b"Key ");
1212        assert_eq!(parsed.value, b"Value ");
1213        assert_eq!(parsed.get_value(), "Value ");
1214        assert_eq!(parsed.get_value_raw(), "Value ".as_bytes());
1215
1216        let (parsed, _) = parse_header(b"Key:").unwrap();
1217        assert_eq!(parsed.key, b"Key");
1218        assert_eq!(parsed.value, b"");
1219
1220        let (parsed, _) = parse_header(b":\n").unwrap();
1221        assert_eq!(parsed.key, b"");
1222        assert_eq!(parsed.value, b"");
1223
1224        let (parsed, _) = parse_header(b"Key:Multi-line\n value").unwrap();
1225        assert_eq!(parsed.key, b"Key");
1226        assert_eq!(parsed.value, b"Multi-line\n value");
1227        assert_eq!(parsed.get_value(), "Multi-line value");
1228        assert_eq!(parsed.get_value_raw(), "Multi-line\n value".as_bytes());
1229
1230        let (parsed, _) = parse_header(b"Key:  Multi\n  line\n value\n").unwrap();
1231        assert_eq!(parsed.key, b"Key");
1232        assert_eq!(parsed.value, b"Multi\n  line\n value");
1233        assert_eq!(parsed.get_value(), "Multi line value");
1234        assert_eq!(parsed.get_value_raw(), "Multi\n  line\n value".as_bytes());
1235
1236        let (parsed, _) = parse_header(b"Key: One\nKey2: Two").unwrap();
1237        assert_eq!(parsed.key, b"Key");
1238        assert_eq!(parsed.value, b"One");
1239
1240        let (parsed, _) = parse_header(b"Key: One\n\tOverhang").unwrap();
1241        assert_eq!(parsed.key, b"Key");
1242        assert_eq!(parsed.value, b"One\n\tOverhang");
1243        assert_eq!(parsed.get_value(), "One Overhang");
1244        assert_eq!(parsed.get_value_raw(), "One\n\tOverhang".as_bytes());
1245
1246        let (parsed, _) = parse_header(b"SPAM: VIAGRA \xAE").unwrap();
1247        assert_eq!(parsed.key, b"SPAM");
1248        assert_eq!(parsed.value, b"VIAGRA \xAE");
1249        assert_eq!(parsed.get_value(), "VIAGRA \u{ae}");
1250        assert_eq!(parsed.get_value_raw(), b"VIAGRA \xAE");
1251
1252        parse_header(b" Leading: Space").unwrap_err();
1253
1254        let (parsed, _) = parse_header(b"Just a string").unwrap();
1255        assert_eq!(parsed.key, b"Just a string");
1256        assert_eq!(parsed.value, b"");
1257        assert_eq!(parsed.get_value(), "");
1258        assert_eq!(parsed.get_value_raw(), b"");
1259
1260        let (parsed, _) = parse_header(b"Key\nBroken: Value").unwrap();
1261        assert_eq!(parsed.key, b"Key");
1262        assert_eq!(parsed.value, b"");
1263        assert_eq!(parsed.get_value(), "");
1264        assert_eq!(parsed.get_value_raw(), b"");
1265
1266        let (parsed, _) = parse_header(b"Key: With CRLF\r\n").unwrap();
1267        assert_eq!(parsed.key, b"Key");
1268        assert_eq!(parsed.value, b"With CRLF");
1269        assert_eq!(parsed.get_value(), "With CRLF");
1270        assert_eq!(parsed.get_value_raw(), b"With CRLF");
1271
1272        let (parsed, _) = parse_header(b"Key: With spurious CRs\r\r\r\n").unwrap();
1273        assert_eq!(parsed.value, b"With spurious CRs");
1274        assert_eq!(parsed.get_value(), "With spurious CRs");
1275        assert_eq!(parsed.get_value_raw(), b"With spurious CRs");
1276
1277        let (parsed, _) = parse_header(b"Key: With \r mixed CR\r\n").unwrap();
1278        assert_eq!(parsed.value, b"With \r mixed CR");
1279        assert_eq!(parsed.get_value(), "With \r mixed CR");
1280        assert_eq!(parsed.get_value_raw(), b"With \r mixed CR");
1281
1282        let (parsed, _) = parse_header(b"Key:\r\n Value after linebreak").unwrap();
1283        assert_eq!(parsed.value, b"\r\n Value after linebreak");
1284        assert_eq!(parsed.get_value(), " Value after linebreak");
1285        assert_eq!(parsed.get_value_raw(), b"\r\n Value after linebreak");
1286    }
1287
1288    #[test]
1289    fn parse_encoded_headers() {
1290        let (parsed, _) = parse_header(b"Subject: =?iso-8859-1?Q?=A1Hola,_se=F1or!?=").unwrap();
1291        assert_eq!(parsed.get_key(), "Subject");
1292        assert_eq!(parsed.get_key_ref(), "Subject");
1293        assert_eq!(parsed.get_value(), "\u{a1}Hola, se\u{f1}or!");
1294        assert_eq!(
1295            parsed.get_value_raw(),
1296            "=?iso-8859-1?Q?=A1Hola,_se=F1or!?=".as_bytes()
1297        );
1298
1299        let (parsed, _) = parse_header(
1300            b"Subject: =?iso-8859-1?Q?=A1Hola,?=\n \
1301                                        =?iso-8859-1?Q?_se=F1or!?=",
1302        )
1303        .unwrap();
1304        assert_eq!(parsed.get_key(), "Subject");
1305        assert_eq!(parsed.get_key_ref(), "Subject");
1306        assert_eq!(parsed.get_value(), "\u{a1}Hola, se\u{f1}or!");
1307        assert_eq!(
1308            parsed.get_value_raw(),
1309            "=?iso-8859-1?Q?=A1Hola,?=\n \
1310                                          =?iso-8859-1?Q?_se=F1or!?="
1311                .as_bytes()
1312        );
1313
1314        let (parsed, _) = parse_header(b"Euro: =?utf-8?Q?=E2=82=AC?=").unwrap();
1315        assert_eq!(parsed.get_key(), "Euro");
1316        assert_eq!(parsed.get_key_ref(), "Euro");
1317        assert_eq!(parsed.get_value(), "\u{20ac}");
1318        assert_eq!(parsed.get_value_raw(), "=?utf-8?Q?=E2=82=AC?=".as_bytes());
1319
1320        let (parsed, _) = parse_header(b"HelloWorld: =?utf-8?B?aGVsbG8gd29ybGQ=?=").unwrap();
1321        assert_eq!(parsed.get_value(), "hello world");
1322        assert_eq!(
1323            parsed.get_value_raw(),
1324            "=?utf-8?B?aGVsbG8gd29ybGQ=?=".as_bytes()
1325        );
1326
1327        let (parsed, _) = parse_header(b"Empty: =?utf-8?Q??=").unwrap();
1328        assert_eq!(parsed.get_value(), "");
1329        assert_eq!(parsed.get_value_raw(), "=?utf-8?Q??=".as_bytes());
1330
1331        let (parsed, _) = parse_header(b"Incomplete: =?").unwrap();
1332        assert_eq!(parsed.get_value(), "=?");
1333        assert_eq!(parsed.get_value_raw(), "=?".as_bytes());
1334
1335        let (parsed, _) = parse_header(b"BadEncoding: =?garbage?Q??=").unwrap();
1336        assert_eq!(parsed.get_value(), "=?garbage?Q??=");
1337        assert_eq!(parsed.get_value_raw(), "=?garbage?Q??=".as_bytes());
1338
1339        let (parsed, _) = parse_header(b"Invalid: =?utf-8?Q?=E2=AC?=").unwrap();
1340        assert_eq!(parsed.get_value(), "\u{fffd}");
1341
1342        let (parsed, _) = parse_header(b"LineBreak: =?utf-8?Q?=E2=82\n =AC?=").unwrap();
1343        assert_eq!(parsed.get_value(), "=?utf-8?Q?=E2=82 =AC?=");
1344
1345        let (parsed, _) = parse_header(b"NotSeparateWord: hello=?utf-8?Q?world?=").unwrap();
1346        assert_eq!(parsed.get_value(), "hello=?utf-8?Q?world?=");
1347
1348        let (parsed, _) = parse_header(b"NotSeparateWord2: =?utf-8?Q?hello?=world").unwrap();
1349        assert_eq!(parsed.get_value(), "=?utf-8?Q?hello?=world");
1350
1351        let (parsed, _) = parse_header(b"Key: \"=?utf-8?Q?value?=\"").unwrap();
1352        assert_eq!(parsed.get_value(), "\"value\"");
1353
1354        let (parsed, _) = parse_header(b"Subject: =?utf-8?q?=5BOntario_Builder=5D_Understanding_home_shopping_=E2=80=93_a_q?=\n \
1355                                        =?utf-8?q?uick_survey?=")
1356            .unwrap();
1357        assert_eq!(parsed.get_key(), "Subject");
1358        assert_eq!(parsed.get_key_ref(), "Subject");
1359        assert_eq!(
1360            parsed.get_value(),
1361            "[Ontario Builder] Understanding home shopping \u{2013} a quick survey"
1362        );
1363
1364        let (parsed, _) = parse_header(
1365            b"Subject: =?utf-8?q?=5BOntario_Builder=5D?= non-qp words\n \
1366             and the subject continues",
1367        )
1368        .unwrap();
1369        assert_eq!(
1370            parsed.get_value(),
1371            "[Ontario Builder] non-qp words and the subject continues"
1372        );
1373
1374        let (parsed, _) = parse_header(
1375            b"Subject: =?utf-8?q?=5BOntario_Builder=5D?= \n \
1376             and the subject continues",
1377        )
1378        .unwrap();
1379        assert_eq!(
1380            parsed.get_value(),
1381            "[Ontario Builder]  and the subject continues"
1382        );
1383        assert_eq!(
1384            parsed.get_value_raw(),
1385            "=?utf-8?q?=5BOntario_Builder=5D?= \n \
1386               and the subject continues"
1387                .as_bytes()
1388        );
1389
1390        let (parsed, _) = parse_header(b"Subject: =?ISO-2022-JP?B?GyRCRnwbKEI=?=\n\t=?ISO-2022-JP?B?GyRCS1wbKEI=?=\n\t=?ISO-2022-JP?B?GyRCOGwbKEI=?=")
1391            .unwrap();
1392        assert_eq!(parsed.get_key(), "Subject");
1393        assert_eq!(parsed.get_key_ref(), "Subject");
1394        assert_eq!(parsed.get_key_raw(), "Subject".as_bytes());
1395        assert_eq!(parsed.get_value(), "\u{65E5}\u{672C}\u{8A9E}");
1396        assert_eq!(parsed.get_value_raw(), "=?ISO-2022-JP?B?GyRCRnwbKEI=?=\n\t=?ISO-2022-JP?B?GyRCS1wbKEI=?=\n\t=?ISO-2022-JP?B?GyRCOGwbKEI=?=".as_bytes());
1397
1398        let (parsed, _) = parse_header(b"Subject: =?ISO-2022-JP?Q?=1B\x24\x42\x46\x7C=1B\x28\x42?=\n\t=?ISO-2022-JP?Q?=1B\x24\x42\x4B\x5C=1B\x28\x42?=\n\t=?ISO-2022-JP?Q?=1B\x24\x42\x38\x6C=1B\x28\x42?=")
1399            .unwrap();
1400        assert_eq!(parsed.get_key(), "Subject");
1401        assert_eq!(parsed.get_key_ref(), "Subject");
1402        assert_eq!(parsed.get_key_raw(), "Subject".as_bytes());
1403        assert_eq!(parsed.get_value(), "\u{65E5}\u{672C}\u{8A9E}");
1404        assert_eq!(parsed.get_value_raw(), "=?ISO-2022-JP?Q?=1B\x24\x42\x46\x7C=1B\x28\x42?=\n\t=?ISO-2022-JP?Q?=1B\x24\x42\x4B\x5C=1B\x28\x42?=\n\t=?ISO-2022-JP?Q?=1B\x24\x42\x38\x6C=1B\x28\x42?=".as_bytes());
1405
1406        let (parsed, _) = parse_header(b"Subject: =?UTF-7?Q?+JgM-?=").unwrap();
1407        assert_eq!(parsed.get_key(), "Subject");
1408        assert_eq!(parsed.get_key_ref(), "Subject");
1409        assert_eq!(parsed.get_key_raw(), "Subject".as_bytes());
1410        assert_eq!(parsed.get_value(), "\u{2603}");
1411        assert_eq!(parsed.get_value_raw(), b"=?UTF-7?Q?+JgM-?=");
1412
1413        let (parsed, _) =
1414            parse_header(b"Content-Type: image/jpeg; name=\"=?UTF-8?B?MDY2MTM5ODEuanBn?=\"")
1415                .unwrap();
1416        assert_eq!(parsed.get_key(), "Content-Type");
1417        assert_eq!(parsed.get_key_ref(), "Content-Type");
1418        assert_eq!(parsed.get_key_raw(), "Content-Type".as_bytes());
1419        assert_eq!(parsed.get_value(), "image/jpeg; name=\"06613981.jpg\"");
1420        assert_eq!(
1421            parsed.get_value_raw(),
1422            "image/jpeg; name=\"=?UTF-8?B?MDY2MTM5ODEuanBn?=\"".as_bytes()
1423        );
1424
1425        let (parsed, _) = parse_header(
1426            b"From: =?UTF-8?Q?\"Motorola_Owners=E2=80=99_Forums\"_?=<forums@motorola.com>",
1427        )
1428        .unwrap();
1429        assert_eq!(parsed.get_key(), "From");
1430        assert_eq!(parsed.get_key_ref(), "From");
1431        assert_eq!(parsed.get_key_raw(), "From".as_bytes());
1432        assert_eq!(
1433            parsed.get_value(),
1434            "\"Motorola Owners\u{2019} Forums\" <forums@motorola.com>"
1435        );
1436    }
1437
1438    #[test]
1439    fn encoded_words_and_spaces() {
1440        let (parsed, _) = parse_header(b"K: an =?utf-8?q?encoded?=\n word").unwrap();
1441        assert_eq!(parsed.get_value(), "an encoded word");
1442        assert_eq!(
1443            parsed.get_value_raw(),
1444            "an =?utf-8?q?encoded?=\n word".as_bytes()
1445        );
1446
1447        let (parsed, _) = parse_header(b"K: =?utf-8?q?glue?= =?utf-8?q?these?= \n words").unwrap();
1448        assert_eq!(parsed.get_value(), "gluethese  words");
1449        assert_eq!(
1450            parsed.get_value_raw(),
1451            "=?utf-8?q?glue?= =?utf-8?q?these?= \n words".as_bytes()
1452        );
1453
1454        let (parsed, _) = parse_header(b"K: =?utf-8?q?glue?= \n =?utf-8?q?again?=").unwrap();
1455        assert_eq!(parsed.get_value(), "glueagain");
1456        assert_eq!(
1457            parsed.get_value_raw(),
1458            "=?utf-8?q?glue?= \n =?utf-8?q?again?=".as_bytes()
1459        );
1460    }
1461
1462    #[test]
1463    fn parse_multiple_headers() {
1464        let (parsed, _) = parse_headers(b"Key: Value\nTwo: Second").unwrap();
1465        assert_eq!(parsed.len(), 2);
1466        assert_eq!(parsed[0].key, b"Key");
1467        assert_eq!(parsed[0].value, b"Value");
1468        assert_eq!(parsed[1].key, b"Two");
1469        assert_eq!(parsed[1].value, b"Second");
1470
1471        let (parsed, _) =
1472            parse_headers(b"Key: Value\n Overhang\nTwo: Second\nThree: Third").unwrap();
1473        assert_eq!(parsed.len(), 3);
1474        assert_eq!(parsed[0].key, b"Key");
1475        assert_eq!(parsed[0].value, b"Value\n Overhang");
1476        assert_eq!(parsed[1].key, b"Two");
1477        assert_eq!(parsed[1].value, b"Second");
1478        assert_eq!(parsed[2].key, b"Three");
1479        assert_eq!(parsed[2].value, b"Third");
1480
1481        let (parsed, _) = parse_headers(b"Key: Value\nTwo: Second\n\nBody").unwrap();
1482        assert_eq!(parsed.len(), 2);
1483        assert_eq!(parsed[0].key, b"Key");
1484        assert_eq!(parsed[0].value, b"Value");
1485        assert_eq!(parsed[1].key, b"Two");
1486        assert_eq!(parsed[1].value, b"Second");
1487
1488        let (parsed, _) = parse_headers(
1489            concat!(
1490                "Return-Path: <kats@foobar.staktrace.com>\n",
1491                "X-Original-To: kats@baz.staktrace.com\n",
1492                "Delivered-To: kats@baz.staktrace.com\n",
1493                "Received: from foobar.staktrace.com (localhost [127.0.0.1])\n",
1494                "    by foobar.staktrace.com (Postfix) with ESMTP id \
1495                 139F711C1C34\n",
1496                "    for <kats@baz.staktrace.com>; Fri, 27 May 2016 02:34:26 \
1497                 -0400 (EDT)\n",
1498                "Date: Fri, 27 May 2016 02:34:25 -0400\n",
1499                "To: kats@baz.staktrace.com\n",
1500                "From: kats@foobar.staktrace.com\n",
1501                "Subject: test Fri, 27 May 2016 02:34:25 -0400\n",
1502                "X-Mailer: swaks v20130209.0 jetmore.org/john/code/swaks/\n",
1503                "Message-Id: \
1504                 <20160527063426.139F711C1C34@foobar.staktrace.com>\n",
1505                "\n",
1506                "This is a test mailing\n"
1507            )
1508            .as_bytes(),
1509        )
1510        .unwrap();
1511        assert_eq!(parsed.len(), 10);
1512        assert_eq!(parsed[0].key, b"Return-Path");
1513        assert_eq!(parsed[9].key, b"Message-Id");
1514
1515        let (parsed, _) =
1516            parse_headers(b"Key: Value\nAnotherKey: AnotherValue\nKey: Value2\nKey: Value3\n")
1517                .unwrap();
1518        assert_eq!(parsed.len(), 4);
1519        assert_eq!(parsed.get_first_value("Key"), Some("Value".to_string()));
1520        assert_eq!(
1521            parsed.get_all_values("Key"),
1522            vec!["Value", "Value2", "Value3"]
1523        );
1524        assert_eq!(
1525            parsed.get_first_value("AnotherKey"),
1526            Some("AnotherValue".to_string())
1527        );
1528        assert_eq!(parsed.get_all_values("AnotherKey"), vec!["AnotherValue"]);
1529        assert_eq!(parsed.get_first_value("NoKey"), None);
1530        assert_eq!(parsed.get_all_values("NoKey"), Vec::<String>::new());
1531
1532        let (parsed, _) = parse_headers(b"Key: value\r\nWith: CRLF\r\n\r\nBody").unwrap();
1533        assert_eq!(parsed.len(), 2);
1534        assert_eq!(parsed.get_first_value("Key"), Some("value".to_string()));
1535        assert_eq!(parsed.get_first_value("With"), Some("CRLF".to_string()));
1536
1537        let (parsed, _) = parse_headers(b"Bad\nKey\n").unwrap();
1538        assert_eq!(parsed.len(), 2);
1539        assert_eq!(parsed.get_first_value("Bad"), Some("".to_string()));
1540        assert_eq!(parsed.get_first_value("Key"), Some("".to_string()));
1541
1542        let (parsed, _) = parse_headers(b"K:V\nBad\nKey").unwrap();
1543        assert_eq!(parsed.len(), 3);
1544        assert_eq!(parsed.get_first_value("K"), Some("V".to_string()));
1545        assert_eq!(parsed.get_first_value("Bad"), Some("".to_string()));
1546        assert_eq!(parsed.get_first_value("Key"), Some("".to_string()));
1547    }
1548
1549    #[test]
1550    fn test_parse_content_type() {
1551        let ctype = parse_content_type("text/html; charset=utf-8");
1552        assert_eq!(ctype.mimetype, "text/html");
1553        assert_eq!(ctype.charset, "utf-8");
1554        assert_eq!(ctype.params.get("boundary"), None);
1555
1556        let ctype = parse_content_type(" foo/bar; x=y; charset=\"fake\" ; x2=y2");
1557        assert_eq!(ctype.mimetype, "foo/bar");
1558        assert_eq!(ctype.charset, "fake");
1559        assert_eq!(ctype.params.get("boundary"), None);
1560
1561        let ctype = parse_content_type(" multipart/bar; boundary=foo ");
1562        assert_eq!(ctype.mimetype, "multipart/bar");
1563        assert_eq!(ctype.charset, "us-ascii");
1564        assert_eq!(ctype.params.get("boundary").unwrap(), "foo");
1565    }
1566
1567    #[test]
1568    fn test_parse_content_disposition() {
1569        let dis = parse_content_disposition("inline");
1570        assert_eq!(dis.disposition, DispositionType::Inline);
1571        assert_eq!(dis.params.get("name"), None);
1572        assert_eq!(dis.params.get("filename"), None);
1573
1574        let dis = parse_content_disposition(
1575            " attachment; x=y; charset=\"fake\" ; x2=y2; name=\"King Joffrey.death\"",
1576        );
1577        assert_eq!(dis.disposition, DispositionType::Attachment);
1578        assert_eq!(
1579            dis.params.get("name"),
1580            Some(&"King Joffrey.death".to_string())
1581        );
1582        assert_eq!(dis.params.get("filename"), None);
1583
1584        let dis = parse_content_disposition(" form-data");
1585        assert_eq!(dis.disposition, DispositionType::FormData);
1586        assert_eq!(dis.params.get("name"), None);
1587        assert_eq!(dis.params.get("filename"), None);
1588    }
1589
1590    #[test]
1591    fn test_parse_mail() {
1592        let mail = parse_mail(b"Key: value\r\n\r\nSome body stuffs").unwrap();
1593        assert_eq!(mail.header_bytes, b"Key: value\r\n\r\n");
1594        assert_eq!(mail.headers.len(), 1);
1595        assert_eq!(mail.headers[0].get_key(), "Key");
1596        assert_eq!(mail.headers[0].get_key_ref(), "Key");
1597        assert_eq!(mail.headers[0].get_value(), "value");
1598        assert_eq!(mail.ctype.mimetype, "text/plain");
1599        assert_eq!(mail.ctype.charset, "us-ascii");
1600        assert_eq!(mail.ctype.params.get("boundary"), None);
1601        assert_eq!(mail.body_bytes, b"Some body stuffs");
1602        assert_eq!(mail.get_body_raw().unwrap(), b"Some body stuffs");
1603        assert_eq!(mail.get_body().unwrap(), "Some body stuffs");
1604        assert_eq!(mail.subparts.len(), 0);
1605
1606        let mail = parse_mail(
1607            concat!(
1608                "Content-Type: MULTIpart/alternative; bounDAry=myboundary\r\n\r\n",
1609                "--myboundary\r\n",
1610                "Content-Type: text/plain\r\n\r\n",
1611                "This is the plaintext version.\r\n",
1612                "--myboundary\r\n",
1613                "Content-Type: text/html;chARset=utf-8\r\n\r\n",
1614                "This is the <b>HTML</b> version with fake --MYBOUNDARY.\r\n",
1615                "--myboundary--"
1616            )
1617            .as_bytes(),
1618        )
1619        .unwrap();
1620        assert_eq!(mail.headers.len(), 1);
1621        assert_eq!(mail.headers[0].get_key(), "Content-Type");
1622        assert_eq!(mail.headers[0].get_key_ref(), "Content-Type");
1623        assert_eq!(mail.ctype.mimetype, "multipart/alternative");
1624        assert_eq!(mail.ctype.charset, "us-ascii");
1625        assert_eq!(mail.ctype.params.get("boundary").unwrap(), "myboundary");
1626        assert_eq!(mail.subparts.len(), 2);
1627        assert_eq!(mail.subparts[0].headers.len(), 1);
1628        assert_eq!(mail.subparts[0].ctype.mimetype, "text/plain");
1629        assert_eq!(mail.subparts[0].ctype.charset, "us-ascii");
1630        assert_eq!(mail.subparts[0].ctype.params.get("boundary"), None);
1631        assert_eq!(mail.subparts[1].ctype.mimetype, "text/html");
1632        assert_eq!(mail.subparts[1].ctype.charset, "utf-8");
1633        assert_eq!(mail.subparts[1].ctype.params.get("boundary"), None);
1634
1635        let mail =
1636            parse_mail(b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=").unwrap();
1637        assert_eq!(mail.get_body_raw().unwrap(), b"hello world");
1638        assert_eq!(mail.get_body().unwrap(), "hello world");
1639
1640        let mail =
1641            parse_mail(b"Content-Type: text/plain; charset=x-unknown\r\n\r\nhello world").unwrap();
1642        assert_eq!(mail.get_body_raw().unwrap(), b"hello world");
1643        assert_eq!(mail.get_body().unwrap(), "hello world");
1644
1645        let mail = parse_mail(b"ConTENT-tyPE: text/html\r\n\r\nhello world").unwrap();
1646        assert_eq!(mail.ctype.mimetype, "text/html");
1647        assert_eq!(mail.get_body_raw().unwrap(), b"hello world");
1648        assert_eq!(mail.get_body().unwrap(), "hello world");
1649
1650        let mail = parse_mail(
1651            b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n+JgM-",
1652        ).unwrap();
1653        assert_eq!(mail.get_body_raw().unwrap(), b"+JgM-");
1654        assert_eq!(mail.get_body().unwrap(), "\u{2603}");
1655
1656        let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\n\r\n+JgM-").unwrap();
1657        assert_eq!(mail.get_body_raw().unwrap(), b"+JgM-");
1658        assert_eq!(mail.get_body().unwrap(), "\u{2603}");
1659    }
1660
1661    #[test]
1662    fn test_missing_terminating_boundary() {
1663        let mail = parse_mail(
1664            concat!(
1665                "Content-Type: multipart/alternative; boundary=myboundary\r\n\r\n",
1666                "--myboundary\r\n",
1667                "Content-Type: text/plain\r\n\r\n",
1668                "part0\r\n",
1669                "--myboundary\r\n",
1670                "Content-Type: text/html\r\n\r\n",
1671                "part1\r\n"
1672            )
1673            .as_bytes(),
1674        )
1675        .unwrap();
1676        assert_eq!(mail.subparts[0].get_body().unwrap(), "part0");
1677        assert_eq!(mail.subparts[1].get_body().unwrap(), "part1\r\n");
1678    }
1679
1680    #[test]
1681    fn test_missing_body() {
1682        let parsed =
1683            parse_mail("Content-Type: multipart/related; boundary=\"----=_\"\n".as_bytes())
1684                .unwrap();
1685        assert_eq!(parsed.headers[0].get_key(), "Content-Type");
1686        assert_eq!(parsed.get_body_raw().unwrap(), b"");
1687        assert_eq!(parsed.get_body().unwrap(), "");
1688    }
1689
1690    #[test]
1691    fn test_no_headers_in_subpart() {
1692        let mail = parse_mail(
1693            concat!(
1694                "Content-Type: multipart/report; report-type=delivery-status;\n",
1695                "\tboundary=\"1404630116.22555.postech.q0.x.x.x\"\n",
1696                "\n",
1697                "--1404630116.22555.postech.q0.x.x.x\n",
1698                "\n",
1699                "--1404630116.22555.postech.q0.x.x.x--\n"
1700            )
1701            .as_bytes(),
1702        )
1703        .unwrap();
1704        assert_eq!(mail.ctype.mimetype, "multipart/report");
1705        assert_eq!(mail.subparts[0].headers.len(), 0);
1706        assert_eq!(mail.subparts[0].ctype.mimetype, "text/plain");
1707        assert_eq!(mail.subparts[0].get_body_raw().unwrap(), b"");
1708        assert_eq!(mail.subparts[0].get_body().unwrap(), "");
1709    }
1710
1711    #[test]
1712    fn test_empty() {
1713        let mail = parse_mail("".as_bytes()).unwrap();
1714        assert_eq!(mail.get_body_raw().unwrap(), b"");
1715        assert_eq!(mail.get_body().unwrap(), "");
1716    }
1717
1718    #[test]
1719    fn test_dont_panic_for_value_with_new_lines() {
1720        let parsed = parse_param_content(r#"application/octet-stream; name=""#);
1721        assert_eq!(parsed.params["name"], "\"");
1722    }
1723
1724    #[test]
1725    fn test_parameter_value_continuations() {
1726        let parsed =
1727            parse_param_content("attachment;\n\tfilename*0=\"X\";\n\tfilename*1=\"Y.pdf\"");
1728        assert_eq!(parsed.value, "attachment");
1729        assert_eq!(parsed.params["filename"], "XY.pdf");
1730        assert_eq!(parsed.params.contains_key("filename*0"), false);
1731        assert_eq!(parsed.params.contains_key("filename*1"), false);
1732
1733        let parsed = parse_param_content(
1734            "attachment;\n\tfilename=XX.pdf;\n\tfilename*0=\"X\";\n\tfilename*1=\"Y.pdf\"",
1735        );
1736        assert_eq!(parsed.value, "attachment");
1737        assert_eq!(parsed.params["filename"], "XX.pdf");
1738        assert_eq!(parsed.params["filename*0"], "X");
1739        assert_eq!(parsed.params["filename*1"], "Y.pdf");
1740
1741        let parsed = parse_param_content("attachment; filename*1=\"Y.pdf\"");
1742        assert_eq!(parsed.params["filename*1"], "Y.pdf");
1743        assert_eq!(parsed.params.contains_key("filename"), false);
1744    }
1745
1746    #[test]
1747    fn test_parameter_encodings() {
1748        let parsed = parse_param_content("attachment;\n\tfilename*0*=us-ascii''%28X%29%20801%20-%20X;\n\tfilename*1*=%20%E2%80%93%20X%20;\n\tfilename*2*=X%20X%2Epdf");
1749        // Note this is a real-world case from mutt, but it's wrong. The original filename had an en dash \u{2013} but mutt
1750        // declared us-ascii as the encoding instead of utf-8 for some reason.
1751        assert_eq!(
1752            parsed.params["filename"],
1753            "(X) 801 - X \u{00E2}\u{20AC}\u{201C} X X X.pdf"
1754        );
1755        assert_eq!(parsed.params.contains_key("filename*0*"), false);
1756        assert_eq!(parsed.params.contains_key("filename*0"), false);
1757        assert_eq!(parsed.params.contains_key("filename*1*"), false);
1758        assert_eq!(parsed.params.contains_key("filename*1"), false);
1759        assert_eq!(parsed.params.contains_key("filename*2*"), false);
1760        assert_eq!(parsed.params.contains_key("filename*2"), false);
1761
1762        // Here is the corrected version.
1763        let parsed = parse_param_content("attachment;\n\tfilename*0*=utf-8''%28X%29%20801%20-%20X;\n\tfilename*1*=%20%E2%80%93%20X%20;\n\tfilename*2*=X%20X%2Epdf");
1764        assert_eq!(parsed.params["filename"], "(X) 801 - X \u{2013} X X X.pdf");
1765        assert_eq!(parsed.params.contains_key("filename*0*"), false);
1766        assert_eq!(parsed.params.contains_key("filename*0"), false);
1767        assert_eq!(parsed.params.contains_key("filename*1*"), false);
1768        assert_eq!(parsed.params.contains_key("filename*1"), false);
1769        assert_eq!(parsed.params.contains_key("filename*2*"), false);
1770        assert_eq!(parsed.params.contains_key("filename*2"), false);
1771        let parsed = parse_param_content("attachment; filename*=utf-8'en'%e2%80%A1.bin");
1772        assert_eq!(parsed.params["filename"], "\u{2021}.bin");
1773        assert_eq!(parsed.params.contains_key("filename*"), false);
1774
1775        let parsed = parse_param_content("attachment; filename*='foo'%e2%80%A1.bin");
1776        assert_eq!(parsed.params["filename*"], "'foo'%e2%80%A1.bin");
1777        assert_eq!(parsed.params.contains_key("filename"), false);
1778
1779        let parsed = parse_param_content("attachment; filename*=nonexistent'foo'%e2%80%a1.bin");
1780        assert_eq!(parsed.params["filename*"], "nonexistent'foo'%e2%80%a1.bin");
1781        assert_eq!(parsed.params.contains_key("filename"), false);
1782
1783        let parsed = parse_param_content(
1784            "attachment; filename*0*=utf-8'en'%e2%80%a1; filename*1*=%e2%80%A1.bin",
1785        );
1786        assert_eq!(parsed.params["filename"], "\u{2021}\u{2021}.bin");
1787        assert_eq!(parsed.params.contains_key("filename*0*"), false);
1788        assert_eq!(parsed.params.contains_key("filename*0"), false);
1789        assert_eq!(parsed.params.contains_key("filename*1*"), false);
1790        assert_eq!(parsed.params.contains_key("filename*1"), false);
1791
1792        let parsed =
1793            parse_param_content("attachment; filename*0*=utf-8'en'%e2%80%a1; filename*1=%20.bin");
1794        assert_eq!(parsed.params["filename"], "\u{2021}%20.bin");
1795        assert_eq!(parsed.params.contains_key("filename*0*"), false);
1796        assert_eq!(parsed.params.contains_key("filename*0"), false);
1797        assert_eq!(parsed.params.contains_key("filename*1*"), false);
1798        assert_eq!(parsed.params.contains_key("filename*1"), false);
1799
1800        let parsed =
1801            parse_param_content("attachment; filename*0*=utf-8'en'%e2%80%a1; filename*2*=%20.bin");
1802        assert_eq!(parsed.params["filename"], "\u{2021}");
1803        assert_eq!(parsed.params["filename*2"], " .bin");
1804        assert_eq!(parsed.params.contains_key("filename*0*"), false);
1805        assert_eq!(parsed.params.contains_key("filename*0"), false);
1806        assert_eq!(parsed.params.contains_key("filename*2*"), false);
1807
1808        let parsed =
1809            parse_param_content("attachment; filename*0*=utf-8'en'%e2%80%a1; filename*0=foo.bin");
1810        assert_eq!(parsed.params["filename"], "foo.bin");
1811        assert_eq!(parsed.params["filename*0*"], "utf-8'en'%e2%80%a1");
1812        assert_eq!(parsed.params.contains_key("filename*0"), false);
1813    }
1814
1815    #[test]
1816    fn test_default_content_encoding() {
1817        let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\n\r\n+JgM-").unwrap();
1818        let body = mail.get_body_encoded();
1819        match body {
1820            Body::SevenBit(body) => {
1821                assert_eq!(body.get_raw(), b"+JgM-");
1822                assert_eq!(body.get_as_string().unwrap(), "\u{2603}");
1823            }
1824            _ => assert!(false),
1825        };
1826    }
1827
1828    #[test]
1829    fn test_7bit_content_encoding() {
1830        let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: 7bit\r\n\r\n+JgM-").unwrap();
1831        let body = mail.get_body_encoded();
1832        match body {
1833            Body::SevenBit(body) => {
1834                assert_eq!(body.get_raw(), b"+JgM-");
1835                assert_eq!(body.get_as_string().unwrap(), "\u{2603}");
1836            }
1837            _ => assert!(false),
1838        };
1839    }
1840
1841    #[test]
1842    fn test_8bit_content_encoding() {
1843        let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: 8bit\r\n\r\n+JgM-").unwrap();
1844        let body = mail.get_body_encoded();
1845        match body {
1846            Body::EightBit(body) => {
1847                assert_eq!(body.get_raw(), b"+JgM-");
1848                assert_eq!(body.get_as_string().unwrap(), "\u{2603}");
1849            }
1850            _ => assert!(false),
1851        };
1852    }
1853
1854    #[test]
1855    fn test_quoted_printable_content_encoding() {
1856        let mail = parse_mail(
1857            b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n+JgM-",
1858        ).unwrap();
1859        match mail.get_body_encoded() {
1860            Body::QuotedPrintable(body) => {
1861                assert_eq!(body.get_raw(), b"+JgM-");
1862                assert_eq!(body.get_decoded().unwrap(), b"+JgM-");
1863                assert_eq!(body.get_decoded_as_string().unwrap(), "\u{2603}");
1864            }
1865            _ => assert!(false),
1866        };
1867    }
1868
1869    #[test]
1870    fn test_base64_content_encoding() {
1871        let mail =
1872            parse_mail(b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=").unwrap();
1873        match mail.get_body_encoded() {
1874            Body::Base64(body) => {
1875                assert_eq!(body.get_raw(), b"aGVsbG 8gd\r\n29ybGQ=");
1876                assert_eq!(body.get_decoded().unwrap(), b"hello world");
1877                assert_eq!(body.get_decoded_as_string().unwrap(), "hello world");
1878            }
1879            _ => assert!(false),
1880        };
1881    }
1882
1883    #[test]
1884    fn test_base64_content_encoding_multiple_strings() {
1885        let mail = parse_mail(
1886            b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=\r\nZm9vCg==",
1887        )
1888        .unwrap();
1889        match mail.get_body_encoded() {
1890            Body::Base64(body) => {
1891                assert_eq!(body.get_raw(), b"aGVsbG 8gd\r\n29ybGQ=\r\nZm9vCg==");
1892                assert_eq!(body.get_decoded().unwrap(), b"hello worldfoo\n");
1893                assert_eq!(body.get_decoded_as_string().unwrap(), "hello worldfoo\n");
1894            }
1895            _ => assert!(false),
1896        };
1897    }
1898
1899    #[test]
1900    fn test_binary_content_encoding() {
1901        let mail = parse_mail(b"Content-Transfer-Encoding: binary\r\n\r\n######").unwrap();
1902        let body = mail.get_body_encoded();
1903        match body {
1904            Body::Binary(body) => {
1905                assert_eq!(body.get_raw(), b"######");
1906            }
1907            _ => assert!(false),
1908        };
1909    }
1910
1911    #[test]
1912    fn test_body_content_encoding_with_multipart() {
1913        let mail_filepath = "./tests/files/test_email_01.txt";
1914        let mail = std::fs::read(mail_filepath)
1915            .expect(&format!("Unable to open the file [{}]", mail_filepath));
1916        let mail = parse_mail(&mail).unwrap();
1917
1918        let subpart_0 = mail.subparts.get(0).unwrap();
1919        match subpart_0.get_body_encoded() {
1920            Body::SevenBit(body) => {
1921                assert_eq!(
1922                    body.get_as_string().unwrap().trim(),
1923                    "<html>Test with attachments</html>"
1924                );
1925            }
1926            _ => assert!(false),
1927        };
1928
1929        let subpart_1 = mail.subparts.get(1).unwrap();
1930        match subpart_1.get_body_encoded() {
1931            Body::Base64(body) => {
1932                let pdf_filepath = "./tests/files/test_email_01_sample.pdf";
1933                let original_pdf = std::fs::read(pdf_filepath)
1934                    .expect(&format!("Unable to open the file [{}]", pdf_filepath));
1935                assert_eq!(body.get_decoded().unwrap(), original_pdf);
1936            }
1937            _ => assert!(false),
1938        };
1939
1940        let subpart_2 = mail.subparts.get(2).unwrap();
1941        match subpart_2.get_body_encoded() {
1942            Body::Base64(body) => {
1943                assert_eq!(
1944                    body.get_decoded_as_string().unwrap(),
1945                    "txt file context for email collector\n1234567890987654321\n"
1946                );
1947            }
1948            _ => assert!(false),
1949        };
1950    }
1951
1952    #[test]
1953    fn test_fuzzer_testcase() {
1954        const INPUT: &str = "U3ViamVjdDplcy1UeXBlOiBtdW50ZW50LVV5cGU6IW11bAAAAAAAAAAAamVjdDplcy1UeXBlOiBtdW50ZW50LVV5cGU6IG11bAAAAAAAAAAAAAAAAABTTUFZdWJqZf86OiP/dCBTdWJqZWN0Ol8KRGF0ZTog/////////////////////wAAAAAAAAAAAHQgYnJmAHQgYnJmZXItRW5jeXBlOnY9NmU3OjA2OgAAAAAAAAAAAAAAADEAAAAAAP/8mAAAAAAAAAAA+f///wAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAPT0/PzEAAAEAAA==";
1955
1956        if let Ok(parsed) = parse_mail(&data_encoding::BASE64.decode(INPUT.as_bytes()).unwrap()) {
1957            if let Some(date) = parsed.headers.get_first_value("Date") {
1958                let _ = dateparse(&date);
1959            }
1960        }
1961    }
1962
1963    #[test]
1964    fn test_fuzzer_testcase_2() {
1965        const INPUT: &str = "U3ViamVjdDogVGhpcyBpcyBhIHRlc3QgZW1haWwKQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PczMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMZm9vYmFyCkRhdGU6IFN1biwgMDIgT2MKCi1TdWJqZWMtZm9vYmFydDo=";
1966        if let Ok(parsed) = parse_mail(&data_encoding::BASE64.decode(INPUT.as_bytes()).unwrap()) {
1967            if let Some(date) = parsed.headers.get_first_value("Date") {
1968                let _ = dateparse(&date);
1969            }
1970        }
1971    }
1972
1973    #[test]
1974    fn test_header_split() {
1975        let mail = parse_mail(
1976            b"Content-Type: text/plain;\r\ncharset=\"utf-8\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
1977        ).unwrap();
1978        assert_eq!(mail.ctype.mimetype, "text/plain");
1979        assert_eq!(mail.ctype.charset, "us-ascii");
1980    }
1981
1982    #[test]
1983    fn test_percent_decoder() {
1984        assert_eq!(percent_decode("hi %0d%0A%%2A%zz%"), b"hi \r\n%*%zz%");
1985    }
1986
1987    #[test]
1988    fn test_default_content_type_in_multipart_digest() {
1989        // Per https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.5
1990        let mail = parse_mail(
1991            concat!(
1992                "Content-Type: multipart/digest; boundary=myboundary\r\n\r\n",
1993                "--myboundary\r\n\r\n",
1994                "blah blah blah\r\n\r\n",
1995                "--myboundary--\r\n"
1996            )
1997            .as_bytes(),
1998        )
1999        .unwrap();
2000        assert_eq!(mail.headers.len(), 1);
2001        assert_eq!(mail.ctype.mimetype, "multipart/digest");
2002        assert_eq!(mail.subparts[0].headers.len(), 0);
2003        assert_eq!(mail.subparts[0].ctype.mimetype, "message/rfc822");
2004
2005        let mail = parse_mail(
2006            concat!(
2007                "Content-Type: multipart/whatever; boundary=myboundary\n",
2008                "\n",
2009                "--myboundary\n",
2010                "\n",
2011                "blah blah blah\n",
2012                "--myboundary\n",
2013                "Content-Type: multipart/digest; boundary=nestedboundary\n",
2014                "\n",
2015                "--nestedboundary\n",
2016                "\n",
2017                "nested default part\n",
2018                "--nestedboundary\n",
2019                "Content-Type: text/html\n",
2020                "\n",
2021                "nested html part\n",
2022                "--nestedboundary\n",
2023                "Content-Type: multipart/insidedigest; boundary=insideboundary\n",
2024                "\n",
2025                "--insideboundary\n",
2026                "\n",
2027                "inside part\n",
2028                "--insideboundary--\n",
2029                "--nestedboundary--\n",
2030                "--myboundary--\n"
2031            )
2032            .as_bytes(),
2033        )
2034        .unwrap();
2035        let mut parts = mail.parts();
2036        let mut part = parts.next().unwrap(); // mail
2037
2038        assert_eq!(part.headers.len(), 1);
2039        assert_eq!(part.ctype.mimetype, "multipart/whatever");
2040
2041        part = parts.next().unwrap(); // mail.subparts[0]
2042        assert_eq!(part.headers.len(), 0);
2043        assert_eq!(part.ctype.mimetype, "text/plain");
2044        assert_eq!(part.get_body_raw().unwrap(), b"blah blah blah");
2045
2046        part = parts.next().unwrap(); // mail.subparts[1]
2047        assert_eq!(part.ctype.mimetype, "multipart/digest");
2048
2049        part = parts.next().unwrap(); // mail.subparts[1].subparts[0]
2050        assert_eq!(part.headers.len(), 0);
2051        assert_eq!(part.ctype.mimetype, "message/rfc822");
2052        assert_eq!(part.get_body_raw().unwrap(), b"nested default part");
2053
2054        part = parts.next().unwrap(); // mail.subparts[1].subparts[1]
2055        assert_eq!(part.headers.len(), 1);
2056        assert_eq!(part.ctype.mimetype, "text/html");
2057        assert_eq!(part.get_body_raw().unwrap(), b"nested html part");
2058
2059        part = parts.next().unwrap(); // mail.subparts[1].subparts[2]
2060        assert_eq!(part.headers.len(), 1);
2061        assert_eq!(part.ctype.mimetype, "multipart/insidedigest");
2062
2063        part = parts.next().unwrap(); // mail.subparts[1].subparts[2].subparts[0]
2064        assert_eq!(part.headers.len(), 0);
2065        assert_eq!(part.ctype.mimetype, "text/plain");
2066        assert_eq!(part.get_body_raw().unwrap(), b"inside part");
2067
2068        assert!(parts.next().is_none());
2069    }
2070
2071    #[test]
2072    fn boundary_is_suffix_of_another_boundary() {
2073        // From https://github.com/staktrace/mailparse/issues/100
2074        let mail = parse_mail(
2075            concat!(
2076                "Content-Type: multipart/mixed; boundary=\"section_boundary\"\n",
2077                "\n",
2078                "--section_boundary\n",
2079                "Content-Type: multipart/alternative; boundary=\"--section_boundary\"\n",
2080                "\n",
2081                "----section_boundary\n",
2082                "Content-Type: text/html;\n",
2083                "\n",
2084                "<em>Good evening!</em>\n",
2085                "----section_boundary\n",
2086                "Content-Type: text/plain;\n",
2087                "\n",
2088                "Good evening!\n",
2089                "----section_boundary\n",
2090                "--section_boundary\n"
2091            )
2092            .as_bytes(),
2093        )
2094        .unwrap();
2095
2096        let mut parts = mail.parts();
2097        let mut part = parts.next().unwrap(); // mail
2098
2099        assert_eq!(part.headers.len(), 1);
2100        assert_eq!(part.ctype.mimetype, "multipart/mixed");
2101        assert_eq!(part.subparts.len(), 1);
2102
2103        part = parts.next().unwrap(); // mail.subparts[0]
2104        assert_eq!(part.headers.len(), 1);
2105        assert_eq!(part.ctype.mimetype, "multipart/alternative");
2106        assert_eq!(part.subparts.len(), 2);
2107
2108        part = parts.next().unwrap(); // mail.subparts[0].subparts[0]
2109        assert_eq!(part.headers.len(), 1);
2110        assert_eq!(part.ctype.mimetype, "text/html");
2111        assert_eq!(part.get_body_raw().unwrap(), b"<em>Good evening!</em>");
2112        assert_eq!(part.subparts.len(), 0);
2113
2114        part = parts.next().unwrap(); // mail.subparts[0].subparts[1]
2115        assert_eq!(part.headers.len(), 1);
2116        assert_eq!(part.ctype.mimetype, "text/plain");
2117        assert_eq!(part.get_body_raw().unwrap(), b"Good evening!");
2118        assert_eq!(part.subparts.len(), 0);
2119
2120        assert!(parts.next().is_none());
2121    }
2122
2123    #[test]
2124    fn test_parts_iterator() {
2125        let mail = parse_mail(
2126            concat!(
2127                "Content-Type: multipart/mixed; boundary=\"top_boundary\"\n",
2128                "\n",
2129                "--top_boundary\n",
2130                "Content-Type: multipart/alternative; boundary=\"internal_boundary\"\n",
2131                "\n",
2132                "--internal_boundary\n",
2133                "Content-Type: text/html;\n",
2134                "\n",
2135                "<em>Good evening!</em>\n",
2136                "--internal_boundary\n",
2137                "Content-Type: text/plain;\n",
2138                "\n",
2139                "Good evening!\n",
2140                "--internal_boundary\n",
2141                "--top_boundary\n",
2142                "Content-Type: text/unknown;\n",
2143                "\n",
2144                "You read this?\n",
2145                "--top_boundary\n"
2146            )
2147            .as_bytes(),
2148        )
2149        .unwrap();
2150
2151        let mut parts = mail.parts();
2152        assert_eq!(parts.next().unwrap().ctype.mimetype, "multipart/mixed");
2153        assert_eq!(
2154            parts.next().unwrap().ctype.mimetype,
2155            "multipart/alternative"
2156        );
2157        assert_eq!(parts.next().unwrap().ctype.mimetype, "text/html");
2158        assert_eq!(parts.next().unwrap().ctype.mimetype, "text/plain");
2159        assert_eq!(parts.next().unwrap().ctype.mimetype, "text/unknown");
2160        assert!(parts.next().is_none());
2161
2162        let mail = parse_mail(concat!("Content-Type: text/plain\n").as_bytes()).unwrap();
2163
2164        let mut parts = mail.parts();
2165        assert_eq!(parts.next().unwrap().ctype.mimetype, "text/plain");
2166        assert!(parts.next().is_none());
2167    }
2168
2169    #[test]
2170    fn test_no_parts() {
2171        let mail = parse_mail(
2172            concat!(
2173                "Content-Type: multipart/mixed; boundary=\"foobar\"\n",
2174                "\n",
2175                "--foobar--\n"
2176            )
2177            .as_bytes(),
2178        )
2179        .unwrap();
2180
2181        let mut parts = mail.parts();
2182        let part = parts.next().unwrap();
2183        assert_eq!(part.ctype.mimetype, "multipart/mixed");
2184
2185        let part = parts.next().unwrap();
2186        assert_eq!(part.ctype.mimetype, "text/plain");
2187        assert!(parts.next().is_none());
2188    }
2189}