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