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