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