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