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