mailparsing/
rfc5322_parser.rs

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