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_type",
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 regular_parameter,
1186 extended_param_with_charset,
1187 extended_param_no_charset,
1188 )),
1189 )(input)
1190}
1191
1192fn extended_param_with_charset(input: Span) -> IResult<Span, MimeParameter> {
1193 context(
1194 "extended_param_with_charset",
1195 map(
1196 tuple((
1197 attribute,
1198 opt(section),
1199 char('*'),
1200 opt(cfws),
1201 char('='),
1202 opt(cfws),
1203 opt(mime_charset),
1204 char('\''),
1205 opt(mime_language),
1206 char('\''),
1207 map(
1208 recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))),
1209 |s: Span| s.to_string(),
1210 ),
1211 )),
1212 |(name, section, _, _, _, _, mime_charset, _, mime_language, _, value)| MimeParameter {
1213 name: name.to_string(),
1214 section,
1215 mime_charset: mime_charset.map(|s| s.to_string()),
1216 mime_language: mime_language.map(|s| s.to_string()),
1217 uses_encoding: true,
1218 value,
1219 },
1220 ),
1221 )(input)
1222}
1223
1224fn extended_param_no_charset(input: Span) -> IResult<Span, MimeParameter> {
1225 context(
1226 "extended_param_no_charset",
1227 map(
1228 tuple((
1229 attribute,
1230 opt(section),
1231 opt(char('*')),
1232 opt(cfws),
1233 char('='),
1234 opt(cfws),
1235 alt((
1236 quoted_string,
1237 map(
1238 recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))),
1239 |s: Span| s.to_string(),
1240 ),
1241 )),
1242 )),
1243 |(name, section, star, _, _, _, value)| MimeParameter {
1244 name: name.to_string(),
1245 section,
1246 mime_charset: None,
1247 mime_language: None,
1248 uses_encoding: star.is_some(),
1249 value,
1250 },
1251 ),
1252 )(input)
1253}
1254
1255fn mime_charset(input: Span) -> IResult<Span, Span> {
1256 context(
1257 "mime_charset",
1258 take_while1(|c| is_mime_token(c) && c != '\''),
1259 )(input)
1260}
1261
1262fn mime_language(input: Span) -> IResult<Span, Span> {
1263 context(
1264 "mime_language",
1265 take_while1(|c| is_mime_token(c) && c != '\''),
1266 )(input)
1267}
1268
1269fn ext_octet(input: Span) -> IResult<Span, Span> {
1270 context(
1271 "ext_octet",
1272 recognize(tuple((
1273 char('%'),
1274 satisfy(|c| c.is_ascii_hexdigit()),
1275 satisfy(|c| c.is_ascii_hexdigit()),
1276 ))),
1277 )(input)
1278}
1279
1280fn section(input: Span) -> IResult<Span, u32> {
1282 context(
1283 "section",
1284 preceded(char('*'), nom::character::complete::u32),
1285 )(input)
1286}
1287
1288fn regular_parameter(input: Span) -> IResult<Span, MimeParameter> {
1290 context(
1291 "regular_parameter",
1292 map(
1293 tuple((attribute, opt(cfws), char('='), opt(cfws), value)),
1294 |(name, _, _, _, value)| MimeParameter {
1295 name: name.to_string(),
1296 value,
1297 section: None,
1298 uses_encoding: false,
1299 mime_charset: None,
1300 mime_language: None,
1301 },
1302 ),
1303 )(input)
1304}
1305
1306fn attribute(input: Span) -> IResult<Span, Span> {
1309 context("attribute", take_while1(is_attribute_char))(input)
1310}
1311
1312fn value(input: Span) -> IResult<Span, String> {
1313 context(
1314 "value",
1315 alt((map(mime_token, |s: Span| s.to_string()), quoted_string)),
1316 )(input)
1317}
1318
1319pub struct Parser;
1320
1321impl Parser {
1322 pub fn parse_mailbox_list_header(text: &str) -> Result<MailboxList> {
1323 parse_with(text, mailbox_list)
1324 }
1325
1326 pub fn parse_mailbox_header(text: &str) -> Result<Mailbox> {
1327 parse_with(text, mailbox)
1328 }
1329
1330 pub fn parse_address_list_header(text: &str) -> Result<AddressList> {
1331 parse_with(text, address_list)
1332 }
1333
1334 pub fn parse_msg_id_header(text: &str) -> Result<MessageID> {
1335 parse_with(text, msg_id)
1336 }
1337
1338 pub fn parse_msg_id_header_list(text: &str) -> Result<Vec<MessageID>> {
1339 parse_with(text, msg_id_list)
1340 }
1341
1342 pub fn parse_content_id_header(text: &str) -> Result<MessageID> {
1343 parse_with(text, content_id)
1344 }
1345
1346 pub fn parse_content_type_header(text: &str) -> Result<MimeParameters> {
1347 parse_with(text, content_type)
1348 }
1349
1350 pub fn parse_content_transfer_encoding_header(text: &str) -> Result<MimeParameters> {
1351 parse_with(text, content_transfer_encoding)
1352 }
1353
1354 pub fn parse_unstructured_header(text: &str) -> Result<String> {
1355 parse_with(text, unstructured)
1356 }
1357
1358 pub fn parse_authentication_results_header(text: &str) -> Result<AuthenticationResults> {
1359 parse_with(text, authentication_results)
1360 }
1361}
1362
1363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1364pub struct AuthenticationResults {
1365 pub serv_id: String,
1366 pub version: Option<u32>,
1367 pub results: Vec<AuthenticationResult>,
1368}
1369
1370fn emit_value_token(value: &str, target: &mut String) {
1372 let use_quoted_string = !value.chars().all(|c| is_mime_token(c) || c == '@');
1373 if use_quoted_string {
1374 target.push('"');
1375 for c in value.chars() {
1376 if c == '"' || c == '\\' {
1377 target.push('\\');
1378 }
1379 target.push(c);
1380 }
1381 target.push('"');
1382 } else {
1383 target.push_str(value);
1384 }
1385}
1386
1387impl EncodeHeaderValue for AuthenticationResults {
1388 fn encode_value(&self) -> SharedString<'static> {
1389 let mut result = match self.version {
1390 Some(v) => format!("{} {v}", self.serv_id),
1391 None => self.serv_id.to_string(),
1392 };
1393 if self.results.is_empty() {
1394 result.push_str("; none");
1395 } else {
1396 for res in &self.results {
1397 result.push_str(";\r\n\t");
1398 emit_value_token(&res.method, &mut result);
1399 if let Some(v) = res.method_version {
1400 result.push_str(&format!("/{v}"));
1401 }
1402 result.push('=');
1403 emit_value_token(&res.result, &mut result);
1404 if let Some(reason) = &res.reason {
1405 result.push_str(" reason=");
1406 emit_value_token(reason, &mut result);
1407 }
1408 for (k, v) in &res.props {
1409 result.push_str(&format!("\r\n\t{k}="));
1410 emit_value_token(v, &mut result);
1411 }
1412 }
1413 }
1414
1415 result.into()
1416 }
1417}
1418
1419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1420pub struct AuthenticationResult {
1421 pub method: String,
1422 pub method_version: Option<u32>,
1423 pub result: String,
1424 pub reason: Option<String>,
1425 pub props: BTreeMap<String, String>,
1426}
1427
1428#[derive(Debug, Clone, PartialEq, Eq)]
1429pub struct AddrSpec {
1430 pub local_part: String,
1431 pub domain: String,
1432}
1433
1434impl AddrSpec {
1435 pub fn new(local_part: &str, domain: &str) -> Self {
1436 Self {
1437 local_part: local_part.to_string(),
1438 domain: domain.to_string(),
1439 }
1440 }
1441
1442 pub fn parse(email: &str) -> Result<Self> {
1443 parse_with(email, addr_spec)
1444 }
1445}
1446
1447impl EncodeHeaderValue for AddrSpec {
1448 fn encode_value(&self) -> SharedString<'static> {
1449 let mut result = String::new();
1450
1451 let needs_quoting = !self.local_part.chars().all(|c| is_atext(c) || c == '.');
1452 if needs_quoting {
1453 result.push('"');
1454 for c in self.local_part.chars() {
1459 if c == '"' || c == '\\' {
1460 result.push('\\');
1461 }
1462 result.push(c);
1463 }
1464 result.push('"');
1465 } else {
1466 result.push_str(&self.local_part);
1467 }
1468 result.push('@');
1469 result.push_str(&self.domain);
1470
1471 result.into()
1472 }
1473}
1474
1475#[derive(Debug, Clone, PartialEq, Eq)]
1476pub enum Address {
1477 Mailbox(Mailbox),
1478 Group { name: String, entries: MailboxList },
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Eq)]
1482pub struct AddressList(pub Vec<Address>);
1483
1484#[derive(Debug, Clone, PartialEq, Eq)]
1485pub struct MailboxList(pub Vec<Mailbox>);
1486
1487#[derive(Debug, Clone, PartialEq, Eq)]
1488pub struct Mailbox {
1489 pub name: Option<String>,
1490 pub address: AddrSpec,
1491}
1492
1493#[derive(Debug, Clone, PartialEq, Eq)]
1494pub struct MessageID(pub String);
1495
1496impl EncodeHeaderValue for MessageID {
1497 fn encode_value(&self) -> SharedString<'static> {
1498 format!("<{}>", self.0).into()
1499 }
1500}
1501
1502impl EncodeHeaderValue for Vec<MessageID> {
1503 fn encode_value(&self) -> SharedString<'static> {
1504 let mut result = String::new();
1505 for id in self {
1506 if !result.is_empty() {
1507 result.push_str("\r\n\t");
1508 }
1509 result.push_str(&format!("<{}>", id.0));
1510 }
1511 result.into()
1512 }
1513}
1514
1515#[derive(Debug, Clone, PartialEq, Eq)]
1516struct MimeParameter {
1517 pub name: String,
1518 pub section: Option<u32>,
1519 pub mime_charset: Option<String>,
1520 pub mime_language: Option<String>,
1521 pub uses_encoding: bool,
1522 pub value: String,
1523}
1524
1525#[derive(Debug, Clone, PartialEq, Eq)]
1526pub struct MimeParameters {
1527 pub value: String,
1528 parameters: Vec<MimeParameter>,
1529}
1530
1531impl MimeParameters {
1532 pub fn new(value: &str) -> Self {
1533 Self {
1534 value: value.to_string(),
1535 parameters: vec![],
1536 }
1537 }
1538
1539 pub fn get(&self, name: &str) -> Option<String> {
1545 let mut elements: Vec<_> = self
1546 .parameters
1547 .iter()
1548 .filter(|p| p.name.eq_ignore_ascii_case(name))
1549 .collect();
1550 if elements.is_empty() {
1551 return None;
1552 }
1553 elements.sort_by(|a, b| a.section.cmp(&b.section));
1554
1555 let mut mime_charset = None;
1556 let mut result = String::new();
1557
1558 for ele in elements {
1559 if let Some(cset) = ele.mime_charset.as_deref() {
1560 mime_charset = Charset::for_label_no_replacement(cset.as_bytes());
1561 }
1562
1563 if let Some((charset, true)) =
1564 mime_charset.as_ref().map(|cset| (cset, ele.uses_encoding))
1565 {
1566 let mut chars = ele.value.chars();
1567 let mut bytes: Vec<u8> = vec![];
1568
1569 fn char_to_bytes(c: char, bytes: &mut Vec<u8>) {
1570 let mut buf = [0u8; 8];
1571 let s = c.encode_utf8(&mut buf);
1572 for b in s.bytes() {
1573 bytes.push(b);
1574 }
1575 }
1576
1577 'next_char: while let Some(c) = chars.next() {
1578 match c {
1579 '%' => {
1580 let mut value = 0u8;
1581 for _ in 0..2 {
1582 match chars.next() {
1583 Some(n) => match n {
1584 '0'..='9' => {
1585 value <<= 4;
1586 value |= n as u32 as u8 - b'0';
1587 }
1588 'a'..='f' => {
1589 value <<= 4;
1590 value |= (n as u32 as u8 - b'a') + 10;
1591 }
1592 'A'..='F' => {
1593 value <<= 4;
1594 value |= (n as u32 as u8 - b'A') + 10;
1595 }
1596 _ => {
1597 char_to_bytes('%', &mut bytes);
1598 char_to_bytes(n, &mut bytes);
1599 break 'next_char;
1600 }
1601 },
1602 None => {
1603 char_to_bytes('%', &mut bytes);
1604 break 'next_char;
1605 }
1606 }
1607 }
1608
1609 bytes.push(value);
1610 }
1611 c => {
1612 char_to_bytes(c, &mut bytes);
1613 }
1614 }
1615 }
1616
1617 let (decoded, _malformed) = charset.decode_without_bom_handling(&bytes);
1618 result.push_str(&decoded);
1619 } else {
1620 result.push_str(&ele.value);
1621 }
1622 }
1623
1624 Some(result)
1625 }
1626
1627 pub fn remove(&mut self, name: &str) {
1629 self.parameters
1630 .retain(|p| !p.name.eq_ignore_ascii_case(name));
1631 }
1632
1633 pub fn set(&mut self, name: &str, value: &str) {
1634 self.remove(name);
1635
1636 self.parameters.push(MimeParameter {
1637 name: name.to_string(),
1638 value: value.to_string(),
1639 section: None,
1640 mime_charset: None,
1641 mime_language: None,
1642 uses_encoding: false,
1643 });
1644 }
1645
1646 pub fn is_multipart(&self) -> bool {
1647 self.value.starts_with("message/") || self.value.starts_with("multipart/")
1648 }
1649
1650 pub fn is_text(&self) -> bool {
1651 self.value.starts_with("text/")
1652 }
1653}
1654
1655impl EncodeHeaderValue for MimeParameters {
1656 fn encode_value(&self) -> SharedString<'static> {
1657 let mut result = self.value.to_string();
1658 let mut names: Vec<&str> = self.parameters.iter().map(|p| p.name.as_str()).collect();
1659 names.sort();
1660 names.dedup();
1661
1662 for name in names {
1663 let value = self.get(name).expect("name to be present");
1664
1665 let needs_encoding = value.chars().any(|c| !is_mime_token(c) || !c.is_ascii());
1666 let use_quoted_string = value
1669 .chars()
1670 .all(|c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii());
1671
1672 let mut params = vec![];
1673 let mut chars = value.chars().peekable();
1674 while chars.peek().is_some() {
1675 let count = params.len();
1676 let is_first = count == 0;
1677 let prefix = if use_quoted_string {
1678 "\""
1679 } else if is_first && needs_encoding {
1680 "UTF-8''"
1681 } else {
1682 ""
1683 };
1684 let limit = 74 - (name.len() + 4 + prefix.len());
1685
1686 let mut encoded = String::new();
1687
1688 while encoded.len() < limit {
1689 let c = match chars.next() {
1690 Some(c) => c,
1691 None => break,
1692 };
1693
1694 if use_quoted_string {
1695 if c == '"' || c == '\\' {
1696 encoded.push('\\');
1697 }
1698 encoded.push(c);
1699 } else if is_mime_token(c) && (!needs_encoding || c != '%') {
1700 encoded.push(c);
1701 } else {
1702 let mut buf = [0u8; 8];
1703 let s = c.encode_utf8(&mut buf);
1704 for b in s.bytes() {
1705 encoded.push('%');
1706 encoded.push(HEX_CHARS[(b as usize) >> 4] as char);
1707 encoded.push(HEX_CHARS[(b as usize) & 0x0f] as char);
1708 }
1709 }
1710 }
1711
1712 if use_quoted_string {
1713 encoded.push('"');
1714 }
1715
1716 params.push(MimeParameter {
1717 name: name.to_string(),
1718 section: Some(count as u32),
1719 mime_charset: if is_first {
1720 Some("UTF-8".to_string())
1721 } else {
1722 None
1723 },
1724 mime_language: None,
1725 uses_encoding: needs_encoding,
1726 value: encoded,
1727 })
1728 }
1729 if params.len() == 1 {
1730 params.last_mut().map(|p| p.section = None);
1731 }
1732 for p in params {
1733 result.push_str(";\r\n\t");
1734 let charset_tick = if !use_quoted_string
1735 && (p.mime_charset.is_some() || p.mime_language.is_some())
1736 {
1737 "'"
1738 } else {
1739 ""
1740 };
1741 let lang_tick = if !use_quoted_string
1742 && (p.mime_language.is_some() || p.mime_charset.is_some())
1743 {
1744 "'"
1745 } else {
1746 ""
1747 };
1748
1749 let section = p
1750 .section
1751 .map(|s| format!("*{s}"))
1752 .unwrap_or_else(String::new);
1753
1754 let uses_encoding = if !use_quoted_string && p.uses_encoding {
1755 "*"
1756 } else {
1757 ""
1758 };
1759 let charset = if use_quoted_string {
1760 "\""
1761 } else {
1762 p.mime_charset.as_deref().unwrap_or("")
1763 };
1764 let lang = p.mime_language.as_deref().unwrap_or("");
1765
1766 let line = format!(
1767 "{name}{section}{uses_encoding}={charset}{charset_tick}{lang}{lang_tick}{value}",
1768 name = &p.name,
1769 value = &p.value
1770 );
1771 result.push_str(&line);
1772 }
1773 }
1774 result.into()
1775 }
1776}
1777
1778static HEX_CHARS: &[u8] = b"0123456789ABCDEF";
1779
1780pub(crate) fn qp_encode(s: &str) -> String {
1781 let prefix = b"=?UTF-8?q?";
1782 let suffix = b"?=";
1783 let limit = 72 - (prefix.len() + suffix.len());
1784
1785 let mut result = Vec::with_capacity(s.len());
1786
1787 result.extend_from_slice(prefix);
1788 let mut line_length = 0;
1789
1790 enum Bytes<'a> {
1791 Passthru(&'a [u8]),
1792 Encode(&'a [u8]),
1793 }
1794
1795 for c in s.chars() {
1798 let mut bytes = [0u8; 4];
1799 let bytes = c.encode_utf8(&mut bytes).as_bytes();
1800
1801 let b = if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
1802 && c != '?'
1803 && c != '='
1804 && c != ' '
1805 && c != '\t'
1806 {
1807 Bytes::Passthru(bytes)
1808 } else if c == ' ' {
1809 Bytes::Passthru(b"_")
1810 } else {
1811 Bytes::Encode(bytes)
1812 };
1813
1814 let need_len = match b {
1815 Bytes::Passthru(b) => b.len(),
1816 Bytes::Encode(b) => b.len() * 3,
1817 };
1818
1819 if need_len > limit - line_length {
1820 result.extend_from_slice(suffix);
1822 result.extend_from_slice(b"\r\n\t");
1823 result.extend_from_slice(prefix);
1824 line_length = 0;
1825 }
1826
1827 match b {
1828 Bytes::Passthru(c) => {
1829 result.extend_from_slice(c);
1830 }
1831 Bytes::Encode(bytes) => {
1832 for &c in bytes {
1833 result.push(b'=');
1834 result.push(HEX_CHARS[(c as usize) >> 4]);
1835 result.push(HEX_CHARS[(c as usize) & 0x0f]);
1836 }
1837 }
1838 }
1839
1840 line_length += need_len;
1841 }
1842
1843 if line_length > 0 {
1844 result.extend_from_slice(suffix);
1845 }
1846
1847 unsafe { String::from_utf8_unchecked(result) }
1850}
1851
1852#[cfg(test)]
1853#[test]
1854fn test_qp_encode() {
1855 let encoded = qp_encode(
1856 "hello, I am a line that is this long, or maybe a little \
1857 bit longer than this, and that should get wrapped by the encoder",
1858 );
1859 k9::snapshot!(
1860 encoded,
1861 r#"
1862=?UTF-8?q?hello,_I_am_a_line_that_is_this_long,_or_maybe_a_little_bit_?=\r
1863\t=?UTF-8?q?longer_than_this,_and_that_should_get_wrapped_by_the_encoder?=
1864"#
1865 );
1866}
1867
1868pub(crate) fn quote_string(s: &str, needs_quote: &str) -> String {
1872 const QUOTE_OVERALL: &str = "@,";
1873 if s.chars()
1874 .any(|c| needs_quote.contains(c) || QUOTE_OVERALL.contains(c))
1875 {
1876 let mut result = String::with_capacity(s.len() + 4);
1877 result.push('"');
1878 for c in s.chars() {
1879 if needs_quote.contains(c) {
1880 result.push('\\');
1881 }
1882 result.push(c);
1883 }
1884 result.push('"');
1885 result
1886 } else {
1887 s.to_string()
1888 }
1889}
1890
1891#[cfg(test)]
1892#[test]
1893fn test_quote_string() {
1894 let nq = "\\\"";
1895 k9::snapshot!(quote_string("hello", nq), "hello");
1896 k9::snapshot!(quote_string("hello there", nq), "hello there");
1897 k9::snapshot!(quote_string("hello, there", nq), "\"hello, there\"");
1898 k9::snapshot!(
1899 quote_string("hello \"there\"", nq),
1900 r#""hello \\"there\\"""#
1901 );
1902 k9::snapshot!(
1903 quote_string("hello c:\\backslash", nq),
1904 r#""hello c:\\\\backslash""#
1905 );
1906}
1907
1908impl EncodeHeaderValue for Mailbox {
1909 fn encode_value(&self) -> SharedString<'static> {
1910 match &self.name {
1911 Some(name) => {
1912 let mut value = if name.is_ascii() {
1913 quote_string(name, "\\\"")
1914 } else {
1915 qp_encode(name)
1916 };
1917
1918 value.push_str(" <");
1919 value.push_str(&self.address.encode_value());
1920 value.push('>');
1921 value.into()
1922 }
1923 None => format!("<{}>", self.address.encode_value()).into(),
1924 }
1925 }
1926}
1927
1928impl EncodeHeaderValue for MailboxList {
1929 fn encode_value(&self) -> SharedString<'static> {
1930 let mut result = String::new();
1931 for mailbox in &self.0 {
1932 if !result.is_empty() {
1933 result.push_str(",\r\n\t");
1934 }
1935 result.push_str(&mailbox.encode_value());
1936 }
1937 result.into()
1938 }
1939}
1940
1941impl EncodeHeaderValue for Address {
1942 fn encode_value(&self) -> SharedString<'static> {
1943 match self {
1944 Self::Mailbox(mbox) => mbox.encode_value(),
1945 Self::Group { name, entries } => {
1946 let mut result = format!("{name}:");
1947 result += &entries.encode_value();
1948 result.push(';');
1949 result.into()
1950 }
1951 }
1952 }
1953}
1954
1955impl EncodeHeaderValue for AddressList {
1956 fn encode_value(&self) -> SharedString<'static> {
1957 let mut result = String::new();
1958 for address in &self.0 {
1959 if !result.is_empty() {
1960 result.push_str(",\r\n\t");
1961 }
1962 result.push_str(&address.encode_value());
1963 }
1964 result.into()
1965 }
1966}
1967
1968#[cfg(test)]
1969mod test {
1970 use super::*;
1971 use crate::{Header, MimePart};
1972
1973 #[test]
1974 fn mailbox_encodes_at() {
1975 let mbox = Mailbox {
1976 name: Some("foo@bar.com".to_string()),
1977 address: AddrSpec {
1978 local_part: "foo".to_string(),
1979 domain: "bar.com".to_string(),
1980 },
1981 };
1982 assert_eq!(mbox.encode_value(), "\"foo@bar.com\" <foo@bar.com>");
1983 }
1984
1985 #[test]
1986 fn mailbox_list_singular() {
1987 let message = concat!(
1988 "From: Someone (hello) <someone@example.com>, other@example.com,\n",
1989 " \"John \\\"Smith\\\"\" (comment) \"More Quotes\" (more comment) <someone(another comment)@crazy.example.com(woot)>\n",
1990 "\n",
1991 "I am the body"
1992 );
1993 let msg = MimePart::parse(message).unwrap();
1994 let list = match msg.headers().from() {
1995 Err(err) => panic!("Doh.\n{err:#}"),
1996 Ok(list) => list,
1997 };
1998
1999 k9::snapshot!(
2000 list,
2001 r#"
2002Some(
2003 MailboxList(
2004 [
2005 Mailbox {
2006 name: Some(
2007 "Someone",
2008 ),
2009 address: AddrSpec {
2010 local_part: "someone",
2011 domain: "example.com",
2012 },
2013 },
2014 Mailbox {
2015 name: None,
2016 address: AddrSpec {
2017 local_part: "other",
2018 domain: "example.com",
2019 },
2020 },
2021 Mailbox {
2022 name: Some(
2023 "John "Smith" More Quotes",
2024 ),
2025 address: AddrSpec {
2026 local_part: "someone",
2027 domain: "crazy.example.com",
2028 },
2029 },
2030 ],
2031 ),
2032)
2033"#
2034 );
2035 }
2036
2037 #[test]
2038 fn docomo_non_compliant_localpart() {
2039 let message = "Sender: hello..there@docomo.ne.jp\n\n\n";
2040 let msg = MimePart::parse(message).unwrap();
2041 let err = msg.headers().sender().unwrap_err();
2042 k9::snapshot!(
2043 err,
2044 r#"
2045HeaderParse(
2046 "0: at line 1:
2047hello..there@docomo.ne.jp
2048 ^___________________
2049expected '@', found .
2050
20511: at line 1, in addr_spec:
2052hello..there@docomo.ne.jp
2053^________________________
2054
20552: at line 1, in mailbox:
2056hello..there@docomo.ne.jp
2057^________________________
2058
2059",
2060)
2061"#
2062 );
2063 }
2064
2065 #[test]
2066 fn sender() {
2067 let message = "Sender: someone@[127.0.0.1]\n\n\n";
2068 let msg = MimePart::parse(message).unwrap();
2069 let list = match msg.headers().sender() {
2070 Err(err) => panic!("Doh.\n{err:#}"),
2071 Ok(list) => list,
2072 };
2073 k9::snapshot!(
2074 list,
2075 r#"
2076Some(
2077 Mailbox {
2078 name: None,
2079 address: AddrSpec {
2080 local_part: "someone",
2081 domain: "[127.0.0.1]",
2082 },
2083 },
2084)
2085"#
2086 );
2087 }
2088
2089 #[test]
2090 fn domain_literal() {
2091 let message = "From: someone@[127.0.0.1]\n\n\n";
2092 let msg = MimePart::parse(message).unwrap();
2093 let list = match msg.headers().from() {
2094 Err(err) => panic!("Doh.\n{err:#}"),
2095 Ok(list) => list,
2096 };
2097 k9::snapshot!(
2098 list,
2099 r#"
2100Some(
2101 MailboxList(
2102 [
2103 Mailbox {
2104 name: None,
2105 address: AddrSpec {
2106 local_part: "someone",
2107 domain: "[127.0.0.1]",
2108 },
2109 },
2110 ],
2111 ),
2112)
2113"#
2114 );
2115 }
2116
2117 #[test]
2118 fn rfc6532() {
2119 let message = concat!(
2120 "From: Keith Moore <moore@cs.utk.edu>\n",
2121 "To: Keld Jørn Simonsen <keld@dkuug.dk>\n",
2122 "CC: André Pirard <PIRARD@vm1.ulg.ac.be>\n",
2123 "Subject: Hello André\n",
2124 "\n\n"
2125 );
2126 let msg = MimePart::parse(message).unwrap();
2127 let list = match msg.headers().from() {
2128 Err(err) => panic!("Doh.\n{err:#}"),
2129 Ok(list) => list,
2130 };
2131 k9::snapshot!(
2132 list,
2133 r#"
2134Some(
2135 MailboxList(
2136 [
2137 Mailbox {
2138 name: Some(
2139 "Keith Moore",
2140 ),
2141 address: AddrSpec {
2142 local_part: "moore",
2143 domain: "cs.utk.edu",
2144 },
2145 },
2146 ],
2147 ),
2148)
2149"#
2150 );
2151
2152 let list = match msg.headers().to() {
2153 Err(err) => panic!("Doh.\n{err:#}"),
2154 Ok(list) => list,
2155 };
2156 k9::snapshot!(
2157 list,
2158 r#"
2159Some(
2160 AddressList(
2161 [
2162 Mailbox(
2163 Mailbox {
2164 name: Some(
2165 "Keld Jørn Simonsen",
2166 ),
2167 address: AddrSpec {
2168 local_part: "keld",
2169 domain: "dkuug.dk",
2170 },
2171 },
2172 ),
2173 ],
2174 ),
2175)
2176"#
2177 );
2178
2179 let list = match msg.headers().cc() {
2180 Err(err) => panic!("Doh.\n{err:#}"),
2181 Ok(list) => list,
2182 };
2183 k9::snapshot!(
2184 list,
2185 r#"
2186Some(
2187 AddressList(
2188 [
2189 Mailbox(
2190 Mailbox {
2191 name: Some(
2192 "André Pirard",
2193 ),
2194 address: AddrSpec {
2195 local_part: "PIRARD",
2196 domain: "vm1.ulg.ac.be",
2197 },
2198 },
2199 ),
2200 ],
2201 ),
2202)
2203"#
2204 );
2205 let list = match msg.headers().subject() {
2206 Err(err) => panic!("Doh.\n{err:#}"),
2207 Ok(list) => list,
2208 };
2209 k9::snapshot!(
2210 list,
2211 r#"
2212Some(
2213 "Hello André",
2214)
2215"#
2216 );
2217 }
2218
2219 #[test]
2220 fn rfc2047_bogus() {
2221 let message = concat!(
2222 "From: =?US-OSCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2223 "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2224 "CC: =?ISO-8859-1?Q?Andr=E?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2225 "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?=\n",
2226 " =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2227 "\n\n"
2228 );
2229 let msg = MimePart::parse(message).unwrap();
2230
2231 k9::assert_equal!(
2234 msg.headers().from().unwrap().unwrap().0[0]
2235 .name
2236 .as_ref()
2237 .unwrap(),
2238 "=?US-OSCII?Q?Keith_Moore?="
2239 );
2240
2241 match &msg.headers().cc().unwrap().unwrap().0[0] {
2242 Address::Mailbox(mbox) => {
2243 k9::assert_equal!(mbox.name.as_ref().unwrap(), "Andr=E Pirard");
2247 }
2248 wat => panic!("should not have {wat:?}"),
2249 }
2250
2251 k9::assert_equal!(
2254 msg.headers().subject().unwrap().unwrap(),
2255 "Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?= u understand the example."
2256 );
2257 }
2258
2259 #[test]
2260 fn rfc2047() {
2261 let message = concat!(
2262 "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2263 "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2264 "CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2265 "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n",
2266 " =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2267 "\n\n"
2268 );
2269 let msg = MimePart::parse(message).unwrap();
2270 let list = match msg.headers().from() {
2271 Err(err) => panic!("Doh.\n{err:#}"),
2272 Ok(list) => list,
2273 };
2274 k9::snapshot!(
2275 list,
2276 r#"
2277Some(
2278 MailboxList(
2279 [
2280 Mailbox {
2281 name: Some(
2282 "Keith Moore",
2283 ),
2284 address: AddrSpec {
2285 local_part: "moore",
2286 domain: "cs.utk.edu",
2287 },
2288 },
2289 ],
2290 ),
2291)
2292"#
2293 );
2294
2295 let list = match msg.headers().to() {
2296 Err(err) => panic!("Doh.\n{err:#}"),
2297 Ok(list) => list,
2298 };
2299 k9::snapshot!(
2300 list,
2301 r#"
2302Some(
2303 AddressList(
2304 [
2305 Mailbox(
2306 Mailbox {
2307 name: Some(
2308 "Keld Jørn Simonsen",
2309 ),
2310 address: AddrSpec {
2311 local_part: "keld",
2312 domain: "dkuug.dk",
2313 },
2314 },
2315 ),
2316 ],
2317 ),
2318)
2319"#
2320 );
2321
2322 let list = match msg.headers().cc() {
2323 Err(err) => panic!("Doh.\n{err:#}"),
2324 Ok(list) => list,
2325 };
2326 k9::snapshot!(
2327 list,
2328 r#"
2329Some(
2330 AddressList(
2331 [
2332 Mailbox(
2333 Mailbox {
2334 name: Some(
2335 "André Pirard",
2336 ),
2337 address: AddrSpec {
2338 local_part: "PIRARD",
2339 domain: "vm1.ulg.ac.be",
2340 },
2341 },
2342 ),
2343 ],
2344 ),
2345)
2346"#
2347 );
2348 let list = match msg.headers().subject() {
2349 Err(err) => panic!("Doh.\n{err:#}"),
2350 Ok(list) => list,
2351 };
2352 k9::snapshot!(
2353 list,
2354 r#"
2355Some(
2356 "Hello If you can read this you understand the example.",
2357)
2358"#
2359 );
2360
2361 k9::snapshot!(
2362 msg.rebuild().unwrap().to_message_string(),
2363 r#"
2364Content-Type: text/plain;\r
2365\tcharset="us-ascii"\r
2366Content-Transfer-Encoding: quoted-printable\r
2367From: Keith Moore <moore@cs.utk.edu>\r
2368To: =?UTF-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r
2369Cc: =?UTF-8?q?Andr=C3=A9_Pirard?= <PIRARD@vm1.ulg.ac.be>\r
2370Subject: Hello If you can read this you understand the example.\r
2371\r
2372=0A\r
2373
2374"#
2375 );
2376 }
2377
2378 #[test]
2379 fn group_addresses() {
2380 let message = concat!(
2381 "To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;\n",
2382 "Cc: Undisclosed recipients:;\n",
2383 "\n\n\n"
2384 );
2385 let msg = MimePart::parse(message).unwrap();
2386 let list = match msg.headers().to() {
2387 Err(err) => panic!("Doh.\n{err:#}"),
2388 Ok(list) => list.unwrap(),
2389 };
2390
2391 k9::snapshot!(
2392 list.encode_value(),
2393 r#"
2394A Group:Ed Jones <c@a.test>,\r
2395\t<joe@where.test>,\r
2396\tJohn <jdoe@one.test>;
2397"#
2398 );
2399
2400 let round_trip = Header::new("To", list.clone());
2401 k9::assert_equal!(list, round_trip.as_address_list().unwrap());
2402
2403 k9::snapshot!(
2404 list,
2405 r#"
2406AddressList(
2407 [
2408 Group {
2409 name: "A Group",
2410 entries: MailboxList(
2411 [
2412 Mailbox {
2413 name: Some(
2414 "Ed Jones",
2415 ),
2416 address: AddrSpec {
2417 local_part: "c",
2418 domain: "a.test",
2419 },
2420 },
2421 Mailbox {
2422 name: None,
2423 address: AddrSpec {
2424 local_part: "joe",
2425 domain: "where.test",
2426 },
2427 },
2428 Mailbox {
2429 name: Some(
2430 "John",
2431 ),
2432 address: AddrSpec {
2433 local_part: "jdoe",
2434 domain: "one.test",
2435 },
2436 },
2437 ],
2438 ),
2439 },
2440 ],
2441)
2442"#
2443 );
2444
2445 let list = match msg.headers().cc() {
2446 Err(err) => panic!("Doh.\n{err:#}"),
2447 Ok(list) => list,
2448 };
2449 k9::snapshot!(
2450 list,
2451 r#"
2452Some(
2453 AddressList(
2454 [
2455 Group {
2456 name: "Undisclosed recipients",
2457 entries: MailboxList(
2458 [],
2459 ),
2460 },
2461 ],
2462 ),
2463)
2464"#
2465 );
2466 }
2467
2468 #[test]
2469 fn message_id() {
2470 let message = concat!(
2471 "Message-Id: <foo@example.com>\n",
2472 "References: <a@example.com> <b@example.com>\n",
2473 " <\"legacy\"@example.com>\n",
2474 " <literal@[127.0.0.1]>\n",
2475 "\n\n\n"
2476 );
2477 let msg = MimePart::parse(message).unwrap();
2478 let list = match msg.headers().message_id() {
2479 Err(err) => panic!("Doh.\n{err:#}"),
2480 Ok(list) => list,
2481 };
2482 k9::snapshot!(
2483 list,
2484 r#"
2485Some(
2486 MessageID(
2487 "foo@example.com",
2488 ),
2489)
2490"#
2491 );
2492
2493 let list = match msg.headers().references() {
2494 Err(err) => panic!("Doh.\n{err:#}"),
2495 Ok(list) => list,
2496 };
2497 k9::snapshot!(
2498 list,
2499 r#"
2500Some(
2501 [
2502 MessageID(
2503 "a@example.com",
2504 ),
2505 MessageID(
2506 "b@example.com",
2507 ),
2508 MessageID(
2509 "legacy@example.com",
2510 ),
2511 MessageID(
2512 "literal@[127.0.0.1]",
2513 ),
2514 ],
2515)
2516"#
2517 );
2518 }
2519
2520 #[test]
2521 fn content_type() {
2522 let message = "Content-Type: text/plain\n\n\n\n";
2523 let msg = MimePart::parse(message).unwrap();
2524 let params = match msg.headers().content_type() {
2525 Err(err) => panic!("Doh.\n{err:#}"),
2526 Ok(params) => params,
2527 };
2528 k9::snapshot!(
2529 params,
2530 r#"
2531Some(
2532 MimeParameters {
2533 value: "text/plain",
2534 parameters: [],
2535 },
2536)
2537"#
2538 );
2539
2540 let message = "Content-Type: text/plain; charset=us-ascii\n\n\n\n";
2541 let msg = MimePart::parse(message).unwrap();
2542 let params = match msg.headers().content_type() {
2543 Err(err) => panic!("Doh.\n{err:#}"),
2544 Ok(params) => params.unwrap(),
2545 };
2546
2547 k9::snapshot!(
2548 params.get("charset"),
2549 r#"
2550Some(
2551 "us-ascii",
2552)
2553"#
2554 );
2555 k9::snapshot!(
2556 params,
2557 r#"
2558MimeParameters {
2559 value: "text/plain",
2560 parameters: [
2561 MimeParameter {
2562 name: "charset",
2563 section: None,
2564 mime_charset: None,
2565 mime_language: None,
2566 uses_encoding: false,
2567 value: "us-ascii",
2568 },
2569 ],
2570}
2571"#
2572 );
2573
2574 let message = "Content-Type: text/plain; charset=\"us-ascii\"\n\n\n\n";
2575 let msg = MimePart::parse(message).unwrap();
2576 let params = match msg.headers().content_type() {
2577 Err(err) => panic!("Doh.\n{err:#}"),
2578 Ok(params) => params,
2579 };
2580 k9::snapshot!(
2581 params,
2582 r#"
2583Some(
2584 MimeParameters {
2585 value: "text/plain",
2586 parameters: [
2587 MimeParameter {
2588 name: "charset",
2589 section: None,
2590 mime_charset: None,
2591 mime_language: None,
2592 uses_encoding: false,
2593 value: "us-ascii",
2594 },
2595 ],
2596 },
2597)
2598"#
2599 );
2600 }
2601
2602 #[test]
2603 fn content_type_rfc2231() {
2604 let message = concat!(
2607 "Content-Type: application/x-stuff;\n",
2608 "\ttitle*0*=us-ascii'en'This%20is%20even%20more%20;\n",
2609 "\ttitle*1*=%2A%2A%2Afun%2A%2A%2A%20;\n",
2610 "\ttitle*2=\"isn't it!\"\n",
2611 "\n\n\n"
2612 );
2613 let msg = MimePart::parse(message).unwrap();
2614 let mut params = match msg.headers().content_type() {
2615 Err(err) => panic!("Doh.\n{err:#}"),
2616 Ok(params) => params.unwrap(),
2617 };
2618
2619 let original_title = params.get("title");
2620 k9::snapshot!(
2621 &original_title,
2622 r#"
2623Some(
2624 "This is even more ***fun*** isn't it!",
2625)
2626"#
2627 );
2628
2629 k9::snapshot!(
2630 ¶ms,
2631 r#"
2632MimeParameters {
2633 value: "application/x-stuff",
2634 parameters: [
2635 MimeParameter {
2636 name: "title",
2637 section: Some(
2638 0,
2639 ),
2640 mime_charset: Some(
2641 "us-ascii",
2642 ),
2643 mime_language: Some(
2644 "en",
2645 ),
2646 uses_encoding: true,
2647 value: "This%20is%20even%20more%20",
2648 },
2649 MimeParameter {
2650 name: "title",
2651 section: Some(
2652 1,
2653 ),
2654 mime_charset: None,
2655 mime_language: None,
2656 uses_encoding: true,
2657 value: "%2A%2A%2Afun%2A%2A%2A%20",
2658 },
2659 MimeParameter {
2660 name: "title",
2661 section: Some(
2662 2,
2663 ),
2664 mime_charset: None,
2665 mime_language: None,
2666 uses_encoding: false,
2667 value: "isn't it!",
2668 },
2669 ],
2670}
2671"#
2672 );
2673
2674 k9::snapshot!(
2675 params.encode_value(),
2676 r#"
2677application/x-stuff;\r
2678\ttitle="This is even more ***fun*** isn't it!"
2679"#
2680 );
2681
2682 params.set("foo", "bar 💩");
2683
2684 params.set(
2685 "long",
2686 "this is some text that should wrap because \
2687 it should be a good bit longer than our target maximum \
2688 length for this sort of thing, and hopefully we see at \
2689 least three lines produced as a result of setting \
2690 this value in this way",
2691 );
2692
2693 params.set(
2694 "longernnamethananyoneshouldreallyuse",
2695 "this is some text that should wrap because \
2696 it should be a good bit longer than our target maximum \
2697 length for this sort of thing, and hopefully we see at \
2698 least three lines produced as a result of setting \
2699 this value in this way",
2700 );
2701
2702 k9::snapshot!(
2703 params.encode_value(),
2704 r#"
2705application/x-stuff;\r
2706\tfoo*=UTF-8''bar%20%F0%9F%92%A9;\r
2707\tlong*0="this is some text that should wrap because it should be a good bi";\r
2708\tlong*1="t longer than our target maximum length for this sort of thing, a";\r
2709\tlong*2="nd hopefully we see at least three lines produced as a result of ";\r
2710\tlong*3="setting this value in this way";\r
2711\tlongernnamethananyoneshouldreallyuse*0="this is some text that should wra";\r
2712\tlongernnamethananyoneshouldreallyuse*1="p because it should be a good bit";\r
2713\tlongernnamethananyoneshouldreallyuse*2=" longer than our target maximum l";\r
2714\tlongernnamethananyoneshouldreallyuse*3="ength for this sort of thing, and";\r
2715\tlongernnamethananyoneshouldreallyuse*4=" hopefully we see at least three ";\r
2716\tlongernnamethananyoneshouldreallyuse*5="lines produced as a result of set";\r
2717\tlongernnamethananyoneshouldreallyuse*6="ting this value in this way";\r
2718\ttitle="This is even more ***fun*** isn't it!"
2719"#
2720 );
2721 }
2722
2723 #[test]
2725 fn authentication_results_b_2() {
2726 let ar = Header::with_name_value("Authentication-Results", "example.org 1; none");
2727 let ar = ar.as_authentication_results().unwrap();
2728 k9::snapshot!(
2729 &ar,
2730 r#"
2731AuthenticationResults {
2732 serv_id: "example.org",
2733 version: Some(
2734 1,
2735 ),
2736 results: [],
2737}
2738"#
2739 );
2740
2741 k9::snapshot!(ar.encode_value(), "example.org 1; none");
2742 }
2743
2744 #[test]
2746 fn authentication_results_b_3() {
2747 let ar = Header::with_name_value(
2748 "Authentication-Results",
2749 "example.com; spf=pass smtp.mailfrom=example.net",
2750 );
2751 k9::snapshot!(
2752 ar.as_authentication_results(),
2753 r#"
2754Ok(
2755 AuthenticationResults {
2756 serv_id: "example.com",
2757 version: None,
2758 results: [
2759 AuthenticationResult {
2760 method: "spf",
2761 method_version: None,
2762 result: "pass",
2763 reason: None,
2764 props: {
2765 "smtp.mailfrom": "example.net",
2766 },
2767 },
2768 ],
2769 },
2770)
2771"#
2772 );
2773 }
2774
2775 #[test]
2777 fn authentication_results_b_4() {
2778 let ar = Header::with_name_value(
2779 "Authentication-Results",
2780 concat!(
2781 "example.com;\n",
2782 "\tauth=pass (cram-md5) smtp.auth=sender@example.net;\n",
2783 "\tspf=pass smtp.mailfrom=example.net"
2784 ),
2785 );
2786 k9::snapshot!(
2787 ar.as_authentication_results(),
2788 r#"
2789Ok(
2790 AuthenticationResults {
2791 serv_id: "example.com",
2792 version: None,
2793 results: [
2794 AuthenticationResult {
2795 method: "auth",
2796 method_version: None,
2797 result: "pass",
2798 reason: None,
2799 props: {
2800 "smtp.auth": "sender@example.net",
2801 },
2802 },
2803 AuthenticationResult {
2804 method: "spf",
2805 method_version: None,
2806 result: "pass",
2807 reason: None,
2808 props: {
2809 "smtp.mailfrom": "example.net",
2810 },
2811 },
2812 ],
2813 },
2814)
2815"#
2816 );
2817
2818 let ar = Header::with_name_value(
2819 "Authentication-Results",
2820 "example.com; iprev=pass\n\tpolicy.iprev=192.0.2.200",
2821 );
2822 k9::snapshot!(
2823 ar.as_authentication_results(),
2824 r#"
2825Ok(
2826 AuthenticationResults {
2827 serv_id: "example.com",
2828 version: None,
2829 results: [
2830 AuthenticationResult {
2831 method: "iprev",
2832 method_version: None,
2833 result: "pass",
2834 reason: None,
2835 props: {
2836 "policy.iprev": "192.0.2.200",
2837 },
2838 },
2839 ],
2840 },
2841)
2842"#
2843 );
2844 }
2845
2846 #[test]
2848 fn authentication_results_b_5() {
2849 let ar = Header::with_name_value(
2850 "Authentication-Results",
2851 "example.com;\n\tdkim=pass (good signature) header.d=example.com",
2852 );
2853 k9::snapshot!(
2854 ar.as_authentication_results(),
2855 r#"
2856Ok(
2857 AuthenticationResults {
2858 serv_id: "example.com",
2859 version: None,
2860 results: [
2861 AuthenticationResult {
2862 method: "dkim",
2863 method_version: None,
2864 result: "pass",
2865 reason: None,
2866 props: {
2867 "header.d": "example.com",
2868 },
2869 },
2870 ],
2871 },
2872)
2873"#
2874 );
2875
2876 let ar = Header::with_name_value(
2877 "Authentication-Results",
2878 "example.com;\n\tauth=pass (cram-md5) smtp.auth=sender@example.com;\n\tspf=fail smtp.mailfrom=example.com"
2879 );
2880 let ar = ar.as_authentication_results().unwrap();
2881 k9::snapshot!(
2882 &ar,
2883 r#"
2884AuthenticationResults {
2885 serv_id: "example.com",
2886 version: None,
2887 results: [
2888 AuthenticationResult {
2889 method: "auth",
2890 method_version: None,
2891 result: "pass",
2892 reason: None,
2893 props: {
2894 "smtp.auth": "sender@example.com",
2895 },
2896 },
2897 AuthenticationResult {
2898 method: "spf",
2899 method_version: None,
2900 result: "fail",
2901 reason: None,
2902 props: {
2903 "smtp.mailfrom": "example.com",
2904 },
2905 },
2906 ],
2907}
2908"#
2909 );
2910
2911 k9::snapshot!(
2912 ar.encode_value(),
2913 r#"
2914example.com;\r
2915\tauth=pass\r
2916\tsmtp.auth=sender@example.com;\r
2917\tspf=fail\r
2918\tsmtp.mailfrom=example.com
2919"#
2920 );
2921 }
2922
2923 #[test]
2925 fn authentication_results_b_6() {
2926 let ar = Header::with_name_value(
2927 "Authentication-Results",
2928 concat!(
2929 "example.com;\n",
2930 "\tdkim=pass reason=\"good signature\"\n",
2931 "\theader.i=@mail-router.example.net;\n",
2932 "\tdkim=fail reason=\"bad signature\"\n",
2933 "\theader.i=@newyork.example.com"
2934 ),
2935 );
2936 let ar = match ar.as_authentication_results() {
2937 Err(err) => panic!("\n{err}"),
2938 Ok(ar) => ar,
2939 };
2940
2941 k9::snapshot!(
2942 &ar,
2943 r#"
2944AuthenticationResults {
2945 serv_id: "example.com",
2946 version: None,
2947 results: [
2948 AuthenticationResult {
2949 method: "dkim",
2950 method_version: None,
2951 result: "pass",
2952 reason: Some(
2953 "good signature",
2954 ),
2955 props: {
2956 "header.i": "@mail-router.example.net",
2957 },
2958 },
2959 AuthenticationResult {
2960 method: "dkim",
2961 method_version: None,
2962 result: "fail",
2963 reason: Some(
2964 "bad signature",
2965 ),
2966 props: {
2967 "header.i": "@newyork.example.com",
2968 },
2969 },
2970 ],
2971}
2972"#
2973 );
2974
2975 k9::snapshot!(
2976 ar.encode_value(),
2977 r#"
2978example.com;\r
2979\tdkim=pass reason="good signature"\r
2980\theader.i=@mail-router.example.net;\r
2981\tdkim=fail reason="bad signature"\r
2982\theader.i=@newyork.example.com
2983"#
2984 );
2985
2986 let ar = Header::with_name_value(
2987 "Authentication-Results",
2988 concat!(
2989 "example.net;\n",
2990 "\tdkim=pass (good signature) header.i=@newyork.example.com"
2991 ),
2992 );
2993 let ar = match ar.as_authentication_results() {
2994 Err(err) => panic!("\n{err}"),
2995 Ok(ar) => ar,
2996 };
2997
2998 k9::snapshot!(
2999 &ar,
3000 r#"
3001AuthenticationResults {
3002 serv_id: "example.net",
3003 version: None,
3004 results: [
3005 AuthenticationResult {
3006 method: "dkim",
3007 method_version: None,
3008 result: "pass",
3009 reason: None,
3010 props: {
3011 "header.i": "@newyork.example.com",
3012 },
3013 },
3014 ],
3015}
3016"#
3017 );
3018
3019 k9::snapshot!(
3020 ar.encode_value(),
3021 r#"
3022example.net;\r
3023\tdkim=pass\r
3024\theader.i=@newyork.example.com
3025"#
3026 );
3027 }
3028
3029 #[test]
3031 fn authentication_results_b_7() {
3032 let ar = Header::with_name_value(
3033 "Authentication-Results",
3034 concat!(
3035 "foo.example.net (foobar) 1 (baz);\n",
3036 "\tdkim (Because I like it) / 1 (One yay) = (wait for it) fail\n",
3037 "\tpolicy (A dot can go here) . (like that) expired\n",
3038 "\t(this surprised me) = (as I wasn't expecting it) 1362471462"
3039 ),
3040 );
3041 let ar = match ar.as_authentication_results() {
3042 Err(err) => panic!("\n{err}"),
3043 Ok(ar) => ar,
3044 };
3045
3046 k9::snapshot!(
3047 &ar,
3048 r#"
3049AuthenticationResults {
3050 serv_id: "foo.example.net",
3051 version: Some(
3052 1,
3053 ),
3054 results: [
3055 AuthenticationResult {
3056 method: "dkim",
3057 method_version: Some(
3058 1,
3059 ),
3060 result: "fail",
3061 reason: None,
3062 props: {
3063 "policy.expired": "1362471462",
3064 },
3065 },
3066 ],
3067}
3068"#
3069 );
3070
3071 k9::snapshot!(
3072 ar.encode_value(),
3073 r#"
3074foo.example.net 1;\r
3075\tdkim/1=fail\r
3076\tpolicy.expired=1362471462
3077"#
3078 );
3079 }
3080}