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