mailparsing/
rfc5322_parser.rs

1use crate::headermap::EncodeHeaderValue;
2use crate::nom_utils::{explain_nom, make_context_error, make_span, IResult, ParseError, Span};
3use crate::{MailParsingError, Result, SharedString};
4use bstr::{BStr, BString, ByteSlice, ByteVec};
5use charset_normalizer_rs::Encoding;
6use nom::branch::alt;
7use nom::bytes::complete::{take_while, take_while1, take_while_m_n};
8use nom::combinator::{all_consuming, map, opt, recognize};
9use nom::error::context;
10use nom::multi::{many0, many1, separated_list1};
11use nom::sequence::{delimited, preceded, separated_pair, terminated};
12use nom::{Compare, Input, Parser as _};
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use std::fmt::Debug;
16use std::marker::PhantomData;
17
18impl MailParsingError {
19    pub fn from_nom(input: Span, err: nom::Err<ParseError<Span<'_>>>) -> Self {
20        MailParsingError::HeaderParse(explain_nom(input, err))
21    }
22}
23
24/// Like nom::bytes::complete::tag, except that we print what the tag
25/// was expecting if there was an error.
26/// I feel like this should be the default behavior TBH.
27fn tag<E>(tag: &'static str) -> TagParser<E> {
28    TagParser {
29        tag,
30        e: PhantomData,
31    }
32}
33
34/// Struct to support displaying better errors for tag()
35struct TagParser<E> {
36    tag: &'static str,
37    e: PhantomData<E>,
38}
39
40/// All this fuss to show what we expected for the TagParser impl
41impl<I, Error: nom::error::ParseError<I> + nom::error::FromExternalError<I, String>> nom::Parser<I>
42    for TagParser<Error>
43where
44    I: Input + Compare<&'static str> + nom::AsBytes,
45{
46    type Output = I;
47    type Error = Error;
48
49    fn process<OM: nom::OutputMode>(
50        &mut self,
51        i: I,
52    ) -> nom::PResult<OM, I, Self::Output, Self::Error> {
53        use nom::error::ErrorKind;
54        use nom::{CompareResult, Err, Mode};
55
56        let tag_len = self.tag.input_len();
57
58        match i.compare(self.tag) {
59            CompareResult::Ok => Ok((i.take_from(tag_len), OM::Output::bind(|| i.take(tag_len)))),
60            CompareResult::Incomplete => Err(Err::Error(OM::Error::bind(|| {
61                Error::from_external_error(
62                    i,
63                    ErrorKind::Fail,
64                    format!(
65                        "expected \"{}\" but ran out of input",
66                        self.tag.escape_debug()
67                    ),
68                )
69            }))),
70
71            CompareResult::Error => {
72                let available = i.take(i.input_len().min(tag_len));
73                Err(Err::Error(OM::Error::bind(|| {
74                    Error::from_external_error(
75                        i,
76                        ErrorKind::Fail,
77                        format!(
78                            "expected \"{}\" but found {:?}",
79                            self.tag.escape_debug(),
80                            BStr::new(available.as_bytes())
81                        ),
82                    )
83                })))
84            }
85        }
86    }
87}
88
89fn is_utf8_non_ascii(c: u8) -> bool {
90    c == 0 || c >= 0x80
91}
92
93// ctl = { '\u{00}'..'\u{1f}' | "\u{7f}" }
94fn is_ctl(c: u8) -> bool {
95    match c {
96        b'\x00'..=b'\x1f' | b'\x7f' => true,
97        _ => false,
98    }
99}
100
101fn not_angle(c: u8) -> bool {
102    match c {
103        b'<' | b'>' => false,
104        _ => true,
105    }
106}
107
108// char = { '\u{01}'..'\u{7f}' }
109fn is_char(c: u8) -> bool {
110    match c {
111        0x01..=0xff => true,
112        _ => false,
113    }
114}
115
116fn is_especial(c: u8) -> bool {
117    match c {
118        b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'/' | b'[' | b']' | b'?'
119        | b'.' | b'=' => true,
120        _ => false,
121    }
122}
123
124fn is_token(c: u8) -> bool {
125    is_char(c) && c != b' ' && !is_especial(c) && !is_ctl(c)
126}
127
128// vchar = { '\u{21}'..'\u{7e}' | utf8_non_ascii }
129fn is_vchar(c: u8) -> bool {
130    (0x21..=0x7e).contains(&c) || is_utf8_non_ascii(c)
131}
132
133fn is_atext(c: u8) -> bool {
134    match c {
135        b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'/' | b'=' | b'?'
136        | b'^' | b'_' | b'`' | b'{' | b'|' | b'}' | b'~' => true,
137        c => c.is_ascii_alphanumeric() || is_utf8_non_ascii(c),
138    }
139}
140
141fn atext(input: Span) -> IResult<Span, Span> {
142    context("atext", take_while1(is_atext)).parse(input)
143}
144
145fn is_obs_no_ws_ctl(c: u8) -> bool {
146    match c {
147        0x01..=0x08 | 0x0b..=0x0c | 0x0e..=0x1f | 0x7f => true,
148        _ => false,
149    }
150}
151
152fn is_obs_ctext(c: u8) -> bool {
153    is_obs_no_ws_ctl(c)
154}
155
156// ctext = { '\u{21}'..'\u{27}' | '\u{2a}'..'\u{5b}' | '\u{5d}'..'\u{7e}' | obs_ctext | utf8_non_ascii }
157fn is_ctext(c: u8) -> bool {
158    match c {
159        0x21..=0x27 | 0x2a..=0x5b | 0x5d..=0x7e => true,
160        c => is_obs_ctext(c) || is_utf8_non_ascii(c),
161    }
162}
163
164// dtext = { '\u{21}'..'\u{5a}' | '\u{5e}'..'\u{7e}' | obs_dtext | utf8_non_ascii }
165// obs_dtext = { obs_no_ws_ctl | quoted_pair }
166fn is_dtext(c: u8) -> bool {
167    match c {
168        0x21..=0x5a | 0x5e..=0x7e => true,
169        c => is_obs_no_ws_ctl(c) || is_utf8_non_ascii(c),
170    }
171}
172
173// qtext = { "\u{21}" | '\u{23}'..'\u{5b}' | '\u{5d}'..'\u{7e}' | obs_qtext | utf8_non_ascii }
174// obs_qtext = { obs_no_ws_ctl }
175fn is_qtext(c: u8) -> bool {
176    match c {
177        0x21 | 0x23..=0x5b | 0x5d..=0x7e => true,
178        c => is_obs_no_ws_ctl(c) || is_utf8_non_ascii(c),
179    }
180}
181
182fn is_tspecial(c: u8) -> bool {
183    match c {
184        b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'\\' | b'"' | b'/' | b'['
185        | b']' | b'?' | b'=' => true,
186        _ => false,
187    }
188}
189
190fn is_attribute_char(c: u8) -> bool {
191    match c {
192        b' ' | b'*' | b'\'' | b'%' => false,
193        _ => is_char(c) && !is_ctl(c) && !is_tspecial(c),
194    }
195}
196
197fn wsp(input: Span) -> IResult<Span, Span> {
198    context("wsp", take_while1(|c| c == b' ' || c == b'\t')).parse(input)
199}
200
201fn newline(input: Span) -> IResult<Span, Span> {
202    context("newline", recognize(preceded(opt(tag("\r")), tag("\n")))).parse(input)
203}
204
205// fws = { ((wsp* ~ "\r"? ~ "\n")* ~ wsp+) | obs_fws }
206fn fws(input: Span) -> IResult<Span, Span> {
207    context(
208        "fws",
209        alt((
210            recognize(preceded(many0(preceded(many0(wsp), newline)), many1(wsp))),
211            obs_fws,
212        )),
213    )
214    .parse(input)
215}
216
217// obs_fws = { wsp+ ~ ("\r"? ~ "\n" ~ wsp+)* }
218fn obs_fws(input: Span) -> IResult<Span, Span> {
219    context(
220        "obs_fws",
221        recognize(preceded(many1(wsp), preceded(newline, many1(wsp)))),
222    )
223    .parse(input)
224}
225
226// mailbox_list = { (mailbox ~ ("," ~ mailbox)*) | obs_mbox_list }
227fn mailbox_list(input: Span) -> IResult<Span, MailboxList> {
228    let (loc, mailboxes) = context(
229        "mailbox_list",
230        alt((separated_list1(tag(","), mailbox), obs_mbox_list)),
231    )
232    .parse(input)?;
233    Ok((loc, MailboxList(mailboxes)))
234}
235
236// obs_mbox_list = {  ((cfws? ~ ",")* ~ mailbox ~ ("," ~ (mailbox | cfws))*)+ }
237fn obs_mbox_list(input: Span) -> IResult<Span, Vec<Mailbox>> {
238    let (loc, entries) = context(
239        "obs_mbox_list",
240        many1(preceded(
241            many0(preceded(opt(cfws), tag(","))),
242            (
243                mailbox,
244                many0(preceded(
245                    tag(","),
246                    alt((map(mailbox, Some), map(cfws, |_| None))),
247                )),
248            ),
249        )),
250    )
251    .parse(input)?;
252
253    let mut result: Vec<Mailbox> = vec![];
254
255    for (first, boxes) in entries {
256        result.push(first);
257        for b in boxes {
258            if let Some(m) = b {
259                result.push(m);
260            }
261        }
262    }
263
264    Ok((loc, result))
265}
266
267// mailbox = { name_addr | addr_spec }
268fn mailbox(input: Span) -> IResult<Span, Mailbox> {
269    if let Ok(res) = name_addr(input) {
270        Ok(res)
271    } else {
272        let (loc, address) = context("mailbox", addr_spec).parse(input)?;
273        Ok((
274            loc,
275            Mailbox {
276                name: None,
277                address,
278            },
279        ))
280    }
281}
282
283// address_list = { (address ~ ("," ~ address)*) | obs_addr_list }
284fn address_list(input: Span) -> IResult<Span, AddressList> {
285    context(
286        "address_list",
287        alt((
288            map(separated_list1(tag(","), address), AddressList),
289            obs_address_list,
290        )),
291    )
292    .parse(input)
293}
294
295// obs_addr_list = {  ((cfws? ~ ",")* ~ address ~ ("," ~ (address | cfws))*)+ }
296fn obs_address_list(input: Span) -> IResult<Span, AddressList> {
297    let (loc, entries) = context(
298        "obs_address_list",
299        many1(preceded(
300            many0(preceded(opt(cfws), tag(","))),
301            (
302                address,
303                many0(preceded(
304                    tag(","),
305                    alt((map(address, Some), map(cfws, |_| None))),
306                )),
307            ),
308        )),
309    )
310    .parse(input)?;
311
312    let mut result: Vec<Address> = vec![];
313
314    for (first, boxes) in entries {
315        result.push(first);
316        for b in boxes {
317            if let Some(m) = b {
318                result.push(m);
319            }
320        }
321    }
322
323    Ok((loc, AddressList(result)))
324}
325
326// address = { mailbox | group }
327fn address(input: Span) -> IResult<Span, Address> {
328    context("address", alt((map(mailbox, Address::Mailbox), group))).parse(input)
329}
330
331// group = { display_name ~ ":" ~ group_list? ~ ";" ~ cfws? }
332fn group(input: Span) -> IResult<Span, Address> {
333    let (loc, (name, _, group_list, _)) = context(
334        "group",
335        terminated(
336            (display_name, tag(":"), opt(group_list), tag(";")),
337            opt(cfws),
338        ),
339    )
340    .parse(input)?;
341    Ok((
342        loc,
343        Address::Group {
344            name: name.into(),
345            entries: group_list.unwrap_or_else(|| MailboxList(vec![])),
346        },
347    ))
348}
349
350// group_list = { mailbox_list | cfws | obs_group_list }
351fn group_list(input: Span) -> IResult<Span, MailboxList> {
352    context(
353        "group_list",
354        alt((
355            mailbox_list,
356            map(cfws, |_| MailboxList(vec![])),
357            obs_group_list,
358        )),
359    )
360    .parse(input)
361}
362
363// obs_group_list = @{ (cfws? ~ ",")+ ~ cfws? }
364fn obs_group_list(input: Span) -> IResult<Span, MailboxList> {
365    context(
366        "obs_group_list",
367        map(
368            terminated(many1(preceded(opt(cfws), tag(","))), opt(cfws)),
369            |_| MailboxList(vec![]),
370        ),
371    )
372    .parse(input)
373}
374
375// name_addr = { display_name? ~ angle_addr }
376fn name_addr(input: Span) -> IResult<Span, Mailbox> {
377    context(
378        "name_addr",
379        map((opt(display_name), angle_addr), |(name, address)| Mailbox {
380            name: name.map(Into::into),
381            address,
382        }),
383    )
384    .parse(input)
385}
386
387// display_name = { phrase }
388fn display_name(input: Span) -> IResult<Span, BString> {
389    context("display_name", phrase).parse(input)
390}
391
392// phrase = { (encoded_word | word)+ | obs_phrase }
393// obs_phrase = { (encoded_word | word) ~ (encoded_word | word | dot | cfws)* }
394fn phrase(input: Span) -> IResult<Span, BString> {
395    let (loc, (a, b)): (Span, (BString, Vec<Option<BString>>)) = context(
396        "phrase",
397        (
398            alt((encoded_word, word)),
399            many0(alt((
400                map(cfws, |_| None),
401                map(encoded_word, Option::Some),
402                map(word, Option::Some),
403                map(tag("."), |_dot| Some(BString::from("."))),
404            ))),
405        ),
406    )
407    .parse(input)?;
408    let mut result = a;
409    for item in b {
410        if let Some(item) = item {
411            result.push(b' ');
412            result.push_str(item);
413        }
414    }
415    Ok((loc, result))
416}
417
418// angle_addr = { cfws? ~ "<" ~ addr_spec ~ ">" ~ cfws? | obs_angle_addr }
419fn angle_addr(input: Span) -> IResult<Span, AddrSpec> {
420    context(
421        "angle_addr",
422        alt((
423            delimited(
424                opt(cfws),
425                delimited(tag("<"), addr_spec, tag(">")),
426                opt(cfws),
427            ),
428            obs_angle_addr,
429        )),
430    )
431    .parse(input)
432}
433
434// obs_angle_addr = { cfws? ~ "<" ~ obs_route ~ addr_spec ~ ">" ~ cfws? }
435fn obs_angle_addr(input: Span) -> IResult<Span, AddrSpec> {
436    context(
437        "obs_angle_addr",
438        delimited(
439            opt(cfws),
440            delimited(tag("<"), preceded(obs_route, addr_spec), tag(">")),
441            opt(cfws),
442        ),
443    )
444    .parse(input)
445}
446
447// obs_route = { obs_domain_list ~ ":" }
448// obs_domain_list = { (cfws | ",")* ~ "@" ~ domain ~ ("," ~ cfws? ~ ("@" ~ domain)?)* }
449fn obs_route(input: Span) -> IResult<Span, Span> {
450    context(
451        "obs_route",
452        recognize(terminated(
453            (
454                many0(alt((cfws, recognize(tag(","))))),
455                recognize(tag("@")),
456                recognize(domain),
457                many0((tag(","), opt(cfws), opt((tag("@"), domain)))),
458            ),
459            tag(":"),
460        )),
461    )
462    .parse(input)
463}
464
465// addr_spec = { local_part ~ "@" ~ domain }
466fn addr_spec(input: Span) -> IResult<Span, AddrSpec> {
467    let (loc, (local_part, domain)) =
468        context("addr_spec", separated_pair(local_part, tag("@"), domain)).parse(input)?;
469    Ok((
470        loc,
471        AddrSpec {
472            local_part: local_part.into(),
473            domain: domain.into(),
474        },
475    ))
476}
477
478fn parse_with<'a, R, F>(text: &'a [u8], parser: F) -> Result<R>
479where
480    F: Fn(Span<'a>) -> IResult<'a, Span<'a>, R>,
481{
482    let input = make_span(text);
483    let (_, result) = all_consuming(parser)
484        .parse(input)
485        .map_err(|err| MailParsingError::from_nom(input, err))?;
486    Ok(result)
487}
488
489#[cfg(test)]
490#[test]
491fn test_addr_spec() {
492    k9::snapshot!(
493        parse_with("darth.vader@a.galaxy.far.far.away".as_bytes(), addr_spec),
494        r#"
495Ok(
496    AddrSpec {
497        local_part: "darth.vader",
498        domain: "a.galaxy.far.far.away",
499    },
500)
501"#
502    );
503
504    k9::snapshot!(
505        parse_with(
506            "\"darth.vader\"@a.galaxy.far.far.away".as_bytes(),
507            addr_spec
508        ),
509        r#"
510Ok(
511    AddrSpec {
512        local_part: "darth.vader",
513        domain: "a.galaxy.far.far.away",
514    },
515)
516"#
517    );
518
519    k9::snapshot!(
520        parse_with(
521            "\"darth\".vader@a.galaxy.far.far.away".as_bytes(),
522            addr_spec
523        ),
524        r#"
525Err(
526    HeaderParse(
527        "Error at line 1, expected "@" but found ".":
528"darth".vader@a.galaxy.far.far.away
529       ^___________________________
530
531while parsing addr_spec
532",
533    ),
534)
535"#
536    );
537
538    k9::snapshot!(
539        parse_with("a@[127.0.0.1]".as_bytes(), addr_spec),
540        r#"
541Ok(
542    AddrSpec {
543        local_part: "a",
544        domain: "[127.0.0.1]",
545    },
546)
547"#
548    );
549
550    k9::snapshot!(
551        parse_with("a@[IPv6::1]".as_bytes(), addr_spec),
552        r#"
553Ok(
554    AddrSpec {
555        local_part: "a",
556        domain: "[IPv6::1]",
557    },
558)
559"#
560    );
561}
562
563// atom = { cfws? ~ atext ~ cfws? }
564fn atom(input: Span) -> IResult<Span, BString> {
565    let (loc, text) = context("atom", delimited(opt(cfws), atext, opt(cfws))).parse(input)?;
566    Ok((loc, (*text).into()))
567}
568
569// word = { atom | quoted_string }
570fn word(input: Span) -> IResult<Span, BString> {
571    context("word", alt((atom, quoted_string))).parse(input)
572}
573
574// obs_local_part = { word ~ (dot ~ word)* }
575fn obs_local_part(input: Span) -> IResult<Span, BString> {
576    let (loc, (word, dotted_words)) =
577        context("obs_local_part", (word, many0((tag("."), word)))).parse(input)?;
578    let mut result = word;
579
580    for (_dot, w) in dotted_words {
581        result.push(b'.');
582        result.push_str(&w);
583    }
584
585    Ok((loc, result))
586}
587
588// local_part = { dot_atom | quoted_string | obs_local_part }
589fn local_part(input: Span) -> IResult<Span, BString> {
590    context("local_part", alt((dot_atom, quoted_string, obs_local_part))).parse(input)
591}
592
593// domain = { dot_atom | domain_literal | obs_domain }
594fn domain(input: Span) -> IResult<Span, BString> {
595    context("domain", alt((dot_atom, domain_literal, obs_domain))).parse(input)
596}
597
598// obs_domain = { atom ~ ( dot ~ atom)* }
599fn obs_domain(input: Span) -> IResult<Span, BString> {
600    let (loc, (atom, dotted_atoms)) =
601        context("obs_domain", (atom, many0((tag("."), atom)))).parse(input)?;
602    let mut result = atom;
603
604    for (_dot, w) in dotted_atoms {
605        result.push(b'.');
606        result.push_str(&w);
607    }
608
609    Ok((loc, result))
610}
611
612// domain_literal = { cfws? ~ "[" ~ (fws? ~ dtext)* ~ fws? ~ "]" ~ cfws? }
613fn domain_literal(input: Span) -> IResult<Span, BString> {
614    let (loc, (bits, trailer)) = context(
615        "domain_literal",
616        delimited(
617            opt(cfws),
618            delimited(
619                tag("["),
620                (
621                    many0((opt(fws), alt((take_while_m_n(1, 1, is_dtext), quoted_pair)))),
622                    opt(fws),
623                ),
624                tag("]"),
625            ),
626            opt(cfws),
627        ),
628    )
629    .parse(input)?;
630
631    let mut result = BString::default();
632    result.push(b'[');
633    for (a, b) in bits {
634        if let Some(a) = a {
635            result.push_str(&a);
636        }
637        result.push_str(b);
638    }
639    if let Some(t) = trailer {
640        result.push_str(&t);
641    }
642    result.push(b']');
643    Ok((loc, result))
644}
645
646// dot_atom_text = @{ atext ~ ("." ~ atext)* }
647fn dot_atom_text(input: Span) -> IResult<Span, BString> {
648    let (loc, (a, b)) =
649        context("dot_atom_text", (atext, many0(preceded(tag("."), atext)))).parse(input)?;
650    let mut result: BString = (*a).into();
651    for item in b {
652        result.push(b'.');
653        result.push_str(&item);
654    }
655
656    Ok((loc, result))
657}
658
659// dot_atom = { cfws? ~ dot_atom_text ~ cfws? }
660fn dot_atom(input: Span) -> IResult<Span, BString> {
661    context("dot_atom", delimited(opt(cfws), dot_atom_text, opt(cfws))).parse(input)
662}
663
664#[cfg(test)]
665#[test]
666fn test_dot_atom() {
667    k9::snapshot!(
668        parse_with("hello".as_bytes(), dot_atom),
669        r#"
670Ok(
671    "hello",
672)
673"#
674    );
675
676    k9::snapshot!(
677        parse_with("hello.there".as_bytes(), dot_atom),
678        r#"
679Ok(
680    "hello.there",
681)
682"#
683    );
684
685    k9::snapshot!(
686        parse_with("hello.".as_bytes(), dot_atom),
687        r#"
688Err(
689    HeaderParse(
690        "Error at line 1, in Eof:
691hello.
692     ^
693
694",
695    ),
696)
697"#
698    );
699
700    k9::snapshot!(
701        parse_with("(wat)hello".as_bytes(), dot_atom),
702        r#"
703Ok(
704    "hello",
705)
706"#
707    );
708}
709
710// cfws = { ( (fws? ~ comment)+ ~ fws?) | fws }
711fn cfws(input: Span) -> IResult<Span, Span> {
712    context(
713        "cfws",
714        recognize(alt((
715            recognize((many1((opt(fws), comment)), opt(fws))),
716            fws,
717        ))),
718    )
719    .parse(input)
720}
721
722// comment = { "(" ~ (fws? ~ ccontent)* ~ fws? ~ ")" }
723fn comment(input: Span) -> IResult<Span, Span> {
724    context(
725        "comment",
726        recognize((tag("("), many0((opt(fws), ccontent)), opt(fws), tag(")"))),
727    )
728    .parse(input)
729}
730
731#[cfg(test)]
732#[test]
733fn test_comment() {
734    k9::snapshot!(
735        BStr::new(&parse_with("(wat)".as_bytes(), comment).unwrap()),
736        "(wat)"
737    );
738}
739
740// ccontent = { ctext | quoted_pair | comment | encoded_word }
741fn ccontent(input: Span) -> IResult<Span, Span> {
742    context(
743        "ccontent",
744        recognize(alt((
745            recognize(take_while_m_n(1, 1, is_ctext)),
746            recognize(quoted_pair),
747            comment,
748            recognize(encoded_word),
749        ))),
750    )
751    .parse(input)
752}
753
754fn is_quoted_pair(c: u8) -> bool {
755    match c {
756        0x00 | b'\r' | b'\n' | b' ' => true,
757        c => is_obs_no_ws_ctl(c) || is_vchar(c),
758    }
759}
760
761// quoted_pair = { ( "\\"  ~ (vchar | wsp)) | obs_qp }
762// obs_qp = { "\\" ~ ( "\u{00}" | obs_no_ws_ctl | "\r" | "\n") }
763fn quoted_pair(input: Span) -> IResult<Span, Span> {
764    context(
765        "quoted_pair",
766        preceded(tag("\\"), take_while_m_n(1, 1, is_quoted_pair)),
767    )
768    .parse(input)
769}
770
771// encoded_word = { "=?" ~ charset ~ ("*" ~ language)? ~ "?" ~ encoding ~ "?" ~ encoded_text ~ "?=" }
772fn encoded_word(input: Span) -> IResult<Span, BString> {
773    let (loc, (charset, _language, _, encoding, _, text)) = context(
774        "encoded_word",
775        delimited(
776            tag("=?"),
777            (
778                charset,
779                opt(preceded(tag("*"), language)),
780                tag("?"),
781                encoding,
782                tag("?"),
783                encoded_text,
784            ),
785            tag("?="),
786        ),
787    )
788    .parse(input)?;
789
790    let bytes = match *encoding.fragment() {
791        b"B" | b"b" => data_encoding::BASE64_MIME
792            .decode(text.as_bytes())
793            .map_err(|err| {
794                make_context_error(
795                    input,
796                    format!("encoded_word: base64 decode failed: {err:#}"),
797                )
798            })?,
799        b"Q" | b"q" => {
800            // for rfc2047 header encoding, _ can be used to represent a space
801            let munged = text.replace("_", " ");
802            // The quoted_printable crate will unhelpfully strip trailing space
803            // from the decoded input string, and we must track and restore it
804            let had_trailing_space = munged.ends_with_str(" ");
805            let mut decoded = quoted_printable::decode(munged, quoted_printable::ParseMode::Robust)
806                .map_err(|err| {
807                    make_context_error(
808                        input,
809                        format!("encoded_word: quoted printable decode failed: {err:#}"),
810                    )
811                })?;
812            if had_trailing_space && !decoded.ends_with(b" ") {
813                decoded.push(b' ');
814            }
815            decoded
816        }
817        encoding => {
818            let encoding = BStr::new(encoding);
819            return Err(make_context_error(
820                input,
821                format!(
822                    "encoded_word: invalid encoding '{encoding}', expected one of b, B, q or Q"
823                ),
824            ));
825        }
826    };
827
828    let charset_name = charset.to_str().map_err(|err| {
829        make_context_error(
830            input,
831            format!(
832                "encoded_word: charset {} is not UTF-8: {err}",
833                BStr::new(*charset)
834            ),
835        )
836    })?;
837
838    let charset = Encoding::by_name(&*charset_name).ok_or_else(|| {
839        make_context_error(
840            input,
841            format!("encoded_word: unsupported charset '{charset_name}'"),
842        )
843    })?;
844
845    let decoded = charset.decode_simple(&bytes).map_err(|err| {
846        make_context_error(
847            input,
848            format!("encoded_word: failed to decode as '{charset_name}': {err}"),
849        )
850    })?;
851
852    Ok((loc, decoded.into()))
853}
854
855// charset = @{ (!"*" ~ token)+ }
856fn charset(input: Span) -> IResult<Span, Span> {
857    context("charset", take_while1(|c| c != b'*' && is_token(c))).parse(input)
858}
859
860// language = @{ token+ }
861fn language(input: Span) -> IResult<Span, Span> {
862    context("language", take_while1(|c| c != b'*' && is_token(c))).parse(input)
863}
864
865// encoding = @{ token+ }
866fn encoding(input: Span) -> IResult<Span, Span> {
867    context("encoding", take_while1(|c| c != b'*' && is_token(c))).parse(input)
868}
869
870// encoded_text = @{ (!( " " | "?") ~ vchar)+ }
871fn encoded_text(input: Span) -> IResult<Span, Span> {
872    context(
873        "encoded_text",
874        take_while1(|c| is_vchar(c) && c != b' ' && c != b'?'),
875    )
876    .parse(input)
877}
878
879// quoted_string = { cfws? ~ "\"" ~ (fws? ~ qcontent)* ~ fws? ~ "\"" ~ cfws? }
880fn quoted_string(input: Span) -> IResult<Span, BString> {
881    let (loc, (bits, trailer)) = context(
882        "quoted_string",
883        delimited(
884            opt(cfws),
885            delimited(
886                tag("\""),
887                (many0((opt(fws), qcontent)), opt(fws)),
888                tag("\""),
889            ),
890            opt(cfws),
891        ),
892    )
893    .parse(input)?;
894
895    let mut result = BString::default();
896    for (a, b) in bits {
897        if let Some(a) = a {
898            result.push_str(&a);
899        }
900        result.push_str(b);
901    }
902    if let Some(t) = trailer {
903        result.push_str(&t);
904    }
905    Ok((loc, result))
906}
907
908// qcontent = { qtext | quoted_pair }
909fn qcontent(input: Span) -> IResult<Span, Span> {
910    context(
911        "qcontent",
912        alt((take_while_m_n(1, 1, is_qtext), quoted_pair)),
913    )
914    .parse(input)
915}
916
917fn content_id(input: Span) -> IResult<Span, MessageID> {
918    let (loc, id) = context("content_id", msg_id).parse(input)?;
919    Ok((loc, id))
920}
921
922fn msg_id(input: Span) -> IResult<Span, MessageID> {
923    let (loc, id) = context("msg_id", alt((strict_msg_id, relaxed_msg_id))).parse(input)?;
924    Ok((loc, id))
925}
926
927fn relaxed_msg_id(input: Span) -> IResult<Span, MessageID> {
928    let (loc, id) = context(
929        "msg_id",
930        delimited(
931            preceded(opt(cfws), tag("<")),
932            many0(take_while_m_n(1, 1, not_angle)),
933            preceded(tag(">"), opt(cfws)),
934        ),
935    )
936    .parse(input)?;
937
938    let mut result = BString::default();
939    for item in id.into_iter() {
940        result.push_str(*item);
941    }
942
943    Ok((loc, MessageID(result)))
944}
945
946// msg_id_list = { msg_id+ }
947fn msg_id_list(input: Span) -> IResult<Span, Vec<MessageID>> {
948    context("msg_id_list", many1(msg_id)).parse(input)
949}
950
951// id_left = { dot_atom_text | obs_id_left }
952// obs_id_left = { local_part }
953fn id_left(input: Span) -> IResult<Span, BString> {
954    context("id_left", alt((dot_atom_text, local_part))).parse(input)
955}
956
957// id_right = { dot_atom_text | no_fold_literal | obs_id_right }
958// obs_id_right = { domain }
959fn id_right(input: Span) -> IResult<Span, BString> {
960    context("id_right", alt((dot_atom_text, no_fold_literal, domain))).parse(input)
961}
962
963// no_fold_literal = { "[" ~ dtext* ~ "]" }
964fn no_fold_literal(input: Span) -> IResult<Span, BString> {
965    context(
966        "no_fold_literal",
967        map(
968            recognize((tag("["), take_while(is_dtext), tag("]"))),
969            |s: Span| (*s).into(),
970        ),
971    )
972    .parse(input)
973}
974
975// msg_id = { cfws? ~ "<" ~ id_left ~ "@" ~ id_right ~ ">" ~ cfws? }
976fn strict_msg_id(input: Span) -> IResult<Span, MessageID> {
977    let (loc, (left, _, right)) = context(
978        "msg_id",
979        delimited(
980            preceded(opt(cfws), tag("<")),
981            (id_left, tag("@"), id_right),
982            preceded(tag(">"), opt(cfws)),
983        ),
984    )
985    .parse(input)?;
986
987    let mut result: BString = left.into();
988    result.push_char('@');
989    result.push_str(right);
990
991    Ok((loc, MessageID(result)))
992}
993
994// obs_unstruct = { (( "\r"* ~ "\n"* ~ ((encoded_word | obs_utext)~ "\r"* ~ "\n"*)+) | fws)+ }
995fn unstructured(input: Span) -> IResult<Span, BString> {
996    #[derive(Debug)]
997    enum Word {
998        Encoded(BString),
999        UText(BString),
1000        Fws,
1001    }
1002
1003    let (loc, words) = context(
1004        "unstructured",
1005        many0(alt((
1006            preceded(
1007                map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws),
1008                terminated(
1009                    alt((
1010                        map(encoded_word, Word::Encoded),
1011                        map(obs_utext, |s| Word::UText((*s).into())),
1012                    )),
1013                    map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws),
1014                ),
1015            ),
1016            map(fws, |_| Word::Fws),
1017        ))),
1018    )
1019    .parse(input)?;
1020
1021    #[derive(Debug)]
1022    enum ProcessedWord {
1023        Encoded(BString),
1024        Text(BString),
1025        Fws,
1026    }
1027    let mut processed = vec![];
1028    for w in words {
1029        match w {
1030            Word::Encoded(p) => {
1031                if processed.len() >= 2
1032                    && matches!(processed.last(), Some(ProcessedWord::Fws))
1033                    && matches!(processed[processed.len() - 2], ProcessedWord::Encoded(_))
1034                {
1035                    // Fws between encoded words is elided
1036                    processed.pop();
1037                }
1038                processed.push(ProcessedWord::Encoded(p));
1039            }
1040            Word::Fws => {
1041                // Collapse runs of Fws/newline to a single Fws
1042                if !matches!(processed.last(), Some(ProcessedWord::Fws)) {
1043                    processed.push(ProcessedWord::Fws);
1044                }
1045            }
1046            Word::UText(c) => match processed.last_mut() {
1047                Some(ProcessedWord::Text(prior)) => prior.push_str(c),
1048                _ => processed.push(ProcessedWord::Text(c)),
1049            },
1050        }
1051    }
1052
1053    let mut result = BString::default();
1054    for word in processed {
1055        match word {
1056            ProcessedWord::Encoded(s) | ProcessedWord::Text(s) => {
1057                result.push_str(&s);
1058            }
1059            ProcessedWord::Fws => {
1060                result.push(b' ');
1061            }
1062        }
1063    }
1064
1065    Ok((loc, result))
1066}
1067
1068fn arc_authentication_results(input: Span) -> IResult<Span, ARCAuthenticationResults> {
1069    context(
1070        "arc_authentication_results",
1071        map(
1072            (
1073                preceded(opt(cfws), tag("i")),
1074                preceded(opt(cfws), tag("=")),
1075                preceded(opt(cfws), nom::character::complete::u8),
1076                preceded(opt(cfws), tag(";")),
1077                preceded(opt(cfws), value),
1078                opt(preceded(cfws, nom::character::complete::u32)),
1079                alt((no_result, many1(resinfo))),
1080                opt(cfws),
1081            ),
1082            |(_i, _eq, instance, _semic, serv_id, version, results, _)| ARCAuthenticationResults {
1083                instance,
1084                serv_id: serv_id.into(),
1085                version,
1086                results,
1087            },
1088        ),
1089    )
1090    .parse(input)
1091}
1092
1093fn authentication_results(input: Span) -> IResult<Span, AuthenticationResults> {
1094    context(
1095        "authentication_results",
1096        map(
1097            (
1098                preceded(opt(cfws), value),
1099                opt(preceded(cfws, nom::character::complete::u32)),
1100                alt((no_result, many1(resinfo))),
1101                opt(cfws),
1102            ),
1103            |(serv_id, version, results, _)| AuthenticationResults {
1104                serv_id: serv_id.into(),
1105                version,
1106                results,
1107            },
1108        ),
1109    )
1110    .parse(input)
1111}
1112
1113fn no_result(input: Span) -> IResult<Span, Vec<AuthenticationResult>> {
1114    context(
1115        "no_result",
1116        map((opt(cfws), tag(";"), opt(cfws), tag("none")), |_| vec![]),
1117    )
1118    .parse(input)
1119}
1120
1121fn resinfo(input: Span) -> IResult<Span, AuthenticationResult> {
1122    context(
1123        "resinfo",
1124        map(
1125            (
1126                opt(cfws),
1127                tag(";"),
1128                methodspec,
1129                opt(preceded(cfws, reasonspec)),
1130                opt(many1(propspec)),
1131            ),
1132            |(_, _, (method, method_version, result), reason, props)| AuthenticationResult {
1133                method: method.into(),
1134                method_version,
1135                result: result.into(),
1136                reason: reason.map(Into::into),
1137                props: match props {
1138                    None => BTreeMap::default(),
1139                    Some(props) => props.into_iter().collect(),
1140                },
1141            },
1142        ),
1143    )
1144    .parse(input)
1145}
1146
1147fn methodspec(input: Span) -> IResult<Span, (BString, Option<u32>, BString)> {
1148    context(
1149        "methodspec",
1150        map(
1151            (
1152                opt(cfws),
1153                (keyword, opt(methodversion)),
1154                opt(cfws),
1155                tag("="),
1156                opt(cfws),
1157                keyword,
1158            ),
1159            |(_, (method, methodversion), _, _, _, result)| (method, methodversion, result),
1160        ),
1161    )
1162    .parse(input)
1163}
1164
1165// Taken from https://datatracker.ietf.org/doc/html/rfc8601 which says
1166// that this is the same as the SMTP Keyword token
1167fn keyword(input: Span) -> IResult<Span, BString> {
1168    context(
1169        "keyword",
1170        map(
1171            take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'+' || c == b'-'),
1172            |s: Span| (*s).into(),
1173        ),
1174    )
1175    .parse(input)
1176}
1177
1178fn methodversion(input: Span) -> IResult<Span, u32> {
1179    context(
1180        "methodversion",
1181        preceded(
1182            (opt(cfws), tag("/"), opt(cfws)),
1183            nom::character::complete::u32,
1184        ),
1185    )
1186    .parse(input)
1187}
1188
1189fn reasonspec(input: Span) -> IResult<Span, BString> {
1190    context(
1191        "reason",
1192        map(
1193            (tag("reason"), opt(cfws), tag("="), opt(cfws), value),
1194            |(_, _, _, _, value)| value,
1195        ),
1196    )
1197    .parse(input)
1198}
1199
1200fn propspec(input: Span) -> IResult<Span, (BString, BString)> {
1201    context(
1202        "propspec",
1203        map(
1204            (
1205                opt(cfws),
1206                keyword,
1207                opt(cfws),
1208                tag("."),
1209                opt(cfws),
1210                keyword,
1211                opt(cfws),
1212                tag("="),
1213                opt(cfws),
1214                alt((
1215                    map(preceded(tag("@"), domain), |d| {
1216                        let mut at_dom = BString::from("@");
1217                        at_dom.push_str(d);
1218                        at_dom
1219                    }),
1220                    map(separated_pair(local_part, tag("@"), domain), |(u, d)| {
1221                        let mut result: BString = u.into();
1222                        result.push(b'@');
1223                        result.push_str(d);
1224                        result
1225                    }),
1226                    domain,
1227                    // value must be last in this alternation
1228                    value,
1229                )),
1230                opt(cfws),
1231            ),
1232            |(_, ptype, _, _, _, property, _, _, _, value, _)| {
1233                (format!("{ptype}.{property}").into(), value)
1234            },
1235        ),
1236    )
1237    .parse(input)
1238}
1239
1240// obs_utext = @{ "\u{00}" | obs_no_ws_ctl | vchar }
1241fn obs_utext(input: Span) -> IResult<Span, Span> {
1242    context(
1243        "obs_utext",
1244        take_while_m_n(1, 1, |c| c == 0x00 || is_obs_no_ws_ctl(c) || is_vchar(c)),
1245    )
1246    .parse(input)
1247}
1248
1249fn is_mime_token(c: u8) -> bool {
1250    is_char(c) && c != b' ' && !is_ctl(c) && !is_tspecial(c)
1251}
1252
1253// mime_token = { (!(" " | ctl | tspecials) ~ char)+ }
1254fn mime_token(input: Span) -> IResult<Span, Span> {
1255    context("mime_token", take_while1(is_mime_token)).parse(input)
1256}
1257
1258// RFC2045 modified by RFC2231 MIME header fields
1259// content_type = { cfws? ~ mime_type ~ cfws? ~ "/" ~ cfws? ~ subtype ~
1260//  cfws? ~ (";"? ~ cfws? ~ parameter ~ cfws?)*
1261// }
1262fn content_type(input: Span) -> IResult<Span, MimeParameters> {
1263    let (loc, (mime_type, _, _, _, mime_subtype, _, parameters)) = context(
1264        "content_type",
1265        preceded(
1266            opt(cfws),
1267            (
1268                mime_token,
1269                opt(cfws),
1270                tag("/"),
1271                opt(cfws),
1272                mime_token,
1273                opt(cfws),
1274                many0(preceded(
1275                    // Note that RFC 2231 is a bit of a mess, showing examples
1276                    // without `;` as a separator in the original text, but
1277                    // in the errata from several years later, corrects those
1278                    // to show the `;`.
1279                    // In the meantime, there are implementations that assume
1280                    // that the `;` is optional, so we therefore allow them
1281                    // to be optional here in our implementation
1282                    preceded(opt(tag(";")), opt(cfws)),
1283                    terminated(parameter, opt(cfws)),
1284                )),
1285            ),
1286        ),
1287    )
1288    .parse(input)?;
1289
1290    let mut value: BString = (*mime_type).into();
1291    value.push_char('/');
1292    value.push_str(mime_subtype);
1293
1294    Ok((loc, MimeParameters { value, parameters }))
1295}
1296
1297fn content_transfer_encoding(input: Span) -> IResult<Span, MimeParameters> {
1298    let (loc, (value, _, parameters)) = context(
1299        "content_transfer_encoding",
1300        preceded(
1301            opt(cfws),
1302            (
1303                mime_token,
1304                opt(cfws),
1305                many0(preceded(
1306                    // Note that RFC 2231 is a bit of a mess, showing examples
1307                    // without `;` as a separator in the original text, but
1308                    // in the errata from several years later, corrects those
1309                    // to show the `;`.
1310                    // In the meantime, there are implementations that assume
1311                    // that the `;` is optional, so we therefore allow them
1312                    // to be optional here in our implementation
1313                    preceded(opt(tag(";")), opt(cfws)),
1314                    terminated(parameter, opt(cfws)),
1315                )),
1316            ),
1317        ),
1318    )
1319    .parse(input)?;
1320
1321    Ok((
1322        loc,
1323        MimeParameters {
1324            value: value.as_bytes().into(),
1325            parameters,
1326        },
1327    ))
1328}
1329
1330// parameter = { regular_parameter | extended_parameter }
1331fn parameter(input: Span) -> IResult<Span, MimeParameter> {
1332    context(
1333        "parameter",
1334        alt((
1335            // Note that RFC2047 explicitly prohibits both of
1336            // these 2047 cases from appearing here, but that
1337            // major MUAs produce this sort of prohibited content
1338            // and we thus need to accommodate it
1339            param_with_unquoted_rfc2047,
1340            param_with_quoted_rfc2047,
1341            regular_parameter,
1342            extended_param_with_charset,
1343            extended_param_no_charset,
1344        )),
1345    )
1346    .parse(input)
1347}
1348
1349fn param_with_unquoted_rfc2047(input: Span) -> IResult<Span, MimeParameter> {
1350    context(
1351        "param_with_unquoted_rfc2047",
1352        map(
1353            (attribute, opt(cfws), tag("="), opt(cfws), encoded_word),
1354            |(name, _, _, _, value)| MimeParameter {
1355                name: name.as_bytes().into(),
1356                value: value.as_bytes().into(),
1357                section: None,
1358                encoding: MimeParameterEncoding::UnquotedRfc2047,
1359                mime_charset: None,
1360                mime_language: None,
1361            },
1362        ),
1363    )
1364    .parse(input)
1365}
1366
1367fn param_with_quoted_rfc2047(input: Span) -> IResult<Span, MimeParameter> {
1368    context(
1369        "param_with_quoted_rfc2047",
1370        map(
1371            (
1372                attribute,
1373                opt(cfws),
1374                tag("="),
1375                opt(cfws),
1376                delimited(tag("\""), encoded_word, tag("\"")),
1377            ),
1378            |(name, _, _, _, value)| MimeParameter {
1379                name: name.as_bytes().into(),
1380                value: value.as_bytes().into(),
1381                section: None,
1382                encoding: MimeParameterEncoding::QuotedRfc2047,
1383                mime_charset: None,
1384                mime_language: None,
1385            },
1386        ),
1387    )
1388    .parse(input)
1389}
1390
1391fn extended_param_with_charset(input: Span) -> IResult<Span, MimeParameter> {
1392    context(
1393        "extended_param_with_charset",
1394        map(
1395            (
1396                attribute,
1397                opt(section),
1398                tag("*"),
1399                opt(cfws),
1400                tag("="),
1401                opt(cfws),
1402                opt(mime_charset),
1403                tag("'"),
1404                opt(mime_language),
1405                tag("'"),
1406                map(
1407                    recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))),
1408                    |s: Span| (*s).into(),
1409                ),
1410            ),
1411            |(name, section, _, _, _, _, mime_charset, _, mime_language, _, value)| MimeParameter {
1412                name: name.as_bytes().into(),
1413                section,
1414                mime_charset: mime_charset.map(|s| s.as_bytes().into()),
1415                mime_language: mime_language.map(|s| s.as_bytes().into()),
1416                encoding: MimeParameterEncoding::Rfc2231,
1417                value,
1418            },
1419        ),
1420    )
1421    .parse(input)
1422}
1423
1424fn extended_param_no_charset(input: Span) -> IResult<Span, MimeParameter> {
1425    context(
1426        "extended_param_no_charset",
1427        map(
1428            (
1429                attribute,
1430                opt(section),
1431                opt(tag("*")),
1432                opt(cfws),
1433                tag("="),
1434                opt(cfws),
1435                alt((
1436                    quoted_string,
1437                    map(
1438                        recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))),
1439                        |s: Span| (*s).into(),
1440                    ),
1441                )),
1442            ),
1443            |(name, section, star, _, _, _, value)| MimeParameter {
1444                name: name.as_bytes().into(),
1445                section,
1446                mime_charset: None,
1447                mime_language: None,
1448                encoding: if star.is_some() {
1449                    MimeParameterEncoding::Rfc2231
1450                } else {
1451                    MimeParameterEncoding::None
1452                },
1453                value,
1454            },
1455        ),
1456    )
1457    .parse(input)
1458}
1459
1460fn mime_charset(input: Span) -> IResult<Span, Span> {
1461    context(
1462        "mime_charset",
1463        take_while1(|c| is_mime_token(c) && c != b'\''),
1464    )
1465    .parse(input)
1466}
1467
1468fn mime_language(input: Span) -> IResult<Span, Span> {
1469    context(
1470        "mime_language",
1471        take_while1(|c| is_mime_token(c) && c != b'\''),
1472    )
1473    .parse(input)
1474}
1475
1476fn ext_octet(input: Span) -> IResult<Span, Span> {
1477    context(
1478        "ext_octet",
1479        recognize((
1480            tag("%"),
1481            take_while_m_n(2, 2, |b: u8| b.is_ascii_hexdigit()),
1482        )),
1483    )
1484    .parse(input)
1485}
1486
1487// section = { "*" ~ ASCII_DIGIT+ }
1488fn section(input: Span) -> IResult<Span, u32> {
1489    context("section", preceded(tag("*"), nom::character::complete::u32)).parse(input)
1490}
1491
1492// regular_parameter = { attribute ~ cfws? ~ "=" ~ cfws? ~ value }
1493fn regular_parameter(input: Span) -> IResult<Span, MimeParameter> {
1494    context(
1495        "regular_parameter",
1496        map(
1497            (attribute, opt(cfws), tag("="), opt(cfws), value),
1498            |(name, _, _, _, value)| MimeParameter {
1499                name: name.as_bytes().into(),
1500                value: value.as_bytes().into(),
1501                section: None,
1502                encoding: MimeParameterEncoding::None,
1503                mime_charset: None,
1504                mime_language: None,
1505            },
1506        ),
1507    )
1508    .parse(input)
1509}
1510
1511// attribute = { attribute_char+ }
1512// attribute_char = { !(" " | ctl | tspecials | "*" | "'" | "%") ~ char }
1513fn attribute(input: Span) -> IResult<Span, Span> {
1514    context("attribute", take_while1(is_attribute_char)).parse(input)
1515}
1516
1517fn value(input: Span) -> IResult<Span, BString> {
1518    context(
1519        "value",
1520        alt((map(mime_token, |s: Span| (*s).into()), quoted_string)),
1521    )
1522    .parse(input)
1523}
1524
1525pub struct Parser;
1526
1527impl Parser {
1528    pub fn parse_mailbox_list_header(text: &[u8]) -> Result<MailboxList> {
1529        parse_with(text, mailbox_list)
1530    }
1531
1532    pub fn parse_mailbox_header(text: &[u8]) -> Result<Mailbox> {
1533        parse_with(text, mailbox)
1534    }
1535
1536    pub fn parse_address_list_header(text: &[u8]) -> Result<AddressList> {
1537        parse_with(text, address_list)
1538    }
1539
1540    pub fn parse_msg_id_header(text: &[u8]) -> Result<MessageID> {
1541        parse_with(text, msg_id)
1542    }
1543
1544    pub fn parse_msg_id_header_list(text: &[u8]) -> Result<Vec<MessageID>> {
1545        parse_with(text, msg_id_list)
1546    }
1547
1548    pub fn parse_content_id_header(text: &[u8]) -> Result<MessageID> {
1549        parse_with(text, content_id)
1550    }
1551
1552    pub fn parse_content_type_header(text: &[u8]) -> Result<MimeParameters> {
1553        parse_with(text, content_type)
1554    }
1555
1556    pub fn parse_content_transfer_encoding_header(text: &[u8]) -> Result<MimeParameters> {
1557        parse_with(text, content_transfer_encoding)
1558    }
1559
1560    pub fn parse_unstructured_header(text: &[u8]) -> Result<BString> {
1561        parse_with(text, unstructured)
1562    }
1563
1564    pub fn parse_authentication_results_header(text: &[u8]) -> Result<AuthenticationResults> {
1565        parse_with(text, authentication_results)
1566    }
1567
1568    pub fn parse_arc_authentication_results_header(
1569        text: &[u8],
1570    ) -> Result<ARCAuthenticationResults> {
1571        parse_with(text, arc_authentication_results)
1572    }
1573}
1574
1575#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1576#[serde(deny_unknown_fields)]
1577pub struct ARCAuthenticationResults {
1578    pub instance: u8,
1579    pub serv_id: BString,
1580    pub version: Option<u32>,
1581    pub results: Vec<AuthenticationResult>,
1582}
1583
1584impl EncodeHeaderValue for ARCAuthenticationResults {
1585    fn encode_value(&self) -> SharedString<'static> {
1586        let mut result = format!("i={}; ", self.instance).into_bytes();
1587
1588        match self.version {
1589            Some(v) => result.push_str(&format!("{} {v}", self.serv_id)),
1590            None => result.push_str(&self.serv_id),
1591        };
1592
1593        if self.results.is_empty() {
1594            result.push_str("; none");
1595        } else {
1596            for res in &self.results {
1597                result.push_str(";\r\n\t");
1598                emit_value_token(&res.method, &mut result);
1599                if let Some(v) = res.method_version {
1600                    result.push_str(&format!("/{v}"));
1601                }
1602                result.push(b'=');
1603                emit_value_token(res.result.as_bytes(), &mut result);
1604                if let Some(reason) = &res.reason {
1605                    result.push_str(" reason=");
1606                    emit_value_token(reason.as_bytes(), &mut result);
1607                }
1608                for (k, v) in &res.props {
1609                    result.push_str(&format!("\r\n\t{k}="));
1610                    emit_value_token(v.as_bytes(), &mut result);
1611                }
1612            }
1613        }
1614
1615        result.into()
1616    }
1617}
1618
1619#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1620#[serde(deny_unknown_fields)]
1621pub struct AuthenticationResults {
1622    pub serv_id: BString,
1623    #[serde(default)]
1624    pub version: Option<u32>,
1625    #[serde(default)]
1626    pub results: Vec<AuthenticationResult>,
1627}
1628
1629/// Emits a value that was parsed by `value`, into target
1630fn emit_value_token(value: &[u8], target: &mut Vec<u8>) {
1631    let use_quoted_string = !value.iter().all(|&c| is_mime_token(c) || c == b'@');
1632    if use_quoted_string {
1633        target.push(b'"');
1634        for (start, end, c) in value.char_indices() {
1635            if c == '"' || c == '\\' {
1636                target.push(b'\\');
1637            }
1638            target.push_str(&value[start..end]);
1639        }
1640        target.push(b'"');
1641    } else {
1642        target.push_str(value);
1643    }
1644}
1645
1646impl EncodeHeaderValue for AuthenticationResults {
1647    fn encode_value(&self) -> SharedString<'static> {
1648        let mut result = match self.version {
1649            Some(v) => format!("{} {v}", self.serv_id).into_bytes(),
1650            None => self.serv_id.to_vec(),
1651        };
1652        if self.results.is_empty() {
1653            result.push_str("; none");
1654        } else {
1655            for res in &self.results {
1656                result.push_str(";\r\n\t");
1657                emit_value_token(&res.method, &mut result);
1658                if let Some(v) = res.method_version {
1659                    result.push_str(&format!("/{v}"));
1660                }
1661                result.push(b'=');
1662                emit_value_token(res.result.as_bytes(), &mut result);
1663                if let Some(reason) = &res.reason {
1664                    result.push_str(" reason=");
1665                    emit_value_token(reason.as_bytes(), &mut result);
1666                }
1667                for (k, v) in &res.props {
1668                    result.push_str(&format!("\r\n\t{k}="));
1669                    emit_value_token(v.as_bytes(), &mut result);
1670                }
1671            }
1672        }
1673
1674        result.into()
1675    }
1676}
1677
1678#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1679#[serde(deny_unknown_fields)]
1680pub struct AuthenticationResult {
1681    pub method: BString,
1682    #[serde(default)]
1683    pub method_version: Option<u32>,
1684    pub result: BString,
1685    #[serde(default)]
1686    pub reason: Option<BString>,
1687    #[serde(default)]
1688    pub props: BTreeMap<BString, BString>,
1689}
1690
1691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1692#[serde(deny_unknown_fields)]
1693pub struct AddrSpec {
1694    pub local_part: BString,
1695    pub domain: BString,
1696}
1697
1698impl AddrSpec {
1699    pub fn new(local_part: &str, domain: &str) -> Self {
1700        Self {
1701            local_part: local_part.into(),
1702            domain: domain.into(),
1703        }
1704    }
1705
1706    pub fn parse(email: &str) -> Result<Self> {
1707        parse_with(email.as_bytes(), addr_spec)
1708    }
1709}
1710
1711impl EncodeHeaderValue for AddrSpec {
1712    fn encode_value(&self) -> SharedString<'static> {
1713        let mut result: Vec<u8> = vec![];
1714
1715        let needs_quoting = !self.local_part.iter().all(|&c| is_atext(c) || c == b'.');
1716        if needs_quoting {
1717            result.push(b'"');
1718            // RFC5321 4.1.2 qtextSMTP:
1719            // within a quoted string, any ASCII graphic or space is permitted without
1720            // blackslash-quoting except double-quote and the backslash itself.
1721
1722            for &c in self.local_part.iter() {
1723                if c == b'"' || c == b'\\' {
1724                    result.push(b'\\');
1725                }
1726                result.push(c);
1727            }
1728            result.push(b'"');
1729        } else {
1730            result.push_str(&self.local_part);
1731        }
1732        result.push(b'@');
1733        result.push_str(&self.domain);
1734
1735        result.into()
1736    }
1737}
1738
1739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1740#[serde(untagged)]
1741pub enum Address {
1742    Mailbox(Mailbox),
1743    Group { name: BString, entries: MailboxList },
1744}
1745
1746#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1747#[serde(deny_unknown_fields, transparent)]
1748pub struct AddressList(pub Vec<Address>);
1749
1750impl std::ops::Deref for AddressList {
1751    type Target = Vec<Address>;
1752    fn deref(&self) -> &Vec<Address> {
1753        &self.0
1754    }
1755}
1756
1757impl AddressList {
1758    pub fn extract_first_mailbox(&self) -> Option<&Mailbox> {
1759        let address = self.0.first()?;
1760        match address {
1761            Address::Mailbox(mailbox) => Some(mailbox),
1762            Address::Group { entries, .. } => entries.extract_first_mailbox(),
1763        }
1764    }
1765}
1766
1767#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1768#[serde(deny_unknown_fields, transparent)]
1769pub struct MailboxList(pub Vec<Mailbox>);
1770
1771impl std::ops::Deref for MailboxList {
1772    type Target = Vec<Mailbox>;
1773    fn deref(&self) -> &Vec<Mailbox> {
1774        &self.0
1775    }
1776}
1777
1778impl MailboxList {
1779    pub fn extract_first_mailbox(&self) -> Option<&Mailbox> {
1780        self.0.first()
1781    }
1782}
1783
1784#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1785#[serde(deny_unknown_fields)]
1786pub struct Mailbox {
1787    pub name: Option<BString>,
1788    pub address: AddrSpec,
1789}
1790
1791#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1792#[serde(transparent)]
1793pub struct MessageID(pub BString);
1794
1795impl EncodeHeaderValue for MessageID {
1796    fn encode_value(&self) -> SharedString<'static> {
1797        let mut result = Vec::<u8>::with_capacity(self.0.len() + 2);
1798        result.push(b'<');
1799        result.push_str(&self.0);
1800        result.push(b'>');
1801        result.into()
1802    }
1803}
1804
1805impl EncodeHeaderValue for Vec<MessageID> {
1806    fn encode_value(&self) -> SharedString<'static> {
1807        let mut result = BString::default();
1808        for id in self {
1809            if !result.is_empty() {
1810                result.push_str("\r\n\t");
1811            }
1812            result.push(b'<');
1813            result.push_str(&id.0);
1814            result.push(b'>');
1815        }
1816        result.into()
1817    }
1818}
1819
1820// In theory, everyone would be aware of RFC 2231 and we can stop here,
1821// but in practice, things are messy.  At some point someone started
1822// to emit encoded-words insides quoted-string values, and for the sake
1823// of compatibility what we see now is technically illegal stuff like
1824// Content-Disposition: attachment; filename="=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?="
1825// being used to represent UTF-8 filenames.
1826// As such, in our RFC 2231 handling, we also need to accommodate
1827// these bogus representations, hence their presence in this enum
1828#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1829pub(crate) enum MimeParameterEncoding {
1830    None,
1831    Rfc2231,
1832    UnquotedRfc2047,
1833    QuotedRfc2047,
1834}
1835
1836#[derive(Debug, Clone, PartialEq, Eq)]
1837struct MimeParameter {
1838    pub name: BString,
1839    pub section: Option<u32>,
1840    pub mime_charset: Option<BString>,
1841    pub mime_language: Option<BString>,
1842    pub encoding: MimeParameterEncoding,
1843    pub value: BString,
1844}
1845
1846#[derive(Debug, Clone, PartialEq, Eq)]
1847pub struct MimeParameters {
1848    pub value: BString,
1849    parameters: Vec<MimeParameter>,
1850}
1851
1852impl MimeParameters {
1853    pub fn new(value: impl AsRef<[u8]>) -> Self {
1854        Self {
1855            value: value.as_ref().into(),
1856            parameters: vec![],
1857        }
1858    }
1859
1860    /// Decode all named parameters per RFC 2231 and return a map
1861    /// of the parameter names to parameters values.
1862    /// Incorrectly encoded parameters are silently ignored
1863    /// and are not returned in the resulting map.
1864    pub fn parameter_map(&self) -> BTreeMap<BString, BString> {
1865        let mut map = BTreeMap::new();
1866
1867        fn contains_key_ignore_case(map: &BTreeMap<BString, BString>, key: &[u8]) -> bool {
1868            for k in map.keys() {
1869                if k.eq_ignore_ascii_case(key) {
1870                    return true;
1871                }
1872            }
1873            false
1874        }
1875
1876        for entry in &self.parameters {
1877            let name = entry.name.as_bytes();
1878            if !contains_key_ignore_case(&map, name) {
1879                if let Some(value) = self.get(name) {
1880                    map.insert(name.into(), value);
1881                }
1882            }
1883        }
1884
1885        map
1886    }
1887
1888    /// Retrieve the value for a named parameter.
1889    /// This method will attempt to decode any %-encoded values
1890    /// per RFC 2231 and combine multi-element fields into a single
1891    /// contiguous value.
1892    /// Invalid charsets and encoding will be silently ignored.
1893    pub fn get(&self, name: impl AsRef<[u8]>) -> Option<BString> {
1894        let name = name.as_ref();
1895        let mut elements: Vec<_> = self
1896            .parameters
1897            .iter()
1898            .filter(|p| p.name.eq_ignore_ascii_case(name.as_bytes()))
1899            .collect();
1900        if elements.is_empty() {
1901            return None;
1902        }
1903        elements.sort_by(|a, b| a.section.cmp(&b.section));
1904
1905        let mut mime_charset = None;
1906        let mut result: Vec<u8> = vec![];
1907
1908        for ele in elements {
1909            if let Some(cset) = ele.mime_charset.as_ref().and_then(|b| b.to_str().ok()) {
1910                mime_charset = Encoding::by_name(&*cset);
1911            }
1912
1913            match ele.encoding {
1914                MimeParameterEncoding::Rfc2231 => {
1915                    if let Some(charset) = mime_charset.as_ref() {
1916                        let mut chars = ele.value.chars();
1917                        let mut bytes: Vec<u8> = vec![];
1918
1919                        fn char_to_bytes(c: char, bytes: &mut Vec<u8>) {
1920                            let mut buf = [0u8; 8];
1921                            let s = c.encode_utf8(&mut buf);
1922                            for b in s.bytes() {
1923                                bytes.push(b);
1924                            }
1925                        }
1926
1927                        'next_char: while let Some(c) = chars.next() {
1928                            match c {
1929                                '%' => {
1930                                    let mut value = 0u8;
1931                                    for _ in 0..2 {
1932                                        match chars.next() {
1933                                            Some(n) => match n {
1934                                                '0'..='9' => {
1935                                                    value <<= 4;
1936                                                    value |= n as u32 as u8 - b'0';
1937                                                }
1938                                                'a'..='f' => {
1939                                                    value <<= 4;
1940                                                    value |= (n as u32 as u8 - b'a') + 10;
1941                                                }
1942                                                'A'..='F' => {
1943                                                    value <<= 4;
1944                                                    value |= (n as u32 as u8 - b'A') + 10;
1945                                                }
1946                                                _ => {
1947                                                    char_to_bytes('%', &mut bytes);
1948                                                    char_to_bytes(n, &mut bytes);
1949                                                    break 'next_char;
1950                                                }
1951                                            },
1952                                            None => {
1953                                                char_to_bytes('%', &mut bytes);
1954                                                break 'next_char;
1955                                            }
1956                                        }
1957                                    }
1958
1959                                    bytes.push(value);
1960                                }
1961                                c => {
1962                                    char_to_bytes(c, &mut bytes);
1963                                }
1964                            }
1965                        }
1966
1967                        if let Ok(decoded) = charset.decode_simple(&bytes) {
1968                            result.push_str(&decoded);
1969                        }
1970                    } else {
1971                        result.push_str(&ele.value);
1972                    }
1973                }
1974                MimeParameterEncoding::UnquotedRfc2047
1975                | MimeParameterEncoding::QuotedRfc2047
1976                | MimeParameterEncoding::None => {
1977                    result.push_str(&ele.value);
1978                }
1979            }
1980        }
1981
1982        Some(result.into())
1983    }
1984
1985    /// Remove the named parameter
1986    pub fn remove(&mut self, name: impl AsRef<[u8]>) {
1987        let name = name.as_ref();
1988        self.parameters
1989            .retain(|p| !p.name.eq_ignore_ascii_case(name));
1990    }
1991
1992    pub fn set(&mut self, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
1993        self.set_with_encoding(name, value, MimeParameterEncoding::None)
1994    }
1995
1996    pub(crate) fn set_with_encoding(
1997        &mut self,
1998        name: impl AsRef<[u8]>,
1999        value: impl AsRef<[u8]>,
2000        encoding: MimeParameterEncoding,
2001    ) {
2002        self.remove(name.as_ref());
2003
2004        self.parameters.push(MimeParameter {
2005            name: name.as_ref().into(),
2006            value: value.as_ref().into(),
2007            section: None,
2008            mime_charset: None,
2009            mime_language: None,
2010            encoding,
2011        });
2012    }
2013
2014    pub fn is_multipart(&self) -> bool {
2015        self.value.starts_with_str("message/") || self.value.starts_with_str("multipart/")
2016    }
2017
2018    pub fn is_text(&self) -> bool {
2019        self.value.starts_with_str("text/")
2020    }
2021}
2022
2023impl EncodeHeaderValue for MimeParameters {
2024    fn encode_value(&self) -> SharedString<'static> {
2025        let mut result = self.value.clone();
2026        let names: BTreeMap<&BStr, MimeParameterEncoding> = self
2027            .parameters
2028            .iter()
2029            .map(|p| (p.name.as_bstr(), p.encoding))
2030            .collect();
2031
2032        for (name, stated_encoding) in names {
2033            let value = self.get(name).expect("name to be present");
2034
2035            match stated_encoding {
2036                MimeParameterEncoding::UnquotedRfc2047 => {
2037                    let encoded = qp_encode(&value);
2038                    result.push_str(&format!(";\r\n\t{name}={encoded}"));
2039                }
2040                MimeParameterEncoding::QuotedRfc2047 => {
2041                    let encoded = qp_encode(&value);
2042                    result.push_str(&format!(";\r\n\t{name}=\"{encoded}\""));
2043                }
2044                MimeParameterEncoding::None | MimeParameterEncoding::Rfc2231 => {
2045                    let needs_encoding = value.iter().any(|&c| !is_mime_token(c) || !c.is_ascii());
2046                    // Prefer to use quoted_string representation when possible, as it doesn't
2047                    // require any RFC 2231 encoding
2048                    let use_quoted_string = value
2049                        .iter()
2050                        .all(|&c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii());
2051
2052                    let mut params = vec![];
2053                    let mut chars = value.char_indices().peekable();
2054                    while chars.peek().is_some() {
2055                        let count = params.len();
2056                        let is_first = count == 0;
2057                        let prefix = if use_quoted_string {
2058                            "\""
2059                        } else if is_first && needs_encoding {
2060                            "UTF-8''"
2061                        } else {
2062                            ""
2063                        };
2064                        let limit = 74 - (name.len() + 4 + prefix.len());
2065
2066                        let mut encoded: Vec<u8> = vec![];
2067
2068                        while encoded.len() < limit {
2069                            let Some((start, end, c)) = chars.next() else {
2070                                break;
2071                            };
2072                            let s = &value[start..end];
2073
2074                            if use_quoted_string {
2075                                if c == '"' || c == '\\' {
2076                                    encoded.push(b'\\');
2077                                }
2078                                encoded.push_str(s);
2079                            } else if (c as u32) <= 0xff
2080                                && is_mime_token(c as u32 as u8)
2081                                && (!needs_encoding || c != '%')
2082                            {
2083                                encoded.push_str(s);
2084                            } else {
2085                                for b in s.bytes() {
2086                                    encoded.push(b'%');
2087                                    encoded.push(HEX_CHARS[(b as usize) >> 4]);
2088                                    encoded.push(HEX_CHARS[(b as usize) & 0x0f]);
2089                                }
2090                            }
2091                        }
2092
2093                        if use_quoted_string {
2094                            encoded.push(b'"');
2095                        }
2096
2097                        params.push(MimeParameter {
2098                            name: name.into(),
2099                            section: Some(count as u32),
2100                            mime_charset: if is_first { Some("UTF-8".into()) } else { None },
2101                            mime_language: None,
2102                            encoding: if needs_encoding {
2103                                MimeParameterEncoding::Rfc2231
2104                            } else {
2105                                MimeParameterEncoding::None
2106                            },
2107                            value: encoded.into(),
2108                        })
2109                    }
2110                    if params.len() == 1 {
2111                        params.last_mut().map(|p| p.section = None);
2112                    }
2113                    for p in params {
2114                        result.push_str(";\r\n\t");
2115                        let charset_tick = if !use_quoted_string
2116                            && (p.mime_charset.is_some() || p.mime_language.is_some())
2117                        {
2118                            "'"
2119                        } else {
2120                            ""
2121                        };
2122                        let lang_tick = if !use_quoted_string
2123                            && (p.mime_language.is_some() || p.mime_charset.is_some())
2124                        {
2125                            "'"
2126                        } else {
2127                            ""
2128                        };
2129
2130                        let section = p
2131                            .section
2132                            .map(|s| format!("*{s}"))
2133                            .unwrap_or_else(String::new);
2134
2135                        let uses_encoding =
2136                            if !use_quoted_string && p.encoding == MimeParameterEncoding::Rfc2231 {
2137                                "*"
2138                            } else {
2139                                ""
2140                            };
2141                        let charset = if use_quoted_string {
2142                            BStr::new("\"")
2143                        } else {
2144                            p.mime_charset
2145                                .as_ref()
2146                                .map(|b| b.as_bstr())
2147                                .unwrap_or(BStr::new(""))
2148                        };
2149                        let lang = p
2150                            .mime_language
2151                            .as_ref()
2152                            .map(|b| b.as_bstr())
2153                            .unwrap_or(BStr::new(""));
2154
2155                        let line = format!(
2156                            "{name}{section}{uses_encoding}={charset}{charset_tick}{lang}{lang_tick}{value}",
2157                            name = &p.name,
2158                            value = &p.value
2159                        );
2160                        result.push_str(&line);
2161                    }
2162                }
2163            }
2164        }
2165        result.into()
2166    }
2167}
2168
2169static HEX_CHARS: &[u8] = b"0123456789ABCDEF";
2170
2171pub(crate) fn qp_encode(s: &[u8]) -> String {
2172    let prefix = b"=?UTF-8?q?";
2173    let suffix = b"?=";
2174    let limit = 72 - (prefix.len() + suffix.len());
2175
2176    let mut result = Vec::with_capacity(s.len());
2177
2178    result.extend_from_slice(prefix);
2179    let mut line_length = 0;
2180
2181    enum Bytes<'a> {
2182        Passthru(&'a [u8]),
2183        Encode(&'a [u8]),
2184    }
2185
2186    // Iterate by char so that we don't confuse space (0x20) with a
2187    // utf8 subsequence and incorrectly encode the input string.
2188    for (start, end, c) in s.char_indices() {
2189        let bytes = &s[start..end];
2190
2191        let b = if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
2192            && c != '?'
2193            && c != '='
2194            && c != ' '
2195            && c != '\t'
2196        {
2197            Bytes::Passthru(bytes)
2198        } else if c == ' ' {
2199            Bytes::Passthru(b"_")
2200        } else {
2201            Bytes::Encode(bytes)
2202        };
2203
2204        let need_len = match b {
2205            Bytes::Passthru(b) => b.len(),
2206            Bytes::Encode(b) => b.len() * 3,
2207        };
2208
2209        if need_len > limit - line_length {
2210            // Need to wrap
2211            result.extend_from_slice(suffix);
2212            result.extend_from_slice(b"\r\n\t");
2213            result.extend_from_slice(prefix);
2214            line_length = 0;
2215        }
2216
2217        match b {
2218            Bytes::Passthru(c) => {
2219                result.extend_from_slice(c);
2220            }
2221            Bytes::Encode(bytes) => {
2222                for &c in bytes {
2223                    result.push(b'=');
2224                    result.push(HEX_CHARS[(c as usize) >> 4]);
2225                    result.push(HEX_CHARS[(c as usize) & 0x0f]);
2226                }
2227            }
2228        }
2229
2230        line_length += need_len;
2231    }
2232
2233    if line_length > 0 {
2234        result.extend_from_slice(suffix);
2235    }
2236
2237    // Safety: we ensured that everything we output is in the ASCII
2238    // range, therefore the string is valid UTF-8
2239    unsafe { String::from_utf8_unchecked(result) }
2240}
2241
2242#[cfg(test)]
2243#[test]
2244fn test_qp_encode() {
2245    let encoded = qp_encode(
2246        b"hello, I am a line that is this long, or maybe a little \
2247        bit longer than this, and that should get wrapped by the encoder",
2248    );
2249    k9::snapshot!(
2250        encoded,
2251        r#"
2252=?UTF-8?q?hello,_I_am_a_line_that_is_this_long,_or_maybe_a_little_bit_?=\r
2253\t=?UTF-8?q?longer_than_this,_and_that_should_get_wrapped_by_the_encoder?=
2254"#
2255    );
2256}
2257
2258/// Quote input string `s`, using a backslash escape, if any
2259/// of the characters is NOT atext.  When quoting, the input
2260/// string is enclosed in quotes.
2261fn quote_string(s: impl AsRef<[u8]>) -> BString {
2262    let s = s.as_ref();
2263
2264    if s.iter().any(|&c| !is_atext(c)) {
2265        let mut result = Vec::<u8>::with_capacity(s.len() + 4);
2266        result.push(b'"');
2267        for (start, end, c) in s.char_indices() {
2268            let c = c as u32;
2269            if c <= 0xff {
2270                let c = c as u8;
2271                if !c.is_ascii_whitespace() && !is_qtext(c) && !is_atext(c) {
2272                    result.push(b'\\');
2273                }
2274            }
2275            result.push_str(&s[start..end]);
2276        }
2277        result.push(b'"');
2278        result.into()
2279    } else {
2280        s.into()
2281    }
2282}
2283
2284#[cfg(test)]
2285#[test]
2286fn test_quote_string() {
2287    k9::snapshot!(
2288        quote_string("TEST [ne_pas_repondre]"),
2289        r#""TEST [ne_pas_repondre]""#
2290    );
2291    k9::snapshot!(quote_string("hello"), "hello");
2292    k9::snapshot!(quote_string("hello there"), r#""hello there""#);
2293    k9::snapshot!(quote_string("hello, there"), "\"hello, there\"");
2294    k9::snapshot!(quote_string("hello \"there\""), r#""hello \\"there\\"""#);
2295    k9::snapshot!(
2296        quote_string("hello c:\\backslash"),
2297        r#""hello c:\\\\backslash""#
2298    );
2299    k9::assert_equal!(quote_string("hello\n there"), "\"hello\n there\"");
2300}
2301
2302impl EncodeHeaderValue for Mailbox {
2303    fn encode_value(&self) -> SharedString<'static> {
2304        match &self.name {
2305            Some(name) => {
2306                let mut value: Vec<u8> = if name.is_ascii() {
2307                    quote_string(name).into()
2308                } else {
2309                    qp_encode(name.as_bytes()).into_bytes()
2310                };
2311
2312                value.push_str(" <");
2313                value.push_str(self.address.encode_value().as_bytes());
2314                value.push(b'>');
2315                value.into()
2316            }
2317            None => {
2318                let mut result: Vec<u8> = vec![];
2319                result.push(b'<');
2320                result.push_str(self.address.encode_value().as_bytes());
2321                result.push(b'>');
2322                result.into()
2323            }
2324        }
2325    }
2326}
2327
2328impl EncodeHeaderValue for MailboxList {
2329    fn encode_value(&self) -> SharedString<'static> {
2330        let mut result: Vec<u8> = vec![];
2331        for mailbox in &self.0 {
2332            if !result.is_empty() {
2333                result.push_str(",\r\n\t");
2334            }
2335            result.push_str(mailbox.encode_value().as_bytes());
2336        }
2337        result.into()
2338    }
2339}
2340
2341impl EncodeHeaderValue for Address {
2342    fn encode_value(&self) -> SharedString<'static> {
2343        match self {
2344            Self::Mailbox(mbox) => mbox.encode_value(),
2345            Self::Group { name, entries } => {
2346                let mut result: Vec<u8> = vec![];
2347                result.push_str(name);
2348                result.push(b':');
2349                result.push_str(entries.encode_value().as_bytes());
2350                result.push(b';');
2351                result.into()
2352            }
2353        }
2354    }
2355}
2356
2357impl EncodeHeaderValue for AddressList {
2358    fn encode_value(&self) -> SharedString<'static> {
2359        let mut result: Vec<u8> = vec![];
2360        for address in &self.0 {
2361            if !result.is_empty() {
2362                result.push_str(",\r\n\t");
2363            }
2364            result.push_str(address.encode_value().as_bytes());
2365        }
2366        result.into()
2367    }
2368}
2369
2370#[cfg(test)]
2371mod test {
2372    use super::*;
2373    use crate::{Header, MessageConformance, MimePart};
2374
2375    #[test]
2376    fn mailbox_encodes_at() {
2377        let mbox = Mailbox {
2378            name: Some("foo@bar.com".into()),
2379            address: AddrSpec {
2380                local_part: "foo".into(),
2381                domain: "bar.com".into(),
2382            },
2383        };
2384        assert_eq!(mbox.encode_value(), "\"foo@bar.com\" <foo@bar.com>");
2385    }
2386
2387    #[test]
2388    fn mailbox_list_singular() {
2389        let message = concat!(
2390            "From:  Someone (hello) <someone@example.com>, other@example.com,\n",
2391            "  \"John \\\"Smith\\\"\" (comment) \"More Quotes\" (more comment) <someone(another comment)@crazy.example.com(woot)>\n",
2392            "\n",
2393            "I am the body"
2394        );
2395        let msg = MimePart::parse(message).unwrap();
2396        let list = match msg.headers().from() {
2397            Err(err) => panic!("Doh.\n{err:#}"),
2398            Ok(list) => list,
2399        };
2400
2401        k9::snapshot!(
2402            list,
2403            r#"
2404Some(
2405    MailboxList(
2406        [
2407            Mailbox {
2408                name: Some(
2409                    "Someone",
2410                ),
2411                address: AddrSpec {
2412                    local_part: "someone",
2413                    domain: "example.com",
2414                },
2415            },
2416            Mailbox {
2417                name: None,
2418                address: AddrSpec {
2419                    local_part: "other",
2420                    domain: "example.com",
2421                },
2422            },
2423            Mailbox {
2424                name: Some(
2425                    "John "Smith" More Quotes",
2426                ),
2427                address: AddrSpec {
2428                    local_part: "someone",
2429                    domain: "crazy.example.com",
2430                },
2431            },
2432        ],
2433    ),
2434)
2435"#
2436        );
2437    }
2438
2439    #[test]
2440    fn docomo_non_compliant_localpart() {
2441        let message = "Sender: hello..there@docomo.ne.jp\n\n\n";
2442        let msg = MimePart::parse(message).unwrap();
2443        let err = msg.headers().sender().unwrap_err();
2444        k9::snapshot!(
2445            err,
2446            r#"
2447InvalidHeaderValueDuringGet {
2448    header_name: "Sender",
2449    error: HeaderParse(
2450        "Error at line 1, expected "@" but found ".":
2451hello..there@docomo.ne.jp
2452     ^___________________
2453
2454while parsing addr_spec
2455while parsing mailbox
2456",
2457    ),
2458}
2459"#
2460        );
2461    }
2462
2463    #[test]
2464    fn sender() {
2465        let message = "Sender: someone@[127.0.0.1]\n\n\n";
2466        let msg = MimePart::parse(message).unwrap();
2467        let list = match msg.headers().sender() {
2468            Err(err) => panic!("Doh.\n{err:#}"),
2469            Ok(list) => list,
2470        };
2471        k9::snapshot!(
2472            list,
2473            r#"
2474Some(
2475    Mailbox {
2476        name: None,
2477        address: AddrSpec {
2478            local_part: "someone",
2479            domain: "[127.0.0.1]",
2480        },
2481    },
2482)
2483"#
2484        );
2485    }
2486
2487    #[test]
2488    fn domain_literal() {
2489        let message = "From: someone@[127.0.0.1]\n\n\n";
2490        let msg = MimePart::parse(message).unwrap();
2491        let list = match msg.headers().from() {
2492            Err(err) => panic!("Doh.\n{err:#}"),
2493            Ok(list) => list,
2494        };
2495        k9::snapshot!(
2496            list,
2497            r#"
2498Some(
2499    MailboxList(
2500        [
2501            Mailbox {
2502                name: None,
2503                address: AddrSpec {
2504                    local_part: "someone",
2505                    domain: "[127.0.0.1]",
2506                },
2507            },
2508        ],
2509    ),
2510)
2511"#
2512        );
2513    }
2514
2515    #[test]
2516    fn rfc6532() {
2517        let message = concat!(
2518            "From: Keith Moore <moore@cs.utk.edu>\n",
2519            "To: Keld Jørn Simonsen <keld@dkuug.dk>\n",
2520            "CC: André Pirard <PIRARD@vm1.ulg.ac.be>\n",
2521            "Subject: Hello André\n",
2522            "\n\n"
2523        );
2524        let msg = MimePart::parse(message).unwrap();
2525        let list = match msg.headers().from() {
2526            Err(err) => panic!("Doh.\n{err:#}"),
2527            Ok(list) => list,
2528        };
2529        k9::snapshot!(
2530            list,
2531            r#"
2532Some(
2533    MailboxList(
2534        [
2535            Mailbox {
2536                name: Some(
2537                    "Keith Moore",
2538                ),
2539                address: AddrSpec {
2540                    local_part: "moore",
2541                    domain: "cs.utk.edu",
2542                },
2543            },
2544        ],
2545    ),
2546)
2547"#
2548        );
2549
2550        let list = match msg.headers().to() {
2551            Err(err) => panic!("Doh.\n{err:#}"),
2552            Ok(list) => list,
2553        };
2554        k9::snapshot!(
2555            list,
2556            r#"
2557Some(
2558    AddressList(
2559        [
2560            Mailbox(
2561                Mailbox {
2562                    name: Some(
2563                        "Keld Jørn Simonsen",
2564                    ),
2565                    address: AddrSpec {
2566                        local_part: "keld",
2567                        domain: "dkuug.dk",
2568                    },
2569                },
2570            ),
2571        ],
2572    ),
2573)
2574"#
2575        );
2576
2577        let list = match msg.headers().cc() {
2578            Err(err) => panic!("Doh.\n{err:#}"),
2579            Ok(list) => list,
2580        };
2581        k9::snapshot!(
2582            list,
2583            r#"
2584Some(
2585    AddressList(
2586        [
2587            Mailbox(
2588                Mailbox {
2589                    name: Some(
2590                        "André Pirard",
2591                    ),
2592                    address: AddrSpec {
2593                        local_part: "PIRARD",
2594                        domain: "vm1.ulg.ac.be",
2595                    },
2596                },
2597            ),
2598        ],
2599    ),
2600)
2601"#
2602        );
2603        let list = match msg.headers().subject() {
2604            Err(err) => panic!("Doh.\n{err:#}"),
2605            Ok(list) => list,
2606        };
2607        k9::snapshot!(
2608            list,
2609            r#"
2610Some(
2611    "Hello André",
2612)
2613"#
2614        );
2615    }
2616
2617    #[test]
2618    fn rfc2047_bogus() {
2619        let message = concat!(
2620            "From: =?US-OSCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2621            "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2622            "CC: =?ISO-8859-1?Q?Andr=E?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2623            "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?=\n",
2624            "  =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2625            "\n\n"
2626        );
2627        let msg = MimePart::parse(message).unwrap();
2628
2629        // Invalid charset causes encoded_word to fail and we will instead match
2630        // obs_utext and return it as it was
2631        k9::assert_equal!(
2632            msg.headers().from().unwrap().unwrap().0[0]
2633                .name
2634                .as_ref()
2635                .unwrap(),
2636            "=?US-OSCII?Q?Keith_Moore?="
2637        );
2638
2639        match &msg.headers().cc().unwrap().unwrap().0[0] {
2640            Address::Mailbox(mbox) => {
2641                // 'Andr=E9?=' is in the non-bogus example below, but above we
2642                // broke it as 'Andr=E?=', and instead of triggering a qp decode
2643                // error, it is passed through here as-is
2644                k9::assert_equal!(mbox.name.as_ref().unwrap(), "Andr=E Pirard");
2645            }
2646            wat => panic!("should not have {wat:?}"),
2647        }
2648
2649        // The invalid base64 (an I was replaced by an !) is interpreted as obs_utext
2650        // and passed through to us
2651        k9::assert_equal!(
2652            msg.headers().subject().unwrap().unwrap(),
2653            "Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?= u understand the example."
2654        );
2655    }
2656
2657    #[test]
2658    fn attachment_filename_mess_totally_bogus() {
2659        let message = concat!("Content-Disposition: attachment; filename=@\n", "\n\n");
2660        let msg = MimePart::parse(message).unwrap();
2661        eprintln!("{msg:#?}");
2662
2663        assert!(msg
2664            .conformance()
2665            .contains(MessageConformance::INVALID_MIME_HEADERS));
2666        msg.headers().content_disposition().unwrap_err();
2667
2668        // There is no Content-Disposition in the rebuilt message, because
2669        // there was no valid Content-Disposition in what we parsed
2670        let rebuilt = msg.rebuild(None).unwrap();
2671        k9::assert_equal!(rebuilt.headers().content_disposition(), Ok(None));
2672    }
2673
2674    #[test]
2675    fn attachment_filename_mess_aberrant() {
2676        let message = concat!(
2677            "Content-Disposition: attachment; filename= =?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\n",
2678            "\n\n"
2679        );
2680        let msg = MimePart::parse(message).unwrap();
2681
2682        let cd = msg.headers().content_disposition().unwrap().unwrap();
2683        k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
2684
2685        let encoded = cd.encode_value();
2686        k9::assert_equal!(encoded, "attachment;\r\n\tfilename==?UTF-8?q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98?=");
2687    }
2688
2689    #[test]
2690    fn attachment_filename_mess_gmail() {
2691        let message = concat!(
2692            "Content-Disposition: attachment; filename=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
2693            "Content-Type: text/plain;\n",
2694            "   name=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
2695            "\n\n"
2696        );
2697        let msg = MimePart::parse(message).unwrap();
2698
2699        let cd = msg.headers().content_disposition().unwrap().unwrap();
2700        k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
2701        let encoded = cd.encode_value();
2702        k9::assert_equal!(encoded, "attachment;\r\n\tfilename=\"=?UTF-8?q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98?=\"");
2703
2704        let ct = msg.headers().content_type().unwrap().unwrap();
2705        k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付");
2706    }
2707
2708    #[test]
2709    fn attachment_filename_mess_fastmail() {
2710        let message = concat!(
2711            "Content-Disposition: attachment;\n",
2712            "  filename*0*=utf-8''%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E6%B7%BB%E4%BB%98;\n",
2713            "  filename*1*=.txt\n",
2714            "Content-Type: text/plain;\n",
2715            "   name=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=\"\n",
2716            "   x-name=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=bork\"\n",
2717            "\n\n"
2718        );
2719        let msg = MimePart::parse(message).unwrap();
2720
2721        let cd = msg.headers().content_disposition().unwrap().unwrap();
2722        k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付.txt");
2723
2724        let ct = msg.headers().content_type().unwrap().unwrap();
2725        eprintln!("{ct:#?}");
2726        k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付.txt");
2727        k9::assert_equal!(
2728            ct.get("x-name").unwrap(),
2729            "=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=bork"
2730        );
2731    }
2732
2733    #[test]
2734    fn rfc2047() {
2735        let message = concat!(
2736            "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2737            "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2738            "CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2739            "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n",
2740            "  =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2741            "\n\n"
2742        );
2743        let msg = MimePart::parse(message).unwrap();
2744        let list = match msg.headers().from() {
2745            Err(err) => panic!("Doh.\n{err:#}"),
2746            Ok(list) => list,
2747        };
2748        k9::snapshot!(
2749            list,
2750            r#"
2751Some(
2752    MailboxList(
2753        [
2754            Mailbox {
2755                name: Some(
2756                    "Keith Moore",
2757                ),
2758                address: AddrSpec {
2759                    local_part: "moore",
2760                    domain: "cs.utk.edu",
2761                },
2762            },
2763        ],
2764    ),
2765)
2766"#
2767        );
2768
2769        let list = match msg.headers().to() {
2770            Err(err) => panic!("Doh.\n{err:#}"),
2771            Ok(list) => list,
2772        };
2773        k9::snapshot!(
2774            list,
2775            r#"
2776Some(
2777    AddressList(
2778        [
2779            Mailbox(
2780                Mailbox {
2781                    name: Some(
2782                        "Keld Jørn Simonsen",
2783                    ),
2784                    address: AddrSpec {
2785                        local_part: "keld",
2786                        domain: "dkuug.dk",
2787                    },
2788                },
2789            ),
2790        ],
2791    ),
2792)
2793"#
2794        );
2795
2796        let list = match msg.headers().cc() {
2797            Err(err) => panic!("Doh.\n{err:#}"),
2798            Ok(list) => list,
2799        };
2800        k9::snapshot!(
2801            list,
2802            r#"
2803Some(
2804    AddressList(
2805        [
2806            Mailbox(
2807                Mailbox {
2808                    name: Some(
2809                        "André Pirard",
2810                    ),
2811                    address: AddrSpec {
2812                        local_part: "PIRARD",
2813                        domain: "vm1.ulg.ac.be",
2814                    },
2815                },
2816            ),
2817        ],
2818    ),
2819)
2820"#
2821        );
2822        let list = match msg.headers().subject() {
2823            Err(err) => panic!("Doh.\n{err:#}"),
2824            Ok(list) => list,
2825        };
2826        k9::snapshot!(
2827            list,
2828            r#"
2829Some(
2830    "Hello If you can read this you understand the example.",
2831)
2832"#
2833        );
2834
2835        k9::snapshot!(
2836            msg.rebuild(None).unwrap().to_message_string(),
2837            r#"
2838Content-Type: text/plain;\r
2839\tcharset="us-ascii"\r
2840Content-Transfer-Encoding: quoted-printable\r
2841From: "Keith Moore" <moore@cs.utk.edu>\r
2842To: =?UTF-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r
2843Cc: =?UTF-8?q?Andr=C3=A9_Pirard?= <PIRARD@vm1.ulg.ac.be>\r
2844Subject: Hello If you can read this you understand the example.\r
2845\r
2846=0A\r
2847
2848"#
2849        );
2850    }
2851
2852    #[test]
2853    fn group_addresses() {
2854        let message = concat!(
2855            "To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;\n",
2856            "Cc: Undisclosed recipients:;\n",
2857            "\n\n\n"
2858        );
2859        let msg = MimePart::parse(message).unwrap();
2860        let list = match msg.headers().to() {
2861            Err(err) => panic!("Doh.\n{err:#}"),
2862            Ok(list) => list.unwrap(),
2863        };
2864
2865        k9::snapshot!(
2866            list.encode_value(),
2867            r#"
2868A Group:"Ed Jones" <c@a.test>,\r
2869\t<joe@where.test>,\r
2870\tJohn <jdoe@one.test>;
2871"#
2872        );
2873
2874        let round_trip = Header::new("To", list.clone());
2875        k9::assert_equal!(list, round_trip.as_address_list().unwrap());
2876
2877        k9::snapshot!(
2878            list,
2879            r#"
2880AddressList(
2881    [
2882        Group {
2883            name: "A Group",
2884            entries: MailboxList(
2885                [
2886                    Mailbox {
2887                        name: Some(
2888                            "Ed Jones",
2889                        ),
2890                        address: AddrSpec {
2891                            local_part: "c",
2892                            domain: "a.test",
2893                        },
2894                    },
2895                    Mailbox {
2896                        name: None,
2897                        address: AddrSpec {
2898                            local_part: "joe",
2899                            domain: "where.test",
2900                        },
2901                    },
2902                    Mailbox {
2903                        name: Some(
2904                            "John",
2905                        ),
2906                        address: AddrSpec {
2907                            local_part: "jdoe",
2908                            domain: "one.test",
2909                        },
2910                    },
2911                ],
2912            ),
2913        },
2914    ],
2915)
2916"#
2917        );
2918
2919        let list = match msg.headers().cc() {
2920            Err(err) => panic!("Doh.\n{err:#}"),
2921            Ok(list) => list,
2922        };
2923        k9::snapshot!(
2924            list,
2925            r#"
2926Some(
2927    AddressList(
2928        [
2929            Group {
2930                name: "Undisclosed recipients",
2931                entries: MailboxList(
2932                    [],
2933                ),
2934            },
2935        ],
2936    ),
2937)
2938"#
2939        );
2940    }
2941
2942    #[test]
2943    fn message_id() {
2944        let message = concat!(
2945            "Message-Id: <foo@example.com>\n",
2946            "References: <a@example.com> <b@example.com>\n",
2947            "  <\"legacy\"@example.com>\n",
2948            "  <literal@[127.0.0.1]>\n",
2949            "\n\n\n"
2950        );
2951        let msg = MimePart::parse(message).unwrap();
2952        let list = match msg.headers().message_id() {
2953            Err(err) => panic!("Doh.\n{err:#}"),
2954            Ok(list) => list,
2955        };
2956        k9::snapshot!(
2957            list,
2958            r#"
2959Some(
2960    MessageID(
2961        "foo@example.com",
2962    ),
2963)
2964"#
2965        );
2966
2967        let list = match msg.headers().references() {
2968            Err(err) => panic!("Doh.\n{err:#}"),
2969            Ok(list) => list,
2970        };
2971        k9::snapshot!(
2972            list,
2973            r#"
2974Some(
2975    [
2976        MessageID(
2977            "a@example.com",
2978        ),
2979        MessageID(
2980            "b@example.com",
2981        ),
2982        MessageID(
2983            "legacy@example.com",
2984        ),
2985        MessageID(
2986            "literal@[127.0.0.1]",
2987        ),
2988    ],
2989)
2990"#
2991        );
2992    }
2993
2994    #[test]
2995    fn content_type() {
2996        let message = "Content-Type: text/plain\n\n\n\n";
2997        let msg = MimePart::parse(message).unwrap();
2998        let params = match msg.headers().content_type() {
2999            Err(err) => panic!("Doh.\n{err:#}"),
3000            Ok(params) => params,
3001        };
3002        k9::snapshot!(
3003            params,
3004            r#"
3005Some(
3006    MimeParameters {
3007        value: "text/plain",
3008        parameters: [],
3009    },
3010)
3011"#
3012        );
3013
3014        let message = "Content-Type: text/plain; charset=us-ascii\n\n\n\n";
3015        let msg = MimePart::parse(message).unwrap();
3016        let params = match msg.headers().content_type() {
3017            Err(err) => panic!("Doh.\n{err:#}"),
3018            Ok(params) => params.unwrap(),
3019        };
3020
3021        k9::snapshot!(
3022            params.get("charset"),
3023            r#"
3024Some(
3025    "us-ascii",
3026)
3027"#
3028        );
3029        k9::snapshot!(
3030            params,
3031            r#"
3032MimeParameters {
3033    value: "text/plain",
3034    parameters: [
3035        MimeParameter {
3036            name: "charset",
3037            section: None,
3038            mime_charset: None,
3039            mime_language: None,
3040            encoding: None,
3041            value: "us-ascii",
3042        },
3043    ],
3044}
3045"#
3046        );
3047
3048        let message = "Content-Type: text/plain; charset=\"us-ascii\"\n\n\n\n";
3049        let msg = MimePart::parse(message).unwrap();
3050        let params = match msg.headers().content_type() {
3051            Err(err) => panic!("Doh.\n{err:#}"),
3052            Ok(params) => params,
3053        };
3054        k9::snapshot!(
3055            params,
3056            r#"
3057Some(
3058    MimeParameters {
3059        value: "text/plain",
3060        parameters: [
3061            MimeParameter {
3062                name: "charset",
3063                section: None,
3064                mime_charset: None,
3065                mime_language: None,
3066                encoding: None,
3067                value: "us-ascii",
3068            },
3069        ],
3070    },
3071)
3072"#
3073        );
3074    }
3075
3076    #[test]
3077    fn content_type_rfc2231() {
3078        // This example is taken from the errata for rfc2231.
3079        // <https://www.rfc-editor.org/errata/eid590>
3080        let message = concat!(
3081            "Content-Type: application/x-stuff;\n",
3082            "\ttitle*0*=us-ascii'en'This%20is%20even%20more%20;\n",
3083            "\ttitle*1*=%2A%2A%2Afun%2A%2A%2A%20;\n",
3084            "\ttitle*2=\"isn't it!\"\n",
3085            "\n\n\n"
3086        );
3087        let msg = MimePart::parse(message).unwrap();
3088        let mut params = match msg.headers().content_type() {
3089            Err(err) => panic!("Doh.\n{err:#}"),
3090            Ok(params) => params.unwrap(),
3091        };
3092
3093        let original_title = params.get("title");
3094        k9::snapshot!(
3095            &original_title,
3096            r#"
3097Some(
3098    "This is even more ***fun*** isn't it!",
3099)
3100"#
3101        );
3102
3103        k9::snapshot!(
3104            &params,
3105            r#"
3106MimeParameters {
3107    value: "application/x-stuff",
3108    parameters: [
3109        MimeParameter {
3110            name: "title",
3111            section: Some(
3112                0,
3113            ),
3114            mime_charset: Some(
3115                "us-ascii",
3116            ),
3117            mime_language: Some(
3118                "en",
3119            ),
3120            encoding: Rfc2231,
3121            value: "This%20is%20even%20more%20",
3122        },
3123        MimeParameter {
3124            name: "title",
3125            section: Some(
3126                1,
3127            ),
3128            mime_charset: None,
3129            mime_language: None,
3130            encoding: Rfc2231,
3131            value: "%2A%2A%2Afun%2A%2A%2A%20",
3132        },
3133        MimeParameter {
3134            name: "title",
3135            section: Some(
3136                2,
3137            ),
3138            mime_charset: None,
3139            mime_language: None,
3140            encoding: None,
3141            value: "isn't it!",
3142        },
3143    ],
3144}
3145"#
3146        );
3147
3148        k9::snapshot!(
3149            params.encode_value(),
3150            r#"
3151application/x-stuff;\r
3152\ttitle="This is even more ***fun*** isn't it!"
3153"#
3154        );
3155
3156        params.set("foo", "bar 💩");
3157
3158        params.set(
3159            "long",
3160            "this is some text that should wrap because \
3161                it should be a good bit longer than our target maximum \
3162                length for this sort of thing, and hopefully we see at \
3163                least three lines produced as a result of setting \
3164                this value in this way",
3165        );
3166
3167        params.set(
3168            "longernnamethananyoneshouldreallyuse",
3169            "this is some text that should wrap because \
3170                it should be a good bit longer than our target maximum \
3171                length for this sort of thing, and hopefully we see at \
3172                least three lines produced as a result of setting \
3173                this value in this way",
3174        );
3175
3176        k9::snapshot!(
3177            params.encode_value(),
3178            r#"
3179application/x-stuff;\r
3180\tfoo*=UTF-8''bar%20%F0%9F%92%A9;\r
3181\tlong*0="this is some text that should wrap because it should be a good bi";\r
3182\tlong*1="t longer than our target maximum length for this sort of thing, a";\r
3183\tlong*2="nd hopefully we see at least three lines produced as a result of ";\r
3184\tlong*3="setting this value in this way";\r
3185\tlongernnamethananyoneshouldreallyuse*0="this is some text that should wra";\r
3186\tlongernnamethananyoneshouldreallyuse*1="p because it should be a good bit";\r
3187\tlongernnamethananyoneshouldreallyuse*2=" longer than our target maximum l";\r
3188\tlongernnamethananyoneshouldreallyuse*3="ength for this sort of thing, and";\r
3189\tlongernnamethananyoneshouldreallyuse*4=" hopefully we see at least three ";\r
3190\tlongernnamethananyoneshouldreallyuse*5="lines produced as a result of set";\r
3191\tlongernnamethananyoneshouldreallyuse*6="ting this value in this way";\r
3192\ttitle="This is even more ***fun*** isn't it!"
3193"#
3194        );
3195    }
3196
3197    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.2>
3198    #[test]
3199    fn authentication_results_b_2() {
3200        let ar = Header::with_name_value("Authentication-Results", "example.org 1; none");
3201        let ar = ar.as_authentication_results().unwrap();
3202        k9::snapshot!(
3203            &ar,
3204            r#"
3205AuthenticationResults {
3206    serv_id: "example.org",
3207    version: Some(
3208        1,
3209    ),
3210    results: [],
3211}
3212"#
3213        );
3214
3215        k9::snapshot!(ar.encode_value(), "example.org 1; none");
3216    }
3217
3218    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.3>
3219    #[test]
3220    fn authentication_results_b_3() {
3221        let ar = Header::with_name_value(
3222            "Authentication-Results",
3223            "example.com; spf=pass smtp.mailfrom=example.net",
3224        );
3225        k9::snapshot!(
3226            ar.as_authentication_results(),
3227            r#"
3228Ok(
3229    AuthenticationResults {
3230        serv_id: "example.com",
3231        version: None,
3232        results: [
3233            AuthenticationResult {
3234                method: "spf",
3235                method_version: None,
3236                result: "pass",
3237                reason: None,
3238                props: {
3239                    "smtp.mailfrom": "example.net",
3240                },
3241            },
3242        ],
3243    },
3244)
3245"#
3246        );
3247    }
3248
3249    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.4>
3250    #[test]
3251    fn authentication_results_b_4() {
3252        let ar = Header::with_name_value(
3253            "Authentication-Results",
3254            concat!(
3255                "example.com;\n",
3256                "\tauth=pass (cram-md5) smtp.auth=sender@example.net;\n",
3257                "\tspf=pass smtp.mailfrom=example.net"
3258            ),
3259        );
3260        k9::snapshot!(
3261            ar.as_authentication_results(),
3262            r#"
3263Ok(
3264    AuthenticationResults {
3265        serv_id: "example.com",
3266        version: None,
3267        results: [
3268            AuthenticationResult {
3269                method: "auth",
3270                method_version: None,
3271                result: "pass",
3272                reason: None,
3273                props: {
3274                    "smtp.auth": "sender@example.net",
3275                },
3276            },
3277            AuthenticationResult {
3278                method: "spf",
3279                method_version: None,
3280                result: "pass",
3281                reason: None,
3282                props: {
3283                    "smtp.mailfrom": "example.net",
3284                },
3285            },
3286        ],
3287    },
3288)
3289"#
3290        );
3291
3292        let ar = Header::with_name_value(
3293            "Authentication-Results",
3294            "example.com; iprev=pass\n\tpolicy.iprev=192.0.2.200",
3295        );
3296        k9::snapshot!(
3297            ar.as_authentication_results(),
3298            r#"
3299Ok(
3300    AuthenticationResults {
3301        serv_id: "example.com",
3302        version: None,
3303        results: [
3304            AuthenticationResult {
3305                method: "iprev",
3306                method_version: None,
3307                result: "pass",
3308                reason: None,
3309                props: {
3310                    "policy.iprev": "192.0.2.200",
3311                },
3312            },
3313        ],
3314    },
3315)
3316"#
3317        );
3318    }
3319
3320    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.5>
3321    #[test]
3322    fn authentication_results_b_5() {
3323        let ar = Header::with_name_value(
3324            "Authentication-Results",
3325            "example.com;\n\tdkim=pass (good signature) header.d=example.com",
3326        );
3327        k9::snapshot!(
3328            ar.as_authentication_results(),
3329            r#"
3330Ok(
3331    AuthenticationResults {
3332        serv_id: "example.com",
3333        version: None,
3334        results: [
3335            AuthenticationResult {
3336                method: "dkim",
3337                method_version: None,
3338                result: "pass",
3339                reason: None,
3340                props: {
3341                    "header.d": "example.com",
3342                },
3343            },
3344        ],
3345    },
3346)
3347"#
3348        );
3349
3350        let ar = Header::with_name_value(
3351            "Authentication-Results",
3352            "example.com;\n\tauth=pass (cram-md5) smtp.auth=sender@example.com;\n\tspf=fail smtp.mailfrom=example.com"
3353        );
3354        let ar = ar.as_authentication_results().unwrap();
3355        k9::snapshot!(
3356            &ar,
3357            r#"
3358AuthenticationResults {
3359    serv_id: "example.com",
3360    version: None,
3361    results: [
3362        AuthenticationResult {
3363            method: "auth",
3364            method_version: None,
3365            result: "pass",
3366            reason: None,
3367            props: {
3368                "smtp.auth": "sender@example.com",
3369            },
3370        },
3371        AuthenticationResult {
3372            method: "spf",
3373            method_version: None,
3374            result: "fail",
3375            reason: None,
3376            props: {
3377                "smtp.mailfrom": "example.com",
3378            },
3379        },
3380    ],
3381}
3382"#
3383        );
3384
3385        k9::snapshot!(
3386            ar.encode_value(),
3387            r#"
3388example.com;\r
3389\tauth=pass\r
3390\tsmtp.auth=sender@example.com;\r
3391\tspf=fail\r
3392\tsmtp.mailfrom=example.com
3393"#
3394        );
3395    }
3396
3397    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.6>
3398    #[test]
3399    fn authentication_results_b_6() {
3400        let ar = Header::with_name_value(
3401            "Authentication-Results",
3402            concat!(
3403                "example.com;\n",
3404                "\tdkim=pass reason=\"good signature\"\n",
3405                "\theader.i=@mail-router.example.net;\n",
3406                "\tdkim=fail reason=\"bad signature\"\n",
3407                "\theader.i=@newyork.example.com"
3408            ),
3409        );
3410        let ar = match ar.as_authentication_results() {
3411            Err(err) => panic!("\n{err}"),
3412            Ok(ar) => ar,
3413        };
3414
3415        k9::snapshot!(
3416            &ar,
3417            r#"
3418AuthenticationResults {
3419    serv_id: "example.com",
3420    version: None,
3421    results: [
3422        AuthenticationResult {
3423            method: "dkim",
3424            method_version: None,
3425            result: "pass",
3426            reason: Some(
3427                "good signature",
3428            ),
3429            props: {
3430                "header.i": "@mail-router.example.net",
3431            },
3432        },
3433        AuthenticationResult {
3434            method: "dkim",
3435            method_version: None,
3436            result: "fail",
3437            reason: Some(
3438                "bad signature",
3439            ),
3440            props: {
3441                "header.i": "@newyork.example.com",
3442            },
3443        },
3444    ],
3445}
3446"#
3447        );
3448
3449        k9::snapshot!(
3450            ar.encode_value(),
3451            r#"
3452example.com;\r
3453\tdkim=pass reason="good signature"\r
3454\theader.i=@mail-router.example.net;\r
3455\tdkim=fail reason="bad signature"\r
3456\theader.i=@newyork.example.com
3457"#
3458        );
3459
3460        let ar = Header::with_name_value(
3461            "Authentication-Results",
3462            concat!(
3463                "example.net;\n",
3464                "\tdkim=pass (good signature) header.i=@newyork.example.com"
3465            ),
3466        );
3467        let ar = match ar.as_authentication_results() {
3468            Err(err) => panic!("\n{err}"),
3469            Ok(ar) => ar,
3470        };
3471
3472        k9::snapshot!(
3473            &ar,
3474            r#"
3475AuthenticationResults {
3476    serv_id: "example.net",
3477    version: None,
3478    results: [
3479        AuthenticationResult {
3480            method: "dkim",
3481            method_version: None,
3482            result: "pass",
3483            reason: None,
3484            props: {
3485                "header.i": "@newyork.example.com",
3486            },
3487        },
3488    ],
3489}
3490"#
3491        );
3492
3493        k9::snapshot!(
3494            ar.encode_value(),
3495            r#"
3496example.net;\r
3497\tdkim=pass\r
3498\theader.i=@newyork.example.com
3499"#
3500        );
3501    }
3502
3503    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.7>
3504    #[test]
3505    fn authentication_results_b_7() {
3506        let ar = Header::with_name_value(
3507            "Authentication-Results",
3508            concat!(
3509                "foo.example.net (foobar) 1 (baz);\n",
3510                "\tdkim (Because I like it) / 1 (One yay) = (wait for it) fail\n",
3511                "\tpolicy (A dot can go here) . (like that) expired\n",
3512                "\t(this surprised me) = (as I wasn't expecting it) 1362471462"
3513            ),
3514        );
3515        let ar = match ar.as_authentication_results() {
3516            Err(err) => panic!("\n{err}"),
3517            Ok(ar) => ar,
3518        };
3519
3520        k9::snapshot!(
3521            &ar,
3522            r#"
3523AuthenticationResults {
3524    serv_id: "foo.example.net",
3525    version: Some(
3526        1,
3527    ),
3528    results: [
3529        AuthenticationResult {
3530            method: "dkim",
3531            method_version: Some(
3532                1,
3533            ),
3534            result: "fail",
3535            reason: None,
3536            props: {
3537                "policy.expired": "1362471462",
3538            },
3539        },
3540    ],
3541}
3542"#
3543        );
3544
3545        k9::snapshot!(
3546            ar.encode_value(),
3547            r#"
3548foo.example.net 1;\r
3549\tdkim/1=fail\r
3550\tpolicy.expired=1362471462
3551"#
3552        );
3553    }
3554
3555    #[test]
3556    fn arc_authentication_results_1() {
3557        let ar = Header::with_name_value(
3558            "ARC-Authentication-Results",
3559            "i=3; clochette.example.org; spf=fail
3560    smtp.from=jqd@d1.example; dkim=fail (512-bit key)
3561    header.i=@d1.example; dmarc=fail; arc=pass (as.2.gmail.example=pass,
3562    ams.2.gmail.example=pass, as.1.lists.example.org=pass,
3563    ams.1.lists.example.org=fail (message has been altered))",
3564        );
3565        let ar = match ar.as_arc_authentication_results() {
3566            Err(err) => panic!("\n{err}"),
3567            Ok(ar) => ar,
3568        };
3569
3570        k9::snapshot!(
3571            &ar,
3572            r#"
3573ARCAuthenticationResults {
3574    instance: 3,
3575    serv_id: "clochette.example.org",
3576    version: None,
3577    results: [
3578        AuthenticationResult {
3579            method: "spf",
3580            method_version: None,
3581            result: "fail",
3582            reason: None,
3583            props: {
3584                "smtp.from": "jqd@d1.example",
3585            },
3586        },
3587        AuthenticationResult {
3588            method: "dkim",
3589            method_version: None,
3590            result: "fail",
3591            reason: None,
3592            props: {
3593                "header.i": "@d1.example",
3594            },
3595        },
3596        AuthenticationResult {
3597            method: "dmarc",
3598            method_version: None,
3599            result: "fail",
3600            reason: None,
3601            props: {},
3602        },
3603        AuthenticationResult {
3604            method: "arc",
3605            method_version: None,
3606            result: "pass",
3607            reason: None,
3608            props: {},
3609        },
3610    ],
3611}
3612"#
3613        );
3614    }
3615}