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