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