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