mailparsing/
rfc5322_parser.rs

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