mailparsing/
rfc5322_parser.rs

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