mailparsing/
header.rs

1use crate::headermap::{EncodeHeaderValue, HeaderMap};
2use crate::rfc5322_parser::Parser;
3use crate::strings::IntoSharedString;
4use crate::{
5    ARCAuthenticationResults, AddressList, AuthenticationResults, MailParsingError, Mailbox,
6    MailboxList, MessageID, 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            kumo_wrap::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_arc_authentication_results(&self) -> Result<ARCAuthenticationResults> {
227        Parser::parse_arc_authentication_results_header(self.get_raw_value())
228    }
229
230    pub fn as_date(&self) -> Result<DateTime<FixedOffset>> {
231        DateTime::parse_from_rfc2822(self.get_raw_value()).map_err(MailParsingError::ChronoError)
232    }
233
234    pub fn parse_headers<S>(header_block: S) -> Result<HeaderParseResult<'a>>
235    where
236        S: IntoSharedString<'a>,
237    {
238        let (header_block, mut overall_conformance) = header_block.into_shared_string();
239        let mut headers = vec![];
240        let mut idx = 0;
241
242        while idx < header_block.len() {
243            let b = header_block[idx];
244            if b == b'\n' {
245                // LF: End of header block
246                idx += 1;
247                overall_conformance.set(MessageConformance::NON_CANONICAL_LINE_ENDINGS, true);
248                break;
249            }
250            if b == b'\r' {
251                if idx + 1 < header_block.len() && header_block[idx + 1] == b'\n' {
252                    // CRLF: End of header block
253                    idx += 2;
254                    break;
255                }
256                return Err(MailParsingError::HeaderParse(
257                    "lone CR in header".to_string(),
258                ));
259            }
260            if headers.is_empty() && b.is_ascii_whitespace() {
261                return Err(MailParsingError::HeaderParse(
262                    "header block must not start with spaces".to_string(),
263                ));
264            }
265            let (header, next) = Self::parse(header_block.slice(idx..header_block.len()))?;
266            overall_conformance |= header.conformance;
267            headers.push(header);
268            debug_assert!(
269                idx != next + idx,
270                "idx={idx}, next={next}, headers: {headers:#?}"
271            );
272            idx += next;
273        }
274        Ok(HeaderParseResult {
275            headers: HeaderMap::new(headers),
276            body_offset: idx,
277            overall_conformance,
278        })
279    }
280
281    pub fn parse<S: Into<SharedString<'a>>>(header_block: S) -> Result<(Self, usize)> {
282        let header_block = header_block.into();
283
284        enum State {
285            Initial,
286            Name,
287            Separator,
288            Value,
289            NewLine,
290        }
291
292        let mut state = State::Initial;
293
294        let mut iter = header_block.as_bytes().iter();
295        let mut c = *iter
296            .next()
297            .ok_or_else(|| MailParsingError::HeaderParse("empty header string".to_string()))?;
298
299        let mut name_end = None;
300        let mut value_start = 0;
301        let mut value_end = 0;
302
303        let mut idx = 0usize;
304        let mut conformance = MessageConformance::default();
305        let mut saw_cr = false;
306        let mut line_start = 0;
307        let mut max_line_len = 0;
308
309        loop {
310            match state {
311                State::Initial => {
312                    if c.is_ascii_whitespace() {
313                        return Err(MailParsingError::HeaderParse(
314                            "header cannot start with space".to_string(),
315                        ));
316                    }
317                    state = State::Name;
318                    continue;
319                }
320                State::Name => {
321                    if c == b':' {
322                        if name_end.is_none() {
323                            name_end.replace(idx);
324                        }
325                        state = State::Separator;
326                    } else if c == b' ' || c == b'\t' {
327                        if name_end.is_none() {
328                            name_end.replace(idx);
329                        }
330                        conformance.set(MessageConformance::NAME_ENDS_WITH_SPACE, true);
331                    } else if c == b'\n' {
332                        // Got a newline before we finished parsing the name
333                        conformance.set(MessageConformance::MISSING_COLON_VALUE, true);
334                        name_end.replace(idx);
335                        max_line_len = max_line_len.max(idx.saturating_sub(line_start));
336                        value_start = idx;
337                        value_end = idx;
338                        idx += 1;
339                        break;
340                    } else if c != b'\r' && !(33..=126).contains(&c) {
341                        return Err(MailParsingError::HeaderParse(format!(
342                            "header name must be comprised of printable US-ASCII characters. Found {c:?}"
343                        )));
344                    }
345                }
346                State::Separator => {
347                    if c != b' ' {
348                        value_start = idx;
349                        value_end = idx;
350                        state = State::Value;
351                        continue;
352                    }
353                }
354                State::Value => {
355                    if c == b'\n' {
356                        if !saw_cr {
357                            conformance.set(MessageConformance::NON_CANONICAL_LINE_ENDINGS, true);
358                        }
359                        state = State::NewLine;
360                        saw_cr = false;
361                        max_line_len = max_line_len.max(idx.saturating_sub(line_start));
362                        line_start = idx + 1;
363                    } else if c != b'\r' {
364                        value_end = idx + 1;
365                        saw_cr = false;
366                    } else {
367                        saw_cr = true;
368                    }
369                }
370                State::NewLine => {
371                    if c == b' ' || c == b'\t' {
372                        state = State::Value;
373                        continue;
374                    }
375                    break;
376                }
377            }
378            idx += 1;
379            c = match iter.next() {
380                None => break,
381                Some(v) => *v,
382            };
383        }
384
385        max_line_len = max_line_len.max(idx.saturating_sub(line_start));
386        if max_line_len > 78 {
387            conformance.set(MessageConformance::LINE_TOO_LONG, true);
388        }
389
390        let name_end = name_end.unwrap_or_else(|| {
391            conformance.set(MessageConformance::MISSING_COLON_VALUE, true);
392            idx
393        });
394
395        let name = header_block.slice(0..name_end);
396        let value = header_block.slice(value_start..value_end.max(value_start));
397        let separator = header_block.slice(name_end..value_start.max(name_end));
398
399        let header = Self {
400            name,
401            value,
402            separator,
403            conformance,
404        };
405
406        Ok((header, idx))
407    }
408
409    /// Re-constitute the header.
410    /// The header value will be parsed out according to the known schema
411    /// of the associated header name, and the parsed form used
412    /// to build a new version of the header.
413    /// This has the side effect of "fixing" non-conforming elements,
414    /// but may come at the cost of "losing" the non-sensical or otherwise
415    /// out of spec elements in the rebuilt header
416    pub fn rebuild(&self) -> Result<Self> {
417        let name = self.get_name();
418
419        macro_rules! hdr {
420            ($header_name:literal, $func_name:ident, encode) => {
421                if name.eq_ignore_ascii_case($header_name) {
422                    let value = self.$func_name().map_err(|err| {
423                        MailParsingError::HeaderParse(format!(
424                            "rebuilding '{name}' header: {err:#}"
425                        ))
426                    })?;
427                    return Ok(Self::with_name_value($header_name, value.encode_value()));
428                }
429            };
430            ($header_name:literal, unstructured) => {
431                if name.eq_ignore_ascii_case($header_name) {
432                    let value = self.as_unstructured().map_err(|err| {
433                        MailParsingError::HeaderParse(format!(
434                            "rebuilding '{name}' header: {err:#}"
435                        ))
436                    })?;
437                    return Ok(Self::new_unstructured($header_name, value));
438                }
439            };
440        }
441
442        hdr!("From", as_mailbox_list, encode);
443        hdr!("Resent-From", as_mailbox_list, encode);
444        hdr!("Reply-To", as_address_list, encode);
445        hdr!("To", as_address_list, encode);
446        hdr!("Cc", as_address_list, encode);
447        hdr!("Bcc", as_address_list, encode);
448        hdr!("Resent-To", as_address_list, encode);
449        hdr!("Resent-Cc", as_address_list, encode);
450        hdr!("Resent-Bcc", as_address_list, encode);
451        hdr!("Date", as_date, encode);
452        hdr!("Sender", as_mailbox, encode);
453        hdr!("Resent-Sender", as_mailbox, encode);
454        hdr!("Message-ID", as_message_id, encode);
455        hdr!("Content-ID", as_content_id, encode);
456        hdr!("Content-Type", as_content_type, encode);
457        hdr!(
458            "Content-Transfer-Encoding",
459            as_content_transfer_encoding,
460            encode
461        );
462        hdr!("Content-Disposition", as_content_disposition, encode);
463        hdr!("References", as_message_id_list, encode);
464        hdr!("Subject", unstructured);
465        hdr!("Comments", unstructured);
466        hdr!("Mime-Version", unstructured);
467
468        // Assume unstructured
469        let value = self.as_unstructured().map_err(|err| {
470            MailParsingError::HeaderParse(format!("rebuilding '{name}' header: {err:#}"))
471        })?;
472        Ok(Self::new_unstructured(name.to_string(), value))
473    }
474}
475
476#[cfg(test)]
477mod test {
478    use super::*;
479    use crate::AddrSpec;
480
481    fn assert_static_lifetime(_header: Header<'static>) {
482        assert!(true, "I wouldn't compile if this wasn't true");
483    }
484
485    #[test]
486    fn header_construction() {
487        let header = Header::with_name_value("To", "someone@example.com");
488        assert_eq!(header.get_name(), "To");
489        assert_eq!(header.get_raw_value(), "someone@example.com");
490        assert_eq!(header.to_header_string(), "To: someone@example.com\r\n");
491        assert_static_lifetime(header);
492    }
493
494    #[test]
495    fn header_parsing() {
496        let message = concat!(
497            "Subject: hello there\n",
498            "From:  Someone <someone@example.com>\n",
499            "\n",
500            "I am the body"
501        );
502
503        let HeaderParseResult {
504            headers,
505            body_offset,
506            overall_conformance,
507        } = Header::parse_headers(message).unwrap();
508        assert_eq!(&message[body_offset..], "I am the body");
509        k9::snapshot!(
510            overall_conformance,
511            "
512MessageConformance(
513    NON_CANONICAL_LINE_ENDINGS,
514)
515"
516        );
517        k9::snapshot!(
518            headers,
519            r#"
520HeaderMap {
521    headers: [
522        Header {
523            name: "Subject",
524            value: "hello there",
525            separator: ": ",
526            conformance: MessageConformance(
527                NON_CANONICAL_LINE_ENDINGS,
528            ),
529        },
530        Header {
531            name: "From",
532            value: "Someone <someone@example.com>",
533            separator: ":  ",
534            conformance: MessageConformance(
535                NON_CANONICAL_LINE_ENDINGS,
536            ),
537        },
538    ],
539}
540"#
541        );
542    }
543
544    #[test]
545    fn as_mailbox() {
546        let sender = Header::with_name_value("Sender", "John Smith <jsmith@example.com>");
547        k9::snapshot!(
548            sender.as_mailbox(),
549            r#"
550Ok(
551    Mailbox {
552        name: Some(
553            "John Smith",
554        ),
555        address: AddrSpec {
556            local_part: "jsmith",
557            domain: "example.com",
558        },
559    },
560)
561"#
562        );
563    }
564
565    #[test]
566    fn assign_mailbox() {
567        let mut sender = Header::with_name_value("Sender", "");
568        sender.assign(Mailbox {
569            name: Some("John Smith".to_string()),
570            address: AddrSpec::new("john.smith", "example.com"),
571        });
572        assert_eq!(
573            sender.to_header_string(),
574            "Sender: \"John Smith\" <john.smith@example.com>\r\n"
575        );
576
577        sender.assign(Mailbox {
578            name: Some("John \"the smith\" Smith".to_string()),
579            address: AddrSpec::new("john.smith", "example.com"),
580        });
581        assert_eq!(
582            sender.to_header_string(),
583            "Sender: \"John \\\"the smith\\\" Smith\" <john.smith@example.com>\r\n"
584        );
585    }
586
587    #[test]
588    fn new_mailbox() {
589        let sender = Header::new(
590            "Sender",
591            Mailbox {
592                name: Some("John".to_string()),
593                address: AddrSpec::new("john.smith", "example.com"),
594            },
595        );
596        assert_eq!(
597            sender.to_header_string(),
598            "Sender: John <john.smith@example.com>\r\n"
599        );
600
601        let sender = Header::new(
602            "Sender",
603            Mailbox {
604                name: Some("John".to_string()),
605                address: AddrSpec::new("john smith", "example.com"),
606            },
607        );
608        assert_eq!(
609            sender.to_header_string(),
610            "Sender: John <\"john smith\"@example.com>\r\n"
611        );
612    }
613
614    #[test]
615    fn new_mailbox_2047() {
616        let sender = Header::new(
617            "Sender",
618            Mailbox {
619                name: Some("André Pirard".to_string()),
620                address: AddrSpec::new("andre", "example.com"),
621            },
622        );
623        assert_eq!(
624            sender.to_header_string(),
625            "Sender: =?UTF-8?q?Andr=C3=A9_Pirard?= <andre@example.com>\r\n"
626        );
627    }
628
629    #[test]
630    fn test_spacing_roundtrip() {
631        let (header, _size) = Header::parse(
632            "Subject: =?UTF-8?q?=D8=AA=D8=B3=D8=AA_=DB=8C=DA=A9_=D8=AF=D9=88_=D8=B3=D9=87?=",
633        )
634        .unwrap();
635        k9::snapshot!(
636            header.as_unstructured(),
637            r#"
638Ok(
639    "تست یک دو سه",
640)
641"#
642        );
643
644        let rebuilt = header.rebuild().unwrap();
645        k9::snapshot!(
646            rebuilt.as_unstructured(),
647            r#"
648Ok(
649    "تست یک دو سه",
650)
651"#
652        );
653    }
654
655    #[test]
656    fn test_unstructured_encode() {
657        let header = Header::new_unstructured("Subject", "hello there");
658        k9::snapshot!(header.value, "hello there");
659
660        let header = Header::new_unstructured("Subject", "hello \"there\"");
661        k9::snapshot!(header.value, "hello \"there\"");
662
663        let header = Header::new_unstructured("Subject", "hello André Pirard");
664        k9::snapshot!(header.value, "=?UTF-8?q?hello_Andr=C3=A9_Pirard?=");
665
666        let header = Header::new_unstructured(
667            "Subject",
668            "hello there, this is a \
669            longer header than the standard width and so it should \
670            get wrapped in the produced value",
671        );
672        k9::snapshot!(
673            header.to_header_string(),
674            r#"
675Subject: hello there, this is a longer header than the standard width and so it\r
676\tshould get wrapped in the produced value\r
677
678"#
679        );
680
681        let input_text = "hello there André, this is a longer header \
682                          than the standard width and so it should get \
683                          wrapped in the produced value. Do you hear me \
684                          André? this should get really long!";
685        let header = Header::new_unstructured("Subject", input_text);
686        k9::snapshot!(
687            header.to_header_string(),
688            r#"
689Subject: =?UTF-8?q?hello_there_Andr=C3=A9,_this_is_a_longer_header_than_the_sta?=\r
690\t=?UTF-8?q?ndard_width_and_so_it_should_get_wrapped_in_the_produced_val?=\r
691\t=?UTF-8?q?ue._Do_you_hear_me_Andr=C3=A9=3F_this_should_get_really_long?=\r
692\t=?UTF-8?q?!?=\r
693
694"#
695        );
696
697        k9::assert_equal!(header.as_unstructured().unwrap(), input_text);
698    }
699
700    #[test]
701    fn test_unstructured_encode_farsi() {
702        let farsi_input = "بوت‌كمپ قدرت نوشتن رهنماکالج";
703        let header = Header::new_unstructured("Subject", farsi_input);
704        eprintln!("{}", header.value);
705        k9::assert_equal!(header.as_unstructured().unwrap(), farsi_input);
706    }
707
708    #[test]
709    fn test_wrapping_in_from_header() {
710        let header = Header::new_unstructured(
711            "From",
712            "=?UTF-8?q?=D8=B1=D9=87=D9=86=D9=85=D8=A7_=DA=A9=D8=A7=D9=84=D8=AC?= \
713            <from-dash-wrap-me@example.com>",
714        );
715
716        eprintln!("made: {}", header.to_header_string());
717
718        // In the original problem report, the header got wrapped onto a second
719        // line at `from-` which broke parsing the header as a mailbox.
720        // This line asserts that it does parse.
721        let _ = header.as_mailbox_list().unwrap();
722    }
723
724    #[test]
725    fn test_multi_line_filename() {
726        let header = Header::with_name_value(
727            "Content-Disposition",
728            "attachment;\r\n\
729            \tfilename*0*=UTF-8''%D0%A7%D0%B0%D1%81%D1%82%D0%B8%D0%BD%D0%B0%20%D0%B2;\r\n\
730            \tfilename*1*=%D0%BA%D0%BB%D0%B0%D0%B4%D0%B5%D0%BD%D0%BE%D0%B3%D0%BE%20;\r\n\
731            \tfilename*2*=%D0%BF%D0%BE%D0%B2%D1%96%D0%B4%D0%BE%D0%BC%D0%BB%D0%B5%D0%BD;\r\n\
732            \tfilename*3*=%D0%BD%D1%8F",
733        );
734
735        match header.as_content_disposition() {
736            Ok(cd) => {
737                k9::snapshot!(
738                    cd.get("filename"),
739                    r#"
740Some(
741    "Частина вкладеного повідомлення",
742)
743"#
744                );
745            }
746            Err(err) => {
747                eprintln!("{err:#}");
748                panic!("expected to parse");
749            }
750        }
751    }
752
753    #[test]
754    fn test_date() {
755        let header = Header::with_name_value("Date", "Tue, 1 Jul 2003 10:52:37 +0200");
756        let date = header.as_date().unwrap();
757        k9::snapshot!(date, "2003-07-01T10:52:37+02:00");
758    }
759
760    #[test]
761    fn conformance_string() {
762        k9::assert_equal!(
763            MessageConformance::LINE_TOO_LONG.to_string(),
764            "LINE_TOO_LONG"
765        );
766        k9::assert_equal!(
767            (MessageConformance::LINE_TOO_LONG | MessageConformance::NEEDS_TRANSFER_ENCODING)
768                .to_string(),
769            "LINE_TOO_LONG|NEEDS_TRANSFER_ENCODING"
770        );
771        k9::assert_equal!(
772            MessageConformance::from_str("").unwrap(),
773            MessageConformance::default()
774        );
775
776        k9::assert_equal!(
777            MessageConformance::from_str("LINE_TOO_LONG").unwrap(),
778            MessageConformance::LINE_TOO_LONG
779        );
780        k9::assert_equal!(
781            MessageConformance::from_str("LINE_TOO_LONG|MISSING_COLON_VALUE").unwrap(),
782            MessageConformance::LINE_TOO_LONG | MessageConformance::MISSING_COLON_VALUE
783        );
784        k9::assert_equal!(
785            MessageConformance::from_str("LINE_TOO_LONG|spoon").unwrap_err(),
786            "invalid MessageConformance flag 'spoon', possible values are \
787            'INVALID_MIME_HEADERS', \
788            'LINE_TOO_LONG', 'MISSING_COLON_VALUE', 'MISSING_DATE_HEADER', \
789            'MISSING_MESSAGE_ID_HEADER', 'MISSING_MIME_VERSION', 'NAME_ENDS_WITH_SPACE', \
790            'NEEDS_TRANSFER_ENCODING', 'NON_CANONICAL_LINE_ENDINGS'"
791        );
792    }
793
794    #[test]
795    fn split_qp_display_name() {
796        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>");
797
798        let mbox = header.as_mailbox_list().unwrap();
799
800        k9::snapshot!(
801            mbox,
802            r#"
803MailboxList(
804    [
805        Mailbox {
806            name: Some(
807                "انتشارات جم ال",
808            ),
809            address: AddrSpec {
810                local_part: "noreply",
811                domain: "email.ahasend.com",
812            },
813        },
814    ],
815)
816"#
817        );
818    }
819}