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_ascii_whitespace() && !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    k9::assert_equal!(quote_string("hello\n there"), "\"hello\n there\"");
2152}
2153
2154impl EncodeHeaderValue for Mailbox {
2155    fn encode_value(&self) -> SharedString<'static> {
2156        match &self.name {
2157            Some(name) => {
2158                let mut value = if name.is_ascii() {
2159                    quote_string(name)
2160                } else {
2161                    qp_encode(name)
2162                };
2163
2164                value.push_str(" <");
2165                value.push_str(&self.address.encode_value());
2166                value.push('>');
2167                value.into()
2168            }
2169            None => format!("<{}>", self.address.encode_value()).into(),
2170        }
2171    }
2172}
2173
2174impl EncodeHeaderValue for MailboxList {
2175    fn encode_value(&self) -> SharedString<'static> {
2176        let mut result = String::new();
2177        for mailbox in &self.0 {
2178            if !result.is_empty() {
2179                result.push_str(",\r\n\t");
2180            }
2181            result.push_str(&mailbox.encode_value());
2182        }
2183        result.into()
2184    }
2185}
2186
2187impl EncodeHeaderValue for Address {
2188    fn encode_value(&self) -> SharedString<'static> {
2189        match self {
2190            Self::Mailbox(mbox) => mbox.encode_value(),
2191            Self::Group { name, entries } => {
2192                let mut result = format!("{name}:");
2193                result += &entries.encode_value();
2194                result.push(';');
2195                result.into()
2196            }
2197        }
2198    }
2199}
2200
2201impl EncodeHeaderValue for AddressList {
2202    fn encode_value(&self) -> SharedString<'static> {
2203        let mut result = String::new();
2204        for address in &self.0 {
2205            if !result.is_empty() {
2206                result.push_str(",\r\n\t");
2207            }
2208            result.push_str(&address.encode_value());
2209        }
2210        result.into()
2211    }
2212}
2213
2214#[cfg(test)]
2215mod test {
2216    use super::*;
2217    use crate::{Header, MessageConformance, MimePart};
2218
2219    #[test]
2220    fn mailbox_encodes_at() {
2221        let mbox = Mailbox {
2222            name: Some("foo@bar.com".to_string()),
2223            address: AddrSpec {
2224                local_part: "foo".to_string(),
2225                domain: "bar.com".to_string(),
2226            },
2227        };
2228        assert_eq!(mbox.encode_value(), "\"foo@bar.com\" <foo@bar.com>");
2229    }
2230
2231    #[test]
2232    fn mailbox_list_singular() {
2233        let message = concat!(
2234            "From:  Someone (hello) <someone@example.com>, other@example.com,\n",
2235            "  \"John \\\"Smith\\\"\" (comment) \"More Quotes\" (more comment) <someone(another comment)@crazy.example.com(woot)>\n",
2236            "\n",
2237            "I am the body"
2238        );
2239        let msg = MimePart::parse(message).unwrap();
2240        let list = match msg.headers().from() {
2241            Err(err) => panic!("Doh.\n{err:#}"),
2242            Ok(list) => list,
2243        };
2244
2245        k9::snapshot!(
2246            list,
2247            r#"
2248Some(
2249    MailboxList(
2250        [
2251            Mailbox {
2252                name: Some(
2253                    "Someone",
2254                ),
2255                address: AddrSpec {
2256                    local_part: "someone",
2257                    domain: "example.com",
2258                },
2259            },
2260            Mailbox {
2261                name: None,
2262                address: AddrSpec {
2263                    local_part: "other",
2264                    domain: "example.com",
2265                },
2266            },
2267            Mailbox {
2268                name: Some(
2269                    "John "Smith" More Quotes",
2270                ),
2271                address: AddrSpec {
2272                    local_part: "someone",
2273                    domain: "crazy.example.com",
2274                },
2275            },
2276        ],
2277    ),
2278)
2279"#
2280        );
2281    }
2282
2283    #[test]
2284    fn docomo_non_compliant_localpart() {
2285        let message = "Sender: hello..there@docomo.ne.jp\n\n\n";
2286        let msg = MimePart::parse(message).unwrap();
2287        let err = msg.headers().sender().unwrap_err();
2288        k9::snapshot!(
2289            err,
2290            r#"
2291InvalidHeaderValueDuringGet {
2292    header_name: "Sender",
2293    error: HeaderParse(
2294        "0: at line 1:
2295hello..there@docomo.ne.jp
2296     ^___________________
2297expected '@', found .
2298
22991: at line 1, in addr_spec:
2300hello..there@docomo.ne.jp
2301^________________________
2302
23032: at line 1, in mailbox:
2304hello..there@docomo.ne.jp
2305^________________________
2306
2307",
2308    ),
2309}
2310"#
2311        );
2312    }
2313
2314    #[test]
2315    fn sender() {
2316        let message = "Sender: someone@[127.0.0.1]\n\n\n";
2317        let msg = MimePart::parse(message).unwrap();
2318        let list = match msg.headers().sender() {
2319            Err(err) => panic!("Doh.\n{err:#}"),
2320            Ok(list) => list,
2321        };
2322        k9::snapshot!(
2323            list,
2324            r#"
2325Some(
2326    Mailbox {
2327        name: None,
2328        address: AddrSpec {
2329            local_part: "someone",
2330            domain: "[127.0.0.1]",
2331        },
2332    },
2333)
2334"#
2335        );
2336    }
2337
2338    #[test]
2339    fn domain_literal() {
2340        let message = "From: someone@[127.0.0.1]\n\n\n";
2341        let msg = MimePart::parse(message).unwrap();
2342        let list = match msg.headers().from() {
2343            Err(err) => panic!("Doh.\n{err:#}"),
2344            Ok(list) => list,
2345        };
2346        k9::snapshot!(
2347            list,
2348            r#"
2349Some(
2350    MailboxList(
2351        [
2352            Mailbox {
2353                name: None,
2354                address: AddrSpec {
2355                    local_part: "someone",
2356                    domain: "[127.0.0.1]",
2357                },
2358            },
2359        ],
2360    ),
2361)
2362"#
2363        );
2364    }
2365
2366    #[test]
2367    fn rfc6532() {
2368        let message = concat!(
2369            "From: Keith Moore <moore@cs.utk.edu>\n",
2370            "To: Keld Jørn Simonsen <keld@dkuug.dk>\n",
2371            "CC: André Pirard <PIRARD@vm1.ulg.ac.be>\n",
2372            "Subject: Hello André\n",
2373            "\n\n"
2374        );
2375        let msg = MimePart::parse(message).unwrap();
2376        let list = match msg.headers().from() {
2377            Err(err) => panic!("Doh.\n{err:#}"),
2378            Ok(list) => list,
2379        };
2380        k9::snapshot!(
2381            list,
2382            r#"
2383Some(
2384    MailboxList(
2385        [
2386            Mailbox {
2387                name: Some(
2388                    "Keith Moore",
2389                ),
2390                address: AddrSpec {
2391                    local_part: "moore",
2392                    domain: "cs.utk.edu",
2393                },
2394            },
2395        ],
2396    ),
2397)
2398"#
2399        );
2400
2401        let list = match msg.headers().to() {
2402            Err(err) => panic!("Doh.\n{err:#}"),
2403            Ok(list) => list,
2404        };
2405        k9::snapshot!(
2406            list,
2407            r#"
2408Some(
2409    AddressList(
2410        [
2411            Mailbox(
2412                Mailbox {
2413                    name: Some(
2414                        "Keld Jørn Simonsen",
2415                    ),
2416                    address: AddrSpec {
2417                        local_part: "keld",
2418                        domain: "dkuug.dk",
2419                    },
2420                },
2421            ),
2422        ],
2423    ),
2424)
2425"#
2426        );
2427
2428        let list = match msg.headers().cc() {
2429            Err(err) => panic!("Doh.\n{err:#}"),
2430            Ok(list) => list,
2431        };
2432        k9::snapshot!(
2433            list,
2434            r#"
2435Some(
2436    AddressList(
2437        [
2438            Mailbox(
2439                Mailbox {
2440                    name: Some(
2441                        "André Pirard",
2442                    ),
2443                    address: AddrSpec {
2444                        local_part: "PIRARD",
2445                        domain: "vm1.ulg.ac.be",
2446                    },
2447                },
2448            ),
2449        ],
2450    ),
2451)
2452"#
2453        );
2454        let list = match msg.headers().subject() {
2455            Err(err) => panic!("Doh.\n{err:#}"),
2456            Ok(list) => list,
2457        };
2458        k9::snapshot!(
2459            list,
2460            r#"
2461Some(
2462    "Hello André",
2463)
2464"#
2465        );
2466    }
2467
2468    #[test]
2469    fn rfc2047_bogus() {
2470        let message = concat!(
2471            "From: =?US-OSCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2472            "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2473            "CC: =?ISO-8859-1?Q?Andr=E?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2474            "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?=\n",
2475            "  =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2476            "\n\n"
2477        );
2478        let msg = MimePart::parse(message).unwrap();
2479
2480        // Invalid charset causes encoded_word to fail and we will instead match
2481        // obs_utext and return it as it was
2482        k9::assert_equal!(
2483            msg.headers().from().unwrap().unwrap().0[0]
2484                .name
2485                .as_ref()
2486                .unwrap(),
2487            "=?US-OSCII?Q?Keith_Moore?="
2488        );
2489
2490        match &msg.headers().cc().unwrap().unwrap().0[0] {
2491            Address::Mailbox(mbox) => {
2492                // 'Andr=E9?=' is in the non-bogus example below, but above we
2493                // broke it as 'Andr=E?=', and instead of triggering a qp decode
2494                // error, it is passed through here as-is
2495                k9::assert_equal!(mbox.name.as_ref().unwrap(), "Andr=E Pirard");
2496            }
2497            wat => panic!("should not have {wat:?}"),
2498        }
2499
2500        // The invalid base64 (an I was replaced by an !) is interpreted as obs_utext
2501        // and passed through to us
2502        k9::assert_equal!(
2503            msg.headers().subject().unwrap().unwrap(),
2504            "Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?= u understand the example."
2505        );
2506    }
2507
2508    #[test]
2509    fn attachment_filename_mess_totally_bogus() {
2510        let message = concat!("Content-Disposition: attachment; filename=@\n", "\n\n");
2511        let msg = MimePart::parse(message).unwrap();
2512        eprintln!("{msg:#?}");
2513
2514        assert!(msg
2515            .conformance()
2516            .contains(MessageConformance::INVALID_MIME_HEADERS));
2517        msg.headers().content_disposition().unwrap_err();
2518
2519        // There is no Content-Disposition in the rebuilt message, because
2520        // there was no valid Content-Disposition in what we parsed
2521        let rebuilt = msg.rebuild(None).unwrap();
2522        k9::assert_equal!(rebuilt.headers().content_disposition(), Ok(None));
2523    }
2524
2525    #[test]
2526    fn attachment_filename_mess_aberrant() {
2527        let message = concat!(
2528            "Content-Disposition: attachment; filename= =?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\n",
2529            "\n\n"
2530        );
2531        let msg = MimePart::parse(message).unwrap();
2532
2533        let cd = msg.headers().content_disposition().unwrap().unwrap();
2534        k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
2535
2536        let encoded = cd.encode_value();
2537        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?=");
2538    }
2539
2540    #[test]
2541    fn attachment_filename_mess_gmail() {
2542        let message = concat!(
2543            "Content-Disposition: attachment; filename=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
2544            "Content-Type: text/plain;\n",
2545            "   name=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
2546            "\n\n"
2547        );
2548        let msg = MimePart::parse(message).unwrap();
2549
2550        let cd = msg.headers().content_disposition().unwrap().unwrap();
2551        k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
2552        let encoded = cd.encode_value();
2553        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?=\"");
2554
2555        let ct = msg.headers().content_type().unwrap().unwrap();
2556        k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付");
2557    }
2558
2559    #[test]
2560    fn attachment_filename_mess_fastmail() {
2561        let message = concat!(
2562            "Content-Disposition: attachment;\n",
2563            "  filename*0*=utf-8''%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E6%B7%BB%E4%BB%98;\n",
2564            "  filename*1*=.txt\n",
2565            "Content-Type: text/plain;\n",
2566            "   name=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=\"\n",
2567            "   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",
2568            "\n\n"
2569        );
2570        let msg = MimePart::parse(message).unwrap();
2571
2572        let cd = msg.headers().content_disposition().unwrap().unwrap();
2573        k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付.txt");
2574
2575        let ct = msg.headers().content_type().unwrap().unwrap();
2576        eprintln!("{ct:#?}");
2577        k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付.txt");
2578        k9::assert_equal!(
2579            ct.get("x-name").unwrap(),
2580            "=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=bork"
2581        );
2582    }
2583
2584    #[test]
2585    fn rfc2047() {
2586        let message = concat!(
2587            "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2588            "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2589            "CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2590            "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n",
2591            "  =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2592            "\n\n"
2593        );
2594        let msg = MimePart::parse(message).unwrap();
2595        let list = match msg.headers().from() {
2596            Err(err) => panic!("Doh.\n{err:#}"),
2597            Ok(list) => list,
2598        };
2599        k9::snapshot!(
2600            list,
2601            r#"
2602Some(
2603    MailboxList(
2604        [
2605            Mailbox {
2606                name: Some(
2607                    "Keith Moore",
2608                ),
2609                address: AddrSpec {
2610                    local_part: "moore",
2611                    domain: "cs.utk.edu",
2612                },
2613            },
2614        ],
2615    ),
2616)
2617"#
2618        );
2619
2620        let list = match msg.headers().to() {
2621            Err(err) => panic!("Doh.\n{err:#}"),
2622            Ok(list) => list,
2623        };
2624        k9::snapshot!(
2625            list,
2626            r#"
2627Some(
2628    AddressList(
2629        [
2630            Mailbox(
2631                Mailbox {
2632                    name: Some(
2633                        "Keld Jørn Simonsen",
2634                    ),
2635                    address: AddrSpec {
2636                        local_part: "keld",
2637                        domain: "dkuug.dk",
2638                    },
2639                },
2640            ),
2641        ],
2642    ),
2643)
2644"#
2645        );
2646
2647        let list = match msg.headers().cc() {
2648            Err(err) => panic!("Doh.\n{err:#}"),
2649            Ok(list) => list,
2650        };
2651        k9::snapshot!(
2652            list,
2653            r#"
2654Some(
2655    AddressList(
2656        [
2657            Mailbox(
2658                Mailbox {
2659                    name: Some(
2660                        "André Pirard",
2661                    ),
2662                    address: AddrSpec {
2663                        local_part: "PIRARD",
2664                        domain: "vm1.ulg.ac.be",
2665                    },
2666                },
2667            ),
2668        ],
2669    ),
2670)
2671"#
2672        );
2673        let list = match msg.headers().subject() {
2674            Err(err) => panic!("Doh.\n{err:#}"),
2675            Ok(list) => list,
2676        };
2677        k9::snapshot!(
2678            list,
2679            r#"
2680Some(
2681    "Hello If you can read this you understand the example.",
2682)
2683"#
2684        );
2685
2686        k9::snapshot!(
2687            msg.rebuild(None).unwrap().to_message_string(),
2688            r#"
2689Content-Type: text/plain;\r
2690\tcharset="us-ascii"\r
2691Content-Transfer-Encoding: quoted-printable\r
2692From: "Keith Moore" <moore@cs.utk.edu>\r
2693To: =?UTF-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r
2694Cc: =?UTF-8?q?Andr=C3=A9_Pirard?= <PIRARD@vm1.ulg.ac.be>\r
2695Subject: Hello If you can read this you understand the example.\r
2696\r
2697=0A\r
2698
2699"#
2700        );
2701    }
2702
2703    #[test]
2704    fn group_addresses() {
2705        let message = concat!(
2706            "To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;\n",
2707            "Cc: Undisclosed recipients:;\n",
2708            "\n\n\n"
2709        );
2710        let msg = MimePart::parse(message).unwrap();
2711        let list = match msg.headers().to() {
2712            Err(err) => panic!("Doh.\n{err:#}"),
2713            Ok(list) => list.unwrap(),
2714        };
2715
2716        k9::snapshot!(
2717            list.encode_value(),
2718            r#"
2719A Group:"Ed Jones" <c@a.test>,\r
2720\t<joe@where.test>,\r
2721\tJohn <jdoe@one.test>;
2722"#
2723        );
2724
2725        let round_trip = Header::new("To", list.clone());
2726        k9::assert_equal!(list, round_trip.as_address_list().unwrap());
2727
2728        k9::snapshot!(
2729            list,
2730            r#"
2731AddressList(
2732    [
2733        Group {
2734            name: "A Group",
2735            entries: MailboxList(
2736                [
2737                    Mailbox {
2738                        name: Some(
2739                            "Ed Jones",
2740                        ),
2741                        address: AddrSpec {
2742                            local_part: "c",
2743                            domain: "a.test",
2744                        },
2745                    },
2746                    Mailbox {
2747                        name: None,
2748                        address: AddrSpec {
2749                            local_part: "joe",
2750                            domain: "where.test",
2751                        },
2752                    },
2753                    Mailbox {
2754                        name: Some(
2755                            "John",
2756                        ),
2757                        address: AddrSpec {
2758                            local_part: "jdoe",
2759                            domain: "one.test",
2760                        },
2761                    },
2762                ],
2763            ),
2764        },
2765    ],
2766)
2767"#
2768        );
2769
2770        let list = match msg.headers().cc() {
2771            Err(err) => panic!("Doh.\n{err:#}"),
2772            Ok(list) => list,
2773        };
2774        k9::snapshot!(
2775            list,
2776            r#"
2777Some(
2778    AddressList(
2779        [
2780            Group {
2781                name: "Undisclosed recipients",
2782                entries: MailboxList(
2783                    [],
2784                ),
2785            },
2786        ],
2787    ),
2788)
2789"#
2790        );
2791    }
2792
2793    #[test]
2794    fn message_id() {
2795        let message = concat!(
2796            "Message-Id: <foo@example.com>\n",
2797            "References: <a@example.com> <b@example.com>\n",
2798            "  <\"legacy\"@example.com>\n",
2799            "  <literal@[127.0.0.1]>\n",
2800            "\n\n\n"
2801        );
2802        let msg = MimePart::parse(message).unwrap();
2803        let list = match msg.headers().message_id() {
2804            Err(err) => panic!("Doh.\n{err:#}"),
2805            Ok(list) => list,
2806        };
2807        k9::snapshot!(
2808            list,
2809            r#"
2810Some(
2811    MessageID(
2812        "foo@example.com",
2813    ),
2814)
2815"#
2816        );
2817
2818        let list = match msg.headers().references() {
2819            Err(err) => panic!("Doh.\n{err:#}"),
2820            Ok(list) => list,
2821        };
2822        k9::snapshot!(
2823            list,
2824            r#"
2825Some(
2826    [
2827        MessageID(
2828            "a@example.com",
2829        ),
2830        MessageID(
2831            "b@example.com",
2832        ),
2833        MessageID(
2834            "legacy@example.com",
2835        ),
2836        MessageID(
2837            "literal@[127.0.0.1]",
2838        ),
2839    ],
2840)
2841"#
2842        );
2843    }
2844
2845    #[test]
2846    fn content_type() {
2847        let message = "Content-Type: text/plain\n\n\n\n";
2848        let msg = MimePart::parse(message).unwrap();
2849        let params = match msg.headers().content_type() {
2850            Err(err) => panic!("Doh.\n{err:#}"),
2851            Ok(params) => params,
2852        };
2853        k9::snapshot!(
2854            params,
2855            r#"
2856Some(
2857    MimeParameters {
2858        value: "text/plain",
2859        parameters: [],
2860    },
2861)
2862"#
2863        );
2864
2865        let message = "Content-Type: text/plain; charset=us-ascii\n\n\n\n";
2866        let msg = MimePart::parse(message).unwrap();
2867        let params = match msg.headers().content_type() {
2868            Err(err) => panic!("Doh.\n{err:#}"),
2869            Ok(params) => params.unwrap(),
2870        };
2871
2872        k9::snapshot!(
2873            params.get("charset"),
2874            r#"
2875Some(
2876    "us-ascii",
2877)
2878"#
2879        );
2880        k9::snapshot!(
2881            params,
2882            r#"
2883MimeParameters {
2884    value: "text/plain",
2885    parameters: [
2886        MimeParameter {
2887            name: "charset",
2888            section: None,
2889            mime_charset: None,
2890            mime_language: None,
2891            encoding: None,
2892            value: "us-ascii",
2893        },
2894    ],
2895}
2896"#
2897        );
2898
2899        let message = "Content-Type: text/plain; charset=\"us-ascii\"\n\n\n\n";
2900        let msg = MimePart::parse(message).unwrap();
2901        let params = match msg.headers().content_type() {
2902            Err(err) => panic!("Doh.\n{err:#}"),
2903            Ok(params) => params,
2904        };
2905        k9::snapshot!(
2906            params,
2907            r#"
2908Some(
2909    MimeParameters {
2910        value: "text/plain",
2911        parameters: [
2912            MimeParameter {
2913                name: "charset",
2914                section: None,
2915                mime_charset: None,
2916                mime_language: None,
2917                encoding: None,
2918                value: "us-ascii",
2919            },
2920        ],
2921    },
2922)
2923"#
2924        );
2925    }
2926
2927    #[test]
2928    fn content_type_rfc2231() {
2929        // This example is taken from the errata for rfc2231.
2930        // <https://www.rfc-editor.org/errata/eid590>
2931        let message = concat!(
2932            "Content-Type: application/x-stuff;\n",
2933            "\ttitle*0*=us-ascii'en'This%20is%20even%20more%20;\n",
2934            "\ttitle*1*=%2A%2A%2Afun%2A%2A%2A%20;\n",
2935            "\ttitle*2=\"isn't it!\"\n",
2936            "\n\n\n"
2937        );
2938        let msg = MimePart::parse(message).unwrap();
2939        let mut params = match msg.headers().content_type() {
2940            Err(err) => panic!("Doh.\n{err:#}"),
2941            Ok(params) => params.unwrap(),
2942        };
2943
2944        let original_title = params.get("title");
2945        k9::snapshot!(
2946            &original_title,
2947            r#"
2948Some(
2949    "This is even more ***fun*** isn't it!",
2950)
2951"#
2952        );
2953
2954        k9::snapshot!(
2955            &params,
2956            r#"
2957MimeParameters {
2958    value: "application/x-stuff",
2959    parameters: [
2960        MimeParameter {
2961            name: "title",
2962            section: Some(
2963                0,
2964            ),
2965            mime_charset: Some(
2966                "us-ascii",
2967            ),
2968            mime_language: Some(
2969                "en",
2970            ),
2971            encoding: Rfc2231,
2972            value: "This%20is%20even%20more%20",
2973        },
2974        MimeParameter {
2975            name: "title",
2976            section: Some(
2977                1,
2978            ),
2979            mime_charset: None,
2980            mime_language: None,
2981            encoding: Rfc2231,
2982            value: "%2A%2A%2Afun%2A%2A%2A%20",
2983        },
2984        MimeParameter {
2985            name: "title",
2986            section: Some(
2987                2,
2988            ),
2989            mime_charset: None,
2990            mime_language: None,
2991            encoding: None,
2992            value: "isn't it!",
2993        },
2994    ],
2995}
2996"#
2997        );
2998
2999        k9::snapshot!(
3000            params.encode_value(),
3001            r#"
3002application/x-stuff;\r
3003\ttitle="This is even more ***fun*** isn't it!"
3004"#
3005        );
3006
3007        params.set("foo", "bar 💩");
3008
3009        params.set(
3010            "long",
3011            "this is some text that should wrap because \
3012                it should be a good bit longer than our target maximum \
3013                length for this sort of thing, and hopefully we see at \
3014                least three lines produced as a result of setting \
3015                this value in this way",
3016        );
3017
3018        params.set(
3019            "longernnamethananyoneshouldreallyuse",
3020            "this is some text that should wrap because \
3021                it should be a good bit longer than our target maximum \
3022                length for this sort of thing, and hopefully we see at \
3023                least three lines produced as a result of setting \
3024                this value in this way",
3025        );
3026
3027        k9::snapshot!(
3028            params.encode_value(),
3029            r#"
3030application/x-stuff;\r
3031\tfoo*=UTF-8''bar%20%F0%9F%92%A9;\r
3032\tlong*0="this is some text that should wrap because it should be a good bi";\r
3033\tlong*1="t longer than our target maximum length for this sort of thing, a";\r
3034\tlong*2="nd hopefully we see at least three lines produced as a result of ";\r
3035\tlong*3="setting this value in this way";\r
3036\tlongernnamethananyoneshouldreallyuse*0="this is some text that should wra";\r
3037\tlongernnamethananyoneshouldreallyuse*1="p because it should be a good bit";\r
3038\tlongernnamethananyoneshouldreallyuse*2=" longer than our target maximum l";\r
3039\tlongernnamethananyoneshouldreallyuse*3="ength for this sort of thing, and";\r
3040\tlongernnamethananyoneshouldreallyuse*4=" hopefully we see at least three ";\r
3041\tlongernnamethananyoneshouldreallyuse*5="lines produced as a result of set";\r
3042\tlongernnamethananyoneshouldreallyuse*6="ting this value in this way";\r
3043\ttitle="This is even more ***fun*** isn't it!"
3044"#
3045        );
3046    }
3047
3048    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.2>
3049    #[test]
3050    fn authentication_results_b_2() {
3051        let ar = Header::with_name_value("Authentication-Results", "example.org 1; none");
3052        let ar = ar.as_authentication_results().unwrap();
3053        k9::snapshot!(
3054            &ar,
3055            r#"
3056AuthenticationResults {
3057    serv_id: "example.org",
3058    version: Some(
3059        1,
3060    ),
3061    results: [],
3062}
3063"#
3064        );
3065
3066        k9::snapshot!(ar.encode_value(), "example.org 1; none");
3067    }
3068
3069    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.3>
3070    #[test]
3071    fn authentication_results_b_3() {
3072        let ar = Header::with_name_value(
3073            "Authentication-Results",
3074            "example.com; spf=pass smtp.mailfrom=example.net",
3075        );
3076        k9::snapshot!(
3077            ar.as_authentication_results(),
3078            r#"
3079Ok(
3080    AuthenticationResults {
3081        serv_id: "example.com",
3082        version: None,
3083        results: [
3084            AuthenticationResult {
3085                method: "spf",
3086                method_version: None,
3087                result: "pass",
3088                reason: None,
3089                props: {
3090                    "smtp.mailfrom": "example.net",
3091                },
3092            },
3093        ],
3094    },
3095)
3096"#
3097        );
3098    }
3099
3100    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.4>
3101    #[test]
3102    fn authentication_results_b_4() {
3103        let ar = Header::with_name_value(
3104            "Authentication-Results",
3105            concat!(
3106                "example.com;\n",
3107                "\tauth=pass (cram-md5) smtp.auth=sender@example.net;\n",
3108                "\tspf=pass smtp.mailfrom=example.net"
3109            ),
3110        );
3111        k9::snapshot!(
3112            ar.as_authentication_results(),
3113            r#"
3114Ok(
3115    AuthenticationResults {
3116        serv_id: "example.com",
3117        version: None,
3118        results: [
3119            AuthenticationResult {
3120                method: "auth",
3121                method_version: None,
3122                result: "pass",
3123                reason: None,
3124                props: {
3125                    "smtp.auth": "sender@example.net",
3126                },
3127            },
3128            AuthenticationResult {
3129                method: "spf",
3130                method_version: None,
3131                result: "pass",
3132                reason: None,
3133                props: {
3134                    "smtp.mailfrom": "example.net",
3135                },
3136            },
3137        ],
3138    },
3139)
3140"#
3141        );
3142
3143        let ar = Header::with_name_value(
3144            "Authentication-Results",
3145            "example.com; iprev=pass\n\tpolicy.iprev=192.0.2.200",
3146        );
3147        k9::snapshot!(
3148            ar.as_authentication_results(),
3149            r#"
3150Ok(
3151    AuthenticationResults {
3152        serv_id: "example.com",
3153        version: None,
3154        results: [
3155            AuthenticationResult {
3156                method: "iprev",
3157                method_version: None,
3158                result: "pass",
3159                reason: None,
3160                props: {
3161                    "policy.iprev": "192.0.2.200",
3162                },
3163            },
3164        ],
3165    },
3166)
3167"#
3168        );
3169    }
3170
3171    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.5>
3172    #[test]
3173    fn authentication_results_b_5() {
3174        let ar = Header::with_name_value(
3175            "Authentication-Results",
3176            "example.com;\n\tdkim=pass (good signature) header.d=example.com",
3177        );
3178        k9::snapshot!(
3179            ar.as_authentication_results(),
3180            r#"
3181Ok(
3182    AuthenticationResults {
3183        serv_id: "example.com",
3184        version: None,
3185        results: [
3186            AuthenticationResult {
3187                method: "dkim",
3188                method_version: None,
3189                result: "pass",
3190                reason: None,
3191                props: {
3192                    "header.d": "example.com",
3193                },
3194            },
3195        ],
3196    },
3197)
3198"#
3199        );
3200
3201        let ar = Header::with_name_value(
3202            "Authentication-Results",
3203            "example.com;\n\tauth=pass (cram-md5) smtp.auth=sender@example.com;\n\tspf=fail smtp.mailfrom=example.com"
3204        );
3205        let ar = ar.as_authentication_results().unwrap();
3206        k9::snapshot!(
3207            &ar,
3208            r#"
3209AuthenticationResults {
3210    serv_id: "example.com",
3211    version: None,
3212    results: [
3213        AuthenticationResult {
3214            method: "auth",
3215            method_version: None,
3216            result: "pass",
3217            reason: None,
3218            props: {
3219                "smtp.auth": "sender@example.com",
3220            },
3221        },
3222        AuthenticationResult {
3223            method: "spf",
3224            method_version: None,
3225            result: "fail",
3226            reason: None,
3227            props: {
3228                "smtp.mailfrom": "example.com",
3229            },
3230        },
3231    ],
3232}
3233"#
3234        );
3235
3236        k9::snapshot!(
3237            ar.encode_value(),
3238            r#"
3239example.com;\r
3240\tauth=pass\r
3241\tsmtp.auth=sender@example.com;\r
3242\tspf=fail\r
3243\tsmtp.mailfrom=example.com
3244"#
3245        );
3246    }
3247
3248    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.6>
3249    #[test]
3250    fn authentication_results_b_6() {
3251        let ar = Header::with_name_value(
3252            "Authentication-Results",
3253            concat!(
3254                "example.com;\n",
3255                "\tdkim=pass reason=\"good signature\"\n",
3256                "\theader.i=@mail-router.example.net;\n",
3257                "\tdkim=fail reason=\"bad signature\"\n",
3258                "\theader.i=@newyork.example.com"
3259            ),
3260        );
3261        let ar = match ar.as_authentication_results() {
3262            Err(err) => panic!("\n{err}"),
3263            Ok(ar) => ar,
3264        };
3265
3266        k9::snapshot!(
3267            &ar,
3268            r#"
3269AuthenticationResults {
3270    serv_id: "example.com",
3271    version: None,
3272    results: [
3273        AuthenticationResult {
3274            method: "dkim",
3275            method_version: None,
3276            result: "pass",
3277            reason: Some(
3278                "good signature",
3279            ),
3280            props: {
3281                "header.i": "@mail-router.example.net",
3282            },
3283        },
3284        AuthenticationResult {
3285            method: "dkim",
3286            method_version: None,
3287            result: "fail",
3288            reason: Some(
3289                "bad signature",
3290            ),
3291            props: {
3292                "header.i": "@newyork.example.com",
3293            },
3294        },
3295    ],
3296}
3297"#
3298        );
3299
3300        k9::snapshot!(
3301            ar.encode_value(),
3302            r#"
3303example.com;\r
3304\tdkim=pass reason="good signature"\r
3305\theader.i=@mail-router.example.net;\r
3306\tdkim=fail reason="bad signature"\r
3307\theader.i=@newyork.example.com
3308"#
3309        );
3310
3311        let ar = Header::with_name_value(
3312            "Authentication-Results",
3313            concat!(
3314                "example.net;\n",
3315                "\tdkim=pass (good signature) header.i=@newyork.example.com"
3316            ),
3317        );
3318        let ar = match ar.as_authentication_results() {
3319            Err(err) => panic!("\n{err}"),
3320            Ok(ar) => ar,
3321        };
3322
3323        k9::snapshot!(
3324            &ar,
3325            r#"
3326AuthenticationResults {
3327    serv_id: "example.net",
3328    version: None,
3329    results: [
3330        AuthenticationResult {
3331            method: "dkim",
3332            method_version: None,
3333            result: "pass",
3334            reason: None,
3335            props: {
3336                "header.i": "@newyork.example.com",
3337            },
3338        },
3339    ],
3340}
3341"#
3342        );
3343
3344        k9::snapshot!(
3345            ar.encode_value(),
3346            r#"
3347example.net;\r
3348\tdkim=pass\r
3349\theader.i=@newyork.example.com
3350"#
3351        );
3352    }
3353
3354    /// <https://datatracker.ietf.org/doc/html/rfc8601#appendix-B.7>
3355    #[test]
3356    fn authentication_results_b_7() {
3357        let ar = Header::with_name_value(
3358            "Authentication-Results",
3359            concat!(
3360                "foo.example.net (foobar) 1 (baz);\n",
3361                "\tdkim (Because I like it) / 1 (One yay) = (wait for it) fail\n",
3362                "\tpolicy (A dot can go here) . (like that) expired\n",
3363                "\t(this surprised me) = (as I wasn't expecting it) 1362471462"
3364            ),
3365        );
3366        let ar = match ar.as_authentication_results() {
3367            Err(err) => panic!("\n{err}"),
3368            Ok(ar) => ar,
3369        };
3370
3371        k9::snapshot!(
3372            &ar,
3373            r#"
3374AuthenticationResults {
3375    serv_id: "foo.example.net",
3376    version: Some(
3377        1,
3378    ),
3379    results: [
3380        AuthenticationResult {
3381            method: "dkim",
3382            method_version: Some(
3383                1,
3384            ),
3385            result: "fail",
3386            reason: None,
3387            props: {
3388                "policy.expired": "1362471462",
3389            },
3390        },
3391    ],
3392}
3393"#
3394        );
3395
3396        k9::snapshot!(
3397            ar.encode_value(),
3398            r#"
3399foo.example.net 1;\r
3400\tdkim/1=fail\r
3401\tpolicy.expired=1362471462
3402"#
3403        );
3404    }
3405
3406    #[test]
3407    fn arc_authentication_results_1() {
3408        let ar = Header::with_name_value(
3409            "ARC-Authentication-Results",
3410            "i=3; clochette.example.org; spf=fail
3411    smtp.from=jqd@d1.example; dkim=fail (512-bit key)
3412    header.i=@d1.example; dmarc=fail; arc=pass (as.2.gmail.example=pass,
3413    ams.2.gmail.example=pass, as.1.lists.example.org=pass,
3414    ams.1.lists.example.org=fail (message has been altered))",
3415        );
3416        let ar = match ar.as_arc_authentication_results() {
3417            Err(err) => panic!("\n{err}"),
3418            Ok(ar) => ar,
3419        };
3420
3421        k9::snapshot!(
3422            &ar,
3423            r#"
3424ARCAuthenticationResults {
3425    instance: 3,
3426    serv_id: "clochette.example.org",
3427    version: None,
3428    results: [
3429        AuthenticationResult {
3430            method: "spf",
3431            method_version: None,
3432            result: "fail",
3433            reason: None,
3434            props: {
3435                "smtp.from": "jqd@d1.example",
3436            },
3437        },
3438        AuthenticationResult {
3439            method: "dkim",
3440            method_version: None,
3441            result: "fail",
3442            reason: None,
3443            props: {
3444                "header.i": "@d1.example",
3445            },
3446        },
3447        AuthenticationResult {
3448            method: "dmarc",
3449            method_version: None,
3450            result: "fail",
3451            reason: None,
3452            props: {},
3453        },
3454        AuthenticationResult {
3455            method: "arc",
3456            method_version: None,
3457            result: "pass",
3458            reason: None,
3459            props: {},
3460        },
3461    ],
3462}
3463"#
3464        );
3465    }
3466}