mailparsing/
header.rs

1use crate::headermap::{EncodeHeaderValue, HeaderMap};
2use crate::rfc5322_parser::Parser;
3use crate::strings::IntoSharedString;
4use crate::{
5    AddressList, AuthenticationResults, MailParsingError, Mailbox, MailboxList, MessageID,
6    MimeParameters, Result, SharedString,
7};
8use chrono::{DateTime, FixedOffset};
9use std::str::FromStr;
10
11bitflags::bitflags! {
12    #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
13    pub struct MessageConformance: u16 {
14        const MISSING_COLON_VALUE = 0b0000_0001;
15        const NON_CANONICAL_LINE_ENDINGS = 0b0000_0010;
16        const NAME_ENDS_WITH_SPACE = 0b0000_0100;
17        const LINE_TOO_LONG = 0b0000_1000;
18        const NEEDS_TRANSFER_ENCODING = 0b0001_0000;
19        const MISSING_DATE_HEADER = 0b0010_0000;
20        const MISSING_MESSAGE_ID_HEADER = 0b0100_0000;
21        const MISSING_MIME_VERSION = 0b1000_0000;
22        const INVALID_MIME_HEADERS = 0b0001_0000_0000;
23    }
24}
25
26impl FromStr for MessageConformance {
27    type Err = String;
28
29    fn from_str(s: &str) -> std::result::Result<Self, String> {
30        let mut result = Self::default();
31        for ele in s.split('|') {
32            if ele.is_empty() {
33                continue;
34            }
35            match Self::from_name(ele) {
36                Some(v) => {
37                    result = result.union(v);
38                }
39                None => {
40                    let mut possible: Vec<String> = Self::all()
41                        .iter_names()
42                        .map(|(name, _)| format!("'{name}'"))
43                        .collect();
44                    possible.sort();
45                    let possible = possible.join(", ");
46                    return Err(format!(
47                        "invalid MessageConformance flag '{ele}', possible values are {possible}"
48                    ));
49                }
50            }
51        }
52        Ok(result)
53    }
54}
55
56impl ToString for MessageConformance {
57    fn to_string(&self) -> String {
58        let mut names: Vec<&str> = self.iter_names().map(|(name, _)| name).collect();
59        names.sort();
60        names.join("|")
61    }
62}
63
64#[derive(Clone, Debug, PartialEq)]
65pub struct Header<'a> {
66    /// The name portion of the header
67    name: SharedString<'a>,
68    /// The value portion of the header
69    value: SharedString<'a>,
70    /// The separator between the name and the value
71    separator: SharedString<'a>,
72    conformance: MessageConformance,
73}
74
75/// Holds the result of parsing a block of headers
76pub struct HeaderParseResult<'a> {
77    pub headers: HeaderMap<'a>,
78    pub body_offset: usize,
79    pub overall_conformance: MessageConformance,
80}
81
82impl<'a> Header<'a> {
83    pub fn with_name_value<N: Into<SharedString<'a>>, V: Into<SharedString<'a>>>(
84        name: N,
85        value: V,
86    ) -> Self {
87        let name = name.into();
88        let value = value.into();
89        Self {
90            name,
91            value,
92            separator: ": ".into(),
93            conformance: MessageConformance::default(),
94        }
95    }
96
97    pub fn to_owned(&'a self) -> Header<'static> {
98        Header {
99            name: self.name.to_owned(),
100            value: self.value.to_owned(),
101            separator: self.separator.to_owned(),
102            conformance: self.conformance,
103        }
104    }
105
106    pub fn new<N: Into<SharedString<'a>>>(name: N, value: impl EncodeHeaderValue) -> Self {
107        let name = name.into();
108        let value = value.encode_value();
109        Self {
110            name,
111            value,
112            separator: ": ".into(),
113            conformance: MessageConformance::default(),
114        }
115    }
116
117    pub fn new_unstructured<N: Into<SharedString<'a>>, V: Into<SharedString<'a>>>(
118        name: N,
119        value: V,
120    ) -> Self {
121        let name = name.into();
122        let value = value.into();
123
124        let value = if value.is_ascii() {
125            crate::textwrap::wrap(&value)
126        } else {
127            crate::rfc5322_parser::qp_encode(&value)
128        }
129        .into();
130
131        Self {
132            name,
133            value,
134            separator: ": ".into(),
135            conformance: MessageConformance::default(),
136        }
137    }
138
139    pub fn assign(&mut self, v: impl EncodeHeaderValue) {
140        self.value = v.encode_value();
141    }
142
143    /// Format the header into the provided output stream,
144    /// as though writing it out as part of a mime part
145    pub fn write_header<W: std::io::Write>(&self, out: &mut W) -> std::io::Result<()> {
146        let line_ending = if self
147            .conformance
148            .contains(MessageConformance::NON_CANONICAL_LINE_ENDINGS)
149        {
150            "\n"
151        } else {
152            "\r\n"
153        };
154        out.write_all(self.name.as_bytes())?;
155        out.write_all(self.separator.as_bytes())?;
156        out.write_all(self.value.as_bytes())?;
157        out.write_all(line_ending.as_bytes())
158    }
159
160    /// Convenience method wrapping write_header that returns
161    /// the formatted header as a standalone string
162    pub fn to_header_string(&self) -> String {
163        let mut out = vec![];
164        self.write_header(&mut out).unwrap();
165        String::from_utf8_lossy(&out).to_string()
166    }
167
168    pub fn get_name(&self) -> &str {
169        &self.name
170    }
171
172    pub fn get_raw_value(&self) -> &str {
173        &self.value
174    }
175
176    pub fn as_content_transfer_encoding(&self) -> Result<MimeParameters> {
177        Parser::parse_content_transfer_encoding_header(self.get_raw_value())
178    }
179
180    pub fn as_content_disposition(&self) -> Result<MimeParameters> {
181        Parser::parse_content_transfer_encoding_header(self.get_raw_value())
182    }
183
184    pub fn as_content_type(&self) -> Result<MimeParameters> {
185        Parser::parse_content_type_header(self.get_raw_value())
186    }
187
188    /// Parse the header into a mailbox-list (as defined in
189    /// RFC 5322), which is how the `From` and `Resent-From`,
190    /// headers are defined.
191    pub fn as_mailbox_list(&self) -> Result<MailboxList> {
192        Parser::parse_mailbox_list_header(self.get_raw_value())
193    }
194
195    /// Parse the header into a mailbox (as defined in
196    /// RFC 5322), which is how the `Sender` and `Resent-Sender`
197    /// headers are defined.
198    pub fn as_mailbox(&self) -> Result<Mailbox> {
199        Parser::parse_mailbox_header(self.get_raw_value())
200    }
201
202    pub fn as_address_list(&self) -> Result<AddressList> {
203        Parser::parse_address_list_header(self.get_raw_value())
204    }
205
206    pub fn as_message_id(&self) -> Result<MessageID> {
207        Parser::parse_msg_id_header(self.get_raw_value())
208    }
209
210    pub fn as_content_id(&self) -> Result<MessageID> {
211        Parser::parse_content_id_header(self.get_raw_value())
212    }
213
214    pub fn as_message_id_list(&self) -> Result<Vec<MessageID>> {
215        Parser::parse_msg_id_header_list(self.get_raw_value())
216    }
217
218    pub fn as_unstructured(&self) -> Result<String> {
219        Parser::parse_unstructured_header(self.get_raw_value())
220    }
221
222    pub fn as_authentication_results(&self) -> Result<AuthenticationResults> {
223        Parser::parse_authentication_results_header(self.get_raw_value())
224    }
225
226    pub fn as_date(&self) -> Result<DateTime<FixedOffset>> {
227        DateTime::parse_from_rfc2822(self.get_raw_value()).map_err(MailParsingError::ChronoError)
228    }
229
230    pub fn parse_headers<S>(header_block: S) -> Result<HeaderParseResult<'a>>
231    where
232        S: IntoSharedString<'a>,
233    {
234        let (header_block, mut overall_conformance) = header_block.into_shared_string();
235        let mut headers = vec![];
236        let mut idx = 0;
237
238        while idx < header_block.len() {
239            let b = header_block[idx];
240            if b == b'\n' {
241                // LF: End of header block
242                idx += 1;
243                overall_conformance.set(MessageConformance::NON_CANONICAL_LINE_ENDINGS, true);
244                break;
245            }
246            if b == b'\r' {
247                if idx + 1 < header_block.len() && header_block[idx + 1] == b'\n' {
248                    // CRLF: End of header block
249                    idx += 2;
250                    break;
251                }
252                return Err(MailParsingError::HeaderParse(
253                    "lone CR in header".to_string(),
254                ));
255            }
256            if headers.is_empty() && b.is_ascii_whitespace() {
257                return Err(MailParsingError::HeaderParse(
258                    "header block must not start with spaces".to_string(),
259                ));
260            }
261            let (header, next) = Self::parse(header_block.slice(idx..header_block.len()))?;
262            overall_conformance |= header.conformance;
263            headers.push(header);
264            debug_assert!(
265                idx != next + idx,
266                "idx={idx}, next={next}, headers: {headers:#?}"
267            );
268            idx += next;
269        }
270        Ok(HeaderParseResult {
271            headers: HeaderMap::new(headers),
272            body_offset: idx,
273            overall_conformance,
274        })
275    }
276
277    pub fn parse<S: Into<SharedString<'a>>>(header_block: S) -> Result<(Self, usize)> {
278        let header_block = header_block.into();
279
280        enum State {
281            Initial,
282            Name,
283            Separator,
284            Value,
285            NewLine,
286        }
287
288        let mut state = State::Initial;
289
290        let mut iter = header_block.as_bytes().iter();
291        let mut c = *iter
292            .next()
293            .ok_or_else(|| MailParsingError::HeaderParse("empty header string".to_string()))?;
294
295        let mut name_end = None;
296        let mut value_start = 0;
297        let mut value_end = 0;
298
299        let mut idx = 0usize;
300        let mut conformance = MessageConformance::default();
301        let mut saw_cr = false;
302        let mut line_start = 0;
303        let mut max_line_len = 0;
304
305        loop {
306            match state {
307                State::Initial => {
308                    if c.is_ascii_whitespace() {
309                        return Err(MailParsingError::HeaderParse(
310                            "header cannot start with space".to_string(),
311                        ));
312                    }
313                    state = State::Name;
314                    continue;
315                }
316                State::Name => {
317                    if c == b':' {
318                        if name_end.is_none() {
319                            name_end.replace(idx);
320                        }
321                        state = State::Separator;
322                    } else if c == b' ' || c == b'\t' {
323                        if name_end.is_none() {
324                            name_end.replace(idx);
325                        }
326                        conformance.set(MessageConformance::NAME_ENDS_WITH_SPACE, true);
327                    } else if c == b'\n' {
328                        // Got a newline before we finished parsing the name
329                        conformance.set(MessageConformance::MISSING_COLON_VALUE, true);
330                        name_end.replace(idx);
331                        max_line_len = max_line_len.max(idx.saturating_sub(line_start));
332                        value_start = idx;
333                        value_end = idx;
334                        idx += 1;
335                        break;
336                    } else if c != b'\r' && !(33..=126).contains(&c) {
337                        return Err(MailParsingError::HeaderParse(format!(
338                            "header name must be comprised of printable US-ASCII characters. Found {c:?}"
339                        )));
340                    }
341                }
342                State::Separator => {
343                    if c != b' ' {
344                        value_start = idx;
345                        value_end = idx;
346                        state = State::Value;
347                        continue;
348                    }
349                }
350                State::Value => {
351                    if c == b'\n' {
352                        if !saw_cr {
353                            conformance.set(MessageConformance::NON_CANONICAL_LINE_ENDINGS, true);
354                        }
355                        state = State::NewLine;
356                        saw_cr = false;
357                        max_line_len = max_line_len.max(idx.saturating_sub(line_start));
358                        line_start = idx + 1;
359                    } else if c != b'\r' {
360                        value_end = idx + 1;
361                        saw_cr = false;
362                    } else {
363                        saw_cr = true;
364                    }
365                }
366                State::NewLine => {
367                    if c == b' ' || c == b'\t' {
368                        state = State::Value;
369                        continue;
370                    }
371                    break;
372                }
373            }
374            idx += 1;
375            c = match iter.next() {
376                None => break,
377                Some(v) => *v,
378            };
379        }
380
381        max_line_len = max_line_len.max(idx.saturating_sub(line_start));
382        if max_line_len > 78 {
383            conformance.set(MessageConformance::LINE_TOO_LONG, true);
384        }
385
386        let name_end = name_end.unwrap_or_else(|| {
387            conformance.set(MessageConformance::MISSING_COLON_VALUE, true);
388            idx
389        });
390
391        let name = header_block.slice(0..name_end);
392        let value = header_block.slice(value_start..value_end.max(value_start));
393        let separator = header_block.slice(name_end..value_start.max(name_end));
394
395        let header = Self {
396            name,
397            value,
398            separator,
399            conformance,
400        };
401
402        Ok((header, idx))
403    }
404
405    /// Re-constitute the header.
406    /// The header value will be parsed out according to the known schema
407    /// of the associated header name, and the parsed form used
408    /// to build a new version of the header.
409    /// This has the side effect of "fixing" non-conforming elements,
410    /// but may come at the cost of "losing" the non-sensical or otherwise
411    /// out of spec elements in the rebuilt header
412    pub fn rebuild(&self) -> Result<Self> {
413        let name = self.get_name();
414
415        macro_rules! hdr {
416            ($header_name:literal, $func_name:ident, encode) => {
417                if name.eq_ignore_ascii_case($header_name) {
418                    let value = self.$func_name().map_err(|err| {
419                        MailParsingError::HeaderParse(format!(
420                            "rebuilding '{name}' header: {err:#}"
421                        ))
422                    })?;
423                    return Ok(Self::with_name_value($header_name, value.encode_value()));
424                }
425            };
426            ($header_name:literal, unstructured) => {
427                if name.eq_ignore_ascii_case($header_name) {
428                    let value = self.as_unstructured().map_err(|err| {
429                        MailParsingError::HeaderParse(format!(
430                            "rebuilding '{name}' header: {err:#}"
431                        ))
432                    })?;
433                    return Ok(Self::new_unstructured($header_name, value));
434                }
435            };
436        }
437
438        hdr!("From", as_mailbox_list, encode);
439        hdr!("Resent-From", as_mailbox_list, encode);
440        hdr!("Reply-To", as_address_list, encode);
441        hdr!("To", as_address_list, encode);
442        hdr!("Cc", as_address_list, encode);
443        hdr!("Bcc", as_address_list, encode);
444        hdr!("Resent-To", as_address_list, encode);
445        hdr!("Resent-Cc", as_address_list, encode);
446        hdr!("Resent-Bcc", as_address_list, encode);
447        hdr!("Date", as_date, encode);
448        hdr!("Sender", as_mailbox, encode);
449        hdr!("Resent-Sender", as_mailbox, encode);
450        hdr!("Message-ID", as_message_id, encode);
451        hdr!("Content-ID", as_content_id, encode);
452        hdr!("Content-Type", as_content_type, encode);
453        hdr!(
454            "Content-Transfer-Encoding",
455            as_content_transfer_encoding,
456            encode
457        );
458        hdr!("Content-Disposition", as_content_disposition, encode);
459        hdr!("References", as_message_id_list, encode);
460        hdr!("Subject", unstructured);
461        hdr!("Comments", unstructured);
462        hdr!("Mime-Version", unstructured);
463
464        // Assume unstructured
465        let value = self.as_unstructured().map_err(|err| {
466            MailParsingError::HeaderParse(format!("rebuilding '{name}' header: {err:#}"))
467        })?;
468        Ok(Self::new_unstructured(name.to_string(), value))
469    }
470}
471
472#[cfg(test)]
473mod test {
474    use super::*;
475    use crate::AddrSpec;
476
477    fn assert_static_lifetime(_header: Header<'static>) {
478        assert!(true, "I wouldn't compile if this wasn't true");
479    }
480
481    #[test]
482    fn header_construction() {
483        let header = Header::with_name_value("To", "someone@example.com");
484        assert_eq!(header.get_name(), "To");
485        assert_eq!(header.get_raw_value(), "someone@example.com");
486        assert_eq!(header.to_header_string(), "To: someone@example.com\r\n");
487        assert_static_lifetime(header);
488    }
489
490    #[test]
491    fn header_parsing() {
492        let message = concat!(
493            "Subject: hello there\n",
494            "From:  Someone <someone@example.com>\n",
495            "\n",
496            "I am the body"
497        );
498
499        let HeaderParseResult {
500            headers,
501            body_offset,
502            overall_conformance,
503        } = Header::parse_headers(message).unwrap();
504        assert_eq!(&message[body_offset..], "I am the body");
505        k9::snapshot!(
506            overall_conformance,
507            "
508MessageConformance(
509    NON_CANONICAL_LINE_ENDINGS,
510)
511"
512        );
513        k9::snapshot!(
514            headers,
515            r#"
516HeaderMap {
517    headers: [
518        Header {
519            name: "Subject",
520            value: "hello there",
521            separator: ": ",
522            conformance: MessageConformance(
523                NON_CANONICAL_LINE_ENDINGS,
524            ),
525        },
526        Header {
527            name: "From",
528            value: "Someone <someone@example.com>",
529            separator: ":  ",
530            conformance: MessageConformance(
531                NON_CANONICAL_LINE_ENDINGS,
532            ),
533        },
534    ],
535}
536"#
537        );
538    }
539
540    #[test]
541    fn as_mailbox() {
542        let sender = Header::with_name_value("Sender", "John Smith <jsmith@example.com>");
543        k9::snapshot!(
544            sender.as_mailbox(),
545            r#"
546Ok(
547    Mailbox {
548        name: Some(
549            "John Smith",
550        ),
551        address: AddrSpec {
552            local_part: "jsmith",
553            domain: "example.com",
554        },
555    },
556)
557"#
558        );
559    }
560
561    #[test]
562    fn assign_mailbox() {
563        let mut sender = Header::with_name_value("Sender", "");
564        sender.assign(Mailbox {
565            name: Some("John Smith".to_string()),
566            address: AddrSpec::new("john.smith", "example.com"),
567        });
568        assert_eq!(
569            sender.to_header_string(),
570            "Sender: John Smith <john.smith@example.com>\r\n"
571        );
572
573        sender.assign(Mailbox {
574            name: Some("John \"the smith\" Smith".to_string()),
575            address: AddrSpec::new("john.smith", "example.com"),
576        });
577        assert_eq!(
578            sender.to_header_string(),
579            "Sender: \"John \\\"the smith\\\" Smith\" <john.smith@example.com>\r\n"
580        );
581    }
582
583    #[test]
584    fn new_mailbox() {
585        let sender = Header::new(
586            "Sender",
587            Mailbox {
588                name: Some("John".to_string()),
589                address: AddrSpec::new("john.smith", "example.com"),
590            },
591        );
592        assert_eq!(
593            sender.to_header_string(),
594            "Sender: John <john.smith@example.com>\r\n"
595        );
596
597        let sender = Header::new(
598            "Sender",
599            Mailbox {
600                name: Some("John".to_string()),
601                address: AddrSpec::new("john smith", "example.com"),
602            },
603        );
604        assert_eq!(
605            sender.to_header_string(),
606            "Sender: John <\"john smith\"@example.com>\r\n"
607        );
608    }
609
610    #[test]
611    fn new_mailbox_2047() {
612        let sender = Header::new(
613            "Sender",
614            Mailbox {
615                name: Some("André Pirard".to_string()),
616                address: AddrSpec::new("andre", "example.com"),
617            },
618        );
619        assert_eq!(
620            sender.to_header_string(),
621            "Sender: =?UTF-8?q?Andr=C3=A9_Pirard?= <andre@example.com>\r\n"
622        );
623    }
624
625    #[test]
626    fn test_spacing_roundtrip() {
627        let (header, _size) = Header::parse(
628            "Subject: =?UTF-8?q?=D8=AA=D8=B3=D8=AA_=DB=8C=DA=A9_=D8=AF=D9=88_=D8=B3=D9=87?=",
629        )
630        .unwrap();
631        k9::snapshot!(
632            header.as_unstructured(),
633            r#"
634Ok(
635    "تست یک دو سه",
636)
637"#
638        );
639
640        let rebuilt = header.rebuild().unwrap();
641        k9::snapshot!(
642            rebuilt.as_unstructured(),
643            r#"
644Ok(
645    "تست یک دو سه",
646)
647"#
648        );
649    }
650
651    #[test]
652    fn test_unstructured_encode() {
653        let header = Header::new_unstructured("Subject", "hello there");
654        k9::snapshot!(header.value, "hello there");
655
656        let header = Header::new_unstructured("Subject", "hello \"there\"");
657        k9::snapshot!(header.value, "hello \"there\"");
658
659        let header = Header::new_unstructured("Subject", "hello André Pirard");
660        k9::snapshot!(header.value, "=?UTF-8?q?hello_Andr=C3=A9_Pirard?=");
661
662        let header = Header::new_unstructured(
663            "Subject",
664            "hello there, this is a \
665            longer header than the standard width and so it should \
666            get wrapped in the produced value",
667        );
668        k9::snapshot!(
669            header.to_header_string(),
670            r#"
671Subject: hello there, this is a longer header than the standard width and so it\r
672\tshould get wrapped in the produced value\r
673
674"#
675        );
676
677        let input_text = "hello there André, this is a longer header \
678                          than the standard width and so it should get \
679                          wrapped in the produced value. Do you hear me \
680                          André? this should get really long!";
681        let header = Header::new_unstructured("Subject", input_text);
682        k9::snapshot!(
683            header.to_header_string(),
684            r#"
685Subject: =?UTF-8?q?hello_there_Andr=C3=A9,_this_is_a_longer_header_than_the_sta?=\r
686\t=?UTF-8?q?ndard_width_and_so_it_should_get_wrapped_in_the_produced_val?=\r
687\t=?UTF-8?q?ue._Do_you_hear_me_Andr=C3=A9=3F_this_should_get_really_long?=\r
688\t=?UTF-8?q?!?=\r
689
690"#
691        );
692
693        k9::assert_equal!(header.as_unstructured().unwrap(), input_text);
694    }
695
696    #[test]
697    fn test_unstructured_encode_farsi() {
698        let farsi_input = "بوت‌كمپ قدرت نوشتن رهنماکالج";
699        let header = Header::new_unstructured("Subject", farsi_input);
700        eprintln!("{}", header.value);
701        k9::assert_equal!(header.as_unstructured().unwrap(), farsi_input);
702    }
703
704    #[test]
705    fn test_wrapping_in_from_header() {
706        let header = Header::new_unstructured(
707            "From",
708            "=?UTF-8?q?=D8=B1=D9=87=D9=86=D9=85=D8=A7_=DA=A9=D8=A7=D9=84=D8=AC?= \
709            <from-dash-wrap-me@example.com>",
710        );
711
712        eprintln!("made: {}", header.to_header_string());
713
714        // In the original problem report, the header got wrapped onto a second
715        // line at `from-` which broke parsing the header as a mailbox.
716        // This line asserts that it does parse.
717        let _ = header.as_mailbox_list().unwrap();
718    }
719
720    #[test]
721    fn test_multi_line_filename() {
722        let header = Header::with_name_value(
723            "Content-Disposition",
724            "attachment;\r\n\
725            \tfilename*0*=UTF-8''%D0%A7%D0%B0%D1%81%D1%82%D0%B8%D0%BD%D0%B0%20%D0%B2;\r\n\
726            \tfilename*1*=%D0%BA%D0%BB%D0%B0%D0%B4%D0%B5%D0%BD%D0%BE%D0%B3%D0%BE%20;\r\n\
727            \tfilename*2*=%D0%BF%D0%BE%D0%B2%D1%96%D0%B4%D0%BE%D0%BC%D0%BB%D0%B5%D0%BD;\r\n\
728            \tfilename*3*=%D0%BD%D1%8F",
729        );
730
731        match header.as_content_disposition() {
732            Ok(cd) => {
733                k9::snapshot!(
734                    cd.get("filename"),
735                    r#"
736Some(
737    "Частина вкладеного повідомлення",
738)
739"#
740                );
741            }
742            Err(err) => {
743                eprintln!("{err:#}");
744                panic!("expected to parse");
745            }
746        }
747    }
748
749    #[test]
750    fn test_date() {
751        let header = Header::with_name_value("Date", "Tue, 1 Jul 2003 10:52:37 +0200");
752        let date = header.as_date().unwrap();
753        k9::snapshot!(date, "2003-07-01T10:52:37+02:00");
754    }
755
756    #[test]
757    fn conformance_string() {
758        k9::assert_equal!(
759            MessageConformance::LINE_TOO_LONG.to_string(),
760            "LINE_TOO_LONG"
761        );
762        k9::assert_equal!(
763            (MessageConformance::LINE_TOO_LONG | MessageConformance::NEEDS_TRANSFER_ENCODING)
764                .to_string(),
765            "LINE_TOO_LONG|NEEDS_TRANSFER_ENCODING"
766        );
767        k9::assert_equal!(
768            MessageConformance::from_str("").unwrap(),
769            MessageConformance::default()
770        );
771
772        k9::assert_equal!(
773            MessageConformance::from_str("LINE_TOO_LONG").unwrap(),
774            MessageConformance::LINE_TOO_LONG
775        );
776        k9::assert_equal!(
777            MessageConformance::from_str("LINE_TOO_LONG|MISSING_COLON_VALUE").unwrap(),
778            MessageConformance::LINE_TOO_LONG | MessageConformance::MISSING_COLON_VALUE
779        );
780        k9::assert_equal!(
781            MessageConformance::from_str("LINE_TOO_LONG|spoon").unwrap_err(),
782            "invalid MessageConformance flag 'spoon', possible values are \
783            'INVALID_MIME_HEADERS', \
784            'LINE_TOO_LONG', 'MISSING_COLON_VALUE', 'MISSING_DATE_HEADER', \
785            'MISSING_MESSAGE_ID_HEADER', 'MISSING_MIME_VERSION', 'NAME_ENDS_WITH_SPACE', \
786            'NEEDS_TRANSFER_ENCODING', 'NON_CANONICAL_LINE_ENDINGS'"
787        );
788    }
789
790    #[test]
791    fn split_qp_display_name() {
792        let header = Header::with_name_value("From", "=?UTF-8?q?=D8=A7=D9=86=D8=AA=D8=B4=D8=A7=D8=B1=D8=A7=D8=AA_=D8=AC=D9=85?=\r\n\t=?UTF-8?q?=D8=A7=D9=84?= <noreply@email.ahasend.com>");
793
794        let mbox = header.as_mailbox_list().unwrap();
795
796        k9::snapshot!(
797            mbox,
798            r#"
799MailboxList(
800    [
801        Mailbox {
802            name: Some(
803                "انتشارات جم ال",
804            ),
805            address: AddrSpec {
806                local_part: "noreply",
807                domain: "email.ahasend.com",
808            },
809        },
810    ],
811)
812"#
813        );
814    }
815}