mailparsing/
header.rs

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