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::{
13 explain_nom, make_context_error, make_span, tag, utf8_non_ascii, IResult, ParseError, Span,
14};
15use serde::{Deserialize, Serialize};
16use serde_with::{serde_as, DeserializeAs, SerializeAs};
17use std::collections::BTreeMap;
18use std::fmt::Debug;
19
20pub struct BStringUtf8;
24
25impl SerializeAs<BString> for BStringUtf8 {
26 fn serialize_as<S>(value: &BString, serializer: S) -> std::result::Result<S::Ok, S::Error>
27 where
28 S: serde::Serializer,
29 {
30 match std::str::from_utf8(value.as_bytes()) {
31 Ok(s) => serializer.serialize_str(s),
32 Err(_) => value.serialize(serializer),
33 }
34 }
35}
36
37impl<'de> DeserializeAs<'de, BString> for BStringUtf8 {
38 fn deserialize_as<D>(deserializer: D) -> std::result::Result<BString, D::Error>
39 where
40 D: serde::Deserializer<'de>,
41 {
42 BString::deserialize(deserializer)
43 }
44}
45
46impl MailParsingError {
47 pub fn from_nom(input: Span, err: nom::Err<ParseError<Span<'_>>>) -> Self {
48 MailParsingError::HeaderParse(explain_nom(input, err))
49 }
50}
51
52fn is_ctl(c: u8) -> bool {
54 match c {
55 b'\x00'..=b'\x1f' | b'\x7f' => true,
56 _ => false,
57 }
58}
59
60fn not_angle(c: u8) -> bool {
61 match c {
62 b'<' | b'>' => false,
63 _ => true,
64 }
65}
66
67fn is_char(c: u8) -> bool {
69 match c {
70 0x01..=0x7f => true,
71 _ => false,
72 }
73}
74
75fn is_especial(c: u8) -> bool {
76 match c {
77 b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'/' | b'[' | b']' | b'?'
78 | b'.' | b'=' => true,
79 _ => false,
80 }
81}
82
83fn is_token(c: u8) -> bool {
84 is_char(c) && c != b' ' && !is_especial(c) && !is_ctl(c)
85}
86
87fn is_vchar_ascii(c: u8) -> bool {
89 (0x21..=0x7e).contains(&c)
90}
91
92fn is_atext_ascii(c: u8) -> bool {
93 match c {
94 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'/' | b'=' | b'?'
95 | b'^' | b'_' | b'`' | b'{' | b'|' | b'}' | b'~' => true,
96 c => c.is_ascii_alphanumeric(),
97 }
98}
99
100fn is_atext(c: u8) -> bool {
104 is_atext_ascii(c) || c >= 0x80
105}
106
107fn atext(input: Span) -> IResult<Span, Span> {
108 context(
109 "atext",
110 recognize(many1(alt((take_while1(is_atext_ascii), utf8_non_ascii)))),
111 )
112 .parse(input)
113}
114
115fn is_obs_no_ws_ctl(c: u8) -> bool {
116 match c {
117 0x01..=0x08 | 0x0b..=0x0c | 0x0e..=0x1f | 0x7f => true,
118 _ => false,
119 }
120}
121
122fn is_obs_ctext(c: u8) -> bool {
123 is_obs_no_ws_ctl(c)
124}
125
126fn is_ctext_ascii(c: u8) -> bool {
128 match c {
129 0x21..=0x27 | 0x2a..=0x5b | 0x5d..=0x7e => true,
130 c => is_obs_ctext(c),
131 }
132}
133
134fn is_dtext_ascii(c: u8) -> bool {
137 match c {
138 0x21..=0x5a | 0x5e..=0x7e => true,
139 c => is_obs_no_ws_ctl(c),
140 }
141}
142
143fn is_qtext_ascii(c: u8) -> bool {
146 match c {
147 0x21 | 0x23..=0x5b | 0x5d..=0x7e => true,
148 c => is_obs_no_ws_ctl(c),
149 }
150}
151
152fn is_qtext(c: u8) -> bool {
156 is_qtext_ascii(c) || c >= 0x80
157}
158
159fn is_tspecial(c: u8) -> bool {
160 match c {
161 b'(' | b')' | b'<' | b'>' | b'@' | b',' | b';' | b':' | b'\\' | b'"' | b'/' | b'['
162 | b']' | b'?' | b'=' => true,
163 _ => false,
164 }
165}
166
167fn is_attribute_char(c: u8) -> bool {
168 match c {
169 b' ' | b'*' | b'\'' | b'%' => false,
170 _ => is_char(c) && !is_ctl(c) && !is_tspecial(c),
171 }
172}
173
174fn wsp(input: Span) -> IResult<Span, Span> {
175 context("wsp", take_while1(|c| c == b' ' || c == b'\t')).parse(input)
176}
177
178fn newline(input: Span) -> IResult<Span, Span> {
179 context("newline", recognize(preceded(opt(tag("\r")), tag("\n")))).parse(input)
180}
181
182fn fws(input: Span) -> IResult<Span, Span> {
184 context(
185 "fws",
186 alt((
187 recognize(preceded(many0(preceded(many0(wsp), newline)), many1(wsp))),
188 obs_fws,
189 )),
190 )
191 .parse(input)
192}
193
194fn obs_fws(input: Span) -> IResult<Span, Span> {
196 context(
197 "obs_fws",
198 recognize(preceded(many1(wsp), preceded(newline, many1(wsp)))),
199 )
200 .parse(input)
201}
202
203fn mailbox_list(input: Span) -> IResult<Span, MailboxList> {
205 let (loc, mailboxes) = context(
206 "mailbox_list",
207 alt((separated_list1(tag(","), mailbox), obs_mbox_list)),
208 )
209 .parse(input)?;
210 Ok((loc, MailboxList(mailboxes)))
211}
212
213fn obs_mbox_list(input: Span) -> IResult<Span, Vec<Mailbox>> {
215 let (loc, entries) = context(
216 "obs_mbox_list",
217 many1(preceded(
218 many0(preceded(opt(cfws), tag(","))),
219 (
220 mailbox,
221 many0(preceded(
222 tag(","),
223 alt((map(mailbox, Some), map(cfws, |_| None))),
224 )),
225 ),
226 )),
227 )
228 .parse(input)?;
229
230 let mut result: Vec<Mailbox> = vec![];
231
232 for (first, boxes) in entries {
233 result.push(first);
234 for b in boxes {
235 if let Some(m) = b {
236 result.push(m);
237 }
238 }
239 }
240
241 Ok((loc, result))
242}
243
244fn mailbox(input: Span) -> IResult<Span, Mailbox> {
246 if let Ok(res) = name_addr(input) {
247 Ok(res)
248 } else {
249 let (loc, address) = context("mailbox", addr_spec).parse(input)?;
250 Ok((
251 loc,
252 Mailbox {
253 name: None,
254 address,
255 },
256 ))
257 }
258}
259
260fn address_list(input: Span) -> IResult<Span, AddressList> {
262 context(
263 "address_list",
264 alt((
265 map(separated_list1(tag(","), address), AddressList),
266 obs_address_list,
267 )),
268 )
269 .parse(input)
270}
271
272fn obs_address_list(input: Span) -> IResult<Span, AddressList> {
274 let (loc, entries) = context(
275 "obs_address_list",
276 many1(preceded(
277 many0(preceded(opt(cfws), tag(","))),
278 (
279 address,
280 many0(preceded(
281 tag(","),
282 alt((map(address, Some), map(cfws, |_| None))),
283 )),
284 ),
285 )),
286 )
287 .parse(input)?;
288
289 let mut result: Vec<Address> = vec![];
290
291 for (first, boxes) in entries {
292 result.push(first);
293 for b in boxes {
294 if let Some(m) = b {
295 result.push(m);
296 }
297 }
298 }
299
300 Ok((loc, AddressList(result)))
301}
302
303fn address(input: Span) -> IResult<Span, Address> {
305 context("address", alt((map(mailbox, Address::Mailbox), group))).parse(input)
306}
307
308fn group(input: Span) -> IResult<Span, Address> {
310 let (loc, (name, _, group_list, _)) = context(
311 "group",
312 terminated(
313 (display_name, tag(":"), opt(group_list), tag(";")),
314 opt(cfws),
315 ),
316 )
317 .parse(input)?;
318 Ok((
319 loc,
320 Address::Group {
321 name,
322 entries: group_list.unwrap_or_else(|| MailboxList(vec![])),
323 },
324 ))
325}
326
327fn group_list(input: Span) -> IResult<Span, MailboxList> {
329 context(
330 "group_list",
331 alt((
332 mailbox_list,
333 map(cfws, |_| MailboxList(vec![])),
334 obs_group_list,
335 )),
336 )
337 .parse(input)
338}
339
340fn obs_group_list(input: Span) -> IResult<Span, MailboxList> {
342 context(
343 "obs_group_list",
344 map(
345 terminated(many1(preceded(opt(cfws), tag(","))), opt(cfws)),
346 |_| MailboxList(vec![]),
347 ),
348 )
349 .parse(input)
350}
351
352fn name_addr(input: Span) -> IResult<Span, Mailbox> {
354 context(
355 "name_addr",
356 map((opt(display_name), angle_addr), |(name, address)| Mailbox {
357 name,
358 address,
359 }),
360 )
361 .parse(input)
362}
363
364fn display_name(input: Span) -> IResult<Span, String> {
366 context("display_name", phrase).parse(input)
367}
368
369fn phrase(input: Span) -> IResult<Span, String> {
372 let (loc, (a, b)): (Span, (BString, Vec<Option<BString>>)) = context(
373 "phrase",
374 (
375 alt((encoded_word, word)),
376 many0(alt((
377 map(cfws, |_| None),
378 map(encoded_word, Option::Some),
379 map(word, Option::Some),
380 map(tag("."), |_dot| Some(BString::from("."))),
381 ))),
382 ),
383 )
384 .parse(input)?;
385 let mut result = a;
386 for item in b {
387 if let Some(item) = item {
388 result.push(b' ');
389 result.push_str(item);
390 }
391 }
392 Ok((
395 loc,
396 String::from_utf8(result.into())
397 .expect("phrase sub-parsers should only produce valid UTF-8"),
398 ))
399}
400
401fn angle_addr(input: Span) -> IResult<Span, AddrSpec> {
403 context(
404 "angle_addr",
405 alt((
406 delimited(
407 opt(cfws),
408 delimited(tag("<"), addr_spec, tag(">")),
409 opt(cfws),
410 ),
411 obs_angle_addr,
412 )),
413 )
414 .parse(input)
415}
416
417fn obs_angle_addr(input: Span) -> IResult<Span, AddrSpec> {
419 context(
420 "obs_angle_addr",
421 delimited(
422 opt(cfws),
423 delimited(tag("<"), preceded(obs_route, addr_spec), tag(">")),
424 opt(cfws),
425 ),
426 )
427 .parse(input)
428}
429
430fn obs_route(input: Span) -> IResult<Span, Span> {
433 context(
434 "obs_route",
435 recognize(terminated(
436 (
437 many0(alt((cfws, recognize(tag(","))))),
438 recognize(tag("@")),
439 recognize(domain),
440 many0((tag(","), opt(cfws), opt((tag("@"), domain)))),
441 ),
442 tag(":"),
443 )),
444 )
445 .parse(input)
446}
447
448fn addr_spec(input: Span) -> IResult<Span, AddrSpec> {
450 let (loc, (local_part, domain)) =
451 context("addr_spec", separated_pair(local_part, tag("@"), domain)).parse(input)?;
452
453 let to_string = |b: BString| -> String {
456 String::from_utf8(b.into())
457 .expect("local_part/domain parsers should only produce valid UTF-8")
458 };
459
460 Ok((
461 loc,
462 AddrSpec {
463 local_part: to_string(local_part),
464 domain: to_string(domain),
465 },
466 ))
467}
468
469fn parse_with<'a, R, F>(text: &'a [u8], parser: F) -> Result<R>
470where
471 F: Fn(Span<'a>) -> IResult<'a, Span<'a>, R>,
472{
473 let input = make_span(text);
474 let (_, result) = all_consuming(parser)
475 .parse(input)
476 .map_err(|err| MailParsingError::from_nom(input, err))?;
477 Ok(result)
478}
479
480#[cfg(test)]
481#[test]
482fn test_addr_spec() {
483 k9::snapshot!(
484 parse_with("darth.vader@a.galaxy.far.far.away".as_bytes(), addr_spec),
485 r#"
486Ok(
487 AddrSpec {
488 local_part: "darth.vader",
489 domain: "a.galaxy.far.far.away",
490 },
491)
492"#
493 );
494
495 k9::snapshot!(
496 parse_with(
497 "\"darth.vader\"@a.galaxy.far.far.away".as_bytes(),
498 addr_spec
499 ),
500 r#"
501Ok(
502 AddrSpec {
503 local_part: "darth.vader",
504 domain: "a.galaxy.far.far.away",
505 },
506)
507"#
508 );
509
510 k9::snapshot!(
511 parse_with(
512 "\"darth\".vader@a.galaxy.far.far.away".as_bytes(),
513 addr_spec
514 ),
515 r#"
516Ok(
517 AddrSpec {
518 local_part: "darth.vader",
519 domain: "a.galaxy.far.far.away",
520 },
521)
522"#
523 );
524
525 k9::snapshot!(
526 parse_with("a@[127.0.0.1]".as_bytes(), addr_spec),
527 r#"
528Ok(
529 AddrSpec {
530 local_part: "a",
531 domain: "[127.0.0.1]",
532 },
533)
534"#
535 );
536
537 k9::snapshot!(
538 parse_with("a@[IPv6::1]".as_bytes(), addr_spec),
539 r#"
540Ok(
541 AddrSpec {
542 local_part: "a",
543 domain: "[IPv6::1]",
544 },
545)
546"#
547 );
548}
549
550#[cfg(test)]
551#[test]
552fn test_obs_local_part_in_addr_spec() {
553 k9::snapshot!(
557 parse_with(r#""first".last@example.com"#.as_bytes(), addr_spec),
558 r#"
559Ok(
560 AddrSpec {
561 local_part: "first.last",
562 domain: "example.com",
563 },
564)
565"#
566 );
567 k9::snapshot!(
568 parse_with(r#"first."last"@example.com"#.as_bytes(), addr_spec),
569 r#"
570Ok(
571 AddrSpec {
572 local_part: "first.last",
573 domain: "example.com",
574 },
575)
576"#
577 );
578 k9::snapshot!(
579 parse_with(r#""first"."last"@example.com"#.as_bytes(), addr_spec),
580 r#"
581Ok(
582 AddrSpec {
583 local_part: "first.last",
584 domain: "example.com",
585 },
586)
587"#
588 );
589}
590
591#[cfg(test)]
592#[test]
593fn test_obs_local_part_encode_roundtrip() {
594 let addr = AddrSpec::new("first.last", "example.com");
599 k9::assert_equal!(addr.encode_value(), "first.last@example.com");
600
601 let addr = AddrSpec::new("first second.last", "example.com");
603 k9::assert_equal!(addr.encode_value(), r#""first second.last"@example.com"#);
604
605 let addr = AddrSpec::new("first\".last", "example.com");
607 k9::assert_equal!(addr.encode_value(), r#""first\".last"@example.com"#);
608}
609
610#[cfg(test)]
611#[test]
612fn test_obs_local_part_with_special_chars() {
613 k9::snapshot!(
616 parse_with(r#""hello world".user@example.com"#.as_bytes(), addr_spec),
617 r#"
618Ok(
619 AddrSpec {
620 local_part: "hello world.user",
621 domain: "example.com",
622 },
623)
624"#
625 );
626 let addr = AddrSpec::new("hello world.user", "example.com");
628 k9::assert_equal!(addr.encode_value(), r#""hello world.user"@example.com"#);
629}
630
631#[cfg(test)]
632#[test]
633fn test_utf8_non_ascii_in_local_part() {
634 k9::snapshot!(
636 parse_with("用户@example.com".as_bytes(), addr_spec),
637 r#"
638Ok(
639 AddrSpec {
640 local_part: "用户",
641 domain: "example.com",
642 },
643)
644"#
645 );
646 k9::snapshot!(
647 parse_with("münchen@example.com".as_bytes(), addr_spec),
648 r#"
649Ok(
650 AddrSpec {
651 local_part: "münchen",
652 domain: "example.com",
653 },
654)
655"#
656 );
657}
658
659#[cfg(test)]
660#[test]
661fn test_utf8_non_ascii_in_domain() {
662 k9::snapshot!(
664 parse_with("user@例え.jp".as_bytes(), addr_spec),
665 r#"
666Ok(
667 AddrSpec {
668 local_part: "user",
669 domain: "例え.jp",
670 },
671)
672"#
673 );
674}
675
676#[cfg(test)]
677#[test]
678fn test_quoted_pair_non_ascii() {
679 k9::snapshot!(
681 parse_with(r#""\München"@example.com"#.as_bytes(), addr_spec),
682 r#"
683Ok(
684 AddrSpec {
685 local_part: "München",
686 domain: "example.com",
687 },
688)
689"#
690 );
691}
692
693#[cfg(test)]
694#[test]
695fn test_invalid_utf8_rejected() {
696 let input = b"user\x80@example.com";
699 parse_with(input, addr_spec).unwrap_err();
700
701 let input = b"user\xC0\xAF@example.com";
703 parse_with(input, addr_spec).unwrap_err();
704
705 let input = b"user\xC3@example.com";
707 parse_with(input, addr_spec).unwrap_err();
708
709 let input = b"\"user\x80\"@example.com";
711 parse_with(input, addr_spec).unwrap_err();
712
713 let input = b"(comment\x80) user@example.com";
715 parse_with(input, mailbox).unwrap_err();
716}
717
718fn atom(input: Span) -> IResult<Span, BString> {
720 let (loc, text) = context("atom", delimited(opt(cfws), atext, opt(cfws))).parse(input)?;
721 Ok((loc, (*text).into()))
722}
723
724fn word(input: Span) -> IResult<Span, BString> {
726 context("word", alt((atom, quoted_string))).parse(input)
727}
728
729fn obs_local_part(input: Span) -> IResult<Span, BString> {
731 let (loc, (word, dotted_words)) =
732 context("obs_local_part", (word, many0((tag("."), word)))).parse(input)?;
733 let mut result = word;
734
735 for (_dot, w) in dotted_words {
736 result.push(b'.');
737 result.push_str(&w);
738 }
739
740 Ok((loc, result))
741}
742
743fn local_part(input: Span) -> IResult<Span, BString> {
751 context("local_part", alt((obs_local_part, dot_atom, quoted_string))).parse(input)
752}
753
754fn domain(input: Span) -> IResult<Span, BString> {
756 context("domain", alt((dot_atom, domain_literal, obs_domain))).parse(input)
757}
758
759fn obs_domain(input: Span) -> IResult<Span, BString> {
761 let (loc, (atom, dotted_atoms)) =
762 context("obs_domain", (atom, many0((tag("."), atom)))).parse(input)?;
763 let mut result = atom;
764
765 for (_dot, w) in dotted_atoms {
766 result.push(b'.');
767 result.push_str(&w);
768 }
769
770 Ok((loc, result))
771}
772
773fn domain_literal(input: Span) -> IResult<Span, BString> {
775 let (loc, (bits, trailer)) = context(
776 "domain_literal",
777 delimited(
778 opt(cfws),
779 delimited(
780 tag("["),
781 (
782 many0((
783 opt(fws),
784 alt((
785 take_while_m_n(1, 1, is_dtext_ascii),
786 utf8_non_ascii,
787 quoted_pair,
788 )),
789 )),
790 opt(fws),
791 ),
792 tag("]"),
793 ),
794 opt(cfws),
795 ),
796 )
797 .parse(input)?;
798
799 let mut result = BString::default();
800 result.push(b'[');
801 for (a, b) in bits {
802 if let Some(a) = a {
803 result.push_str(&a);
804 }
805 result.push_str(b);
806 }
807 if let Some(t) = trailer {
808 result.push_str(&t);
809 }
810 result.push(b']');
811 Ok((loc, result))
812}
813
814fn dot_atom_text(input: Span) -> IResult<Span, BString> {
816 let (loc, (a, b)) =
817 context("dot_atom_text", (atext, many0(preceded(tag("."), atext)))).parse(input)?;
818 let mut result: BString = (*a).into();
819 for item in b {
820 result.push(b'.');
821 result.push_str(&item);
822 }
823
824 Ok((loc, result))
825}
826
827fn dot_atom(input: Span) -> IResult<Span, BString> {
829 context("dot_atom", delimited(opt(cfws), dot_atom_text, opt(cfws))).parse(input)
830}
831
832#[cfg(test)]
833#[test]
834fn test_dot_atom() {
835 k9::snapshot!(
836 parse_with("hello".as_bytes(), dot_atom),
837 r#"
838Ok(
839 "hello",
840)
841"#
842 );
843
844 k9::snapshot!(
845 parse_with("hello.there".as_bytes(), dot_atom),
846 r#"
847Ok(
848 "hello.there",
849)
850"#
851 );
852
853 k9::snapshot!(
854 parse_with("hello.".as_bytes(), dot_atom),
855 r#"
856Err(
857 HeaderParse(
858 "Error at line 1, in Eof:
859hello.
860 ^
861
862",
863 ),
864)
865"#
866 );
867
868 k9::snapshot!(
869 parse_with("(wat)hello".as_bytes(), dot_atom),
870 r#"
871Ok(
872 "hello",
873)
874"#
875 );
876}
877
878fn cfws(input: Span) -> IResult<Span, Span> {
880 context(
881 "cfws",
882 recognize(alt((
883 recognize((many1((opt(fws), comment)), opt(fws))),
884 fws,
885 ))),
886 )
887 .parse(input)
888}
889
890fn comment(input: Span) -> IResult<Span, Span> {
892 context(
893 "comment",
894 recognize((tag("("), many0((opt(fws), ccontent)), opt(fws), tag(")"))),
895 )
896 .parse(input)
897}
898
899#[cfg(test)]
900#[test]
901fn test_comment() {
902 k9::snapshot!(
903 BStr::new(&parse_with("(wat)".as_bytes(), comment).unwrap()),
904 "(wat)"
905 );
906}
907
908fn ccontent(input: Span) -> IResult<Span, Span> {
910 context(
911 "ccontent",
912 recognize(alt((
913 recognize(alt((take_while_m_n(1, 1, is_ctext_ascii), utf8_non_ascii))),
914 recognize(quoted_pair),
915 comment,
916 recognize(encoded_word),
917 ))),
918 )
919 .parse(input)
920}
921
922fn is_quoted_pair_ascii(c: u8) -> bool {
923 match c {
924 0x00 | b'\r' | b'\n' | b' ' => true,
925 c => is_obs_no_ws_ctl(c) || is_vchar_ascii(c),
926 }
927}
928
929fn is_quoted_pair(c: u8) -> bool {
933 is_quoted_pair_ascii(c) || c >= 0x80
934}
935
936fn quoted_pair(input: Span) -> IResult<Span, Span> {
939 context(
940 "quoted_pair",
941 preceded(
942 tag("\\"),
943 alt((take_while_m_n(1, 1, is_quoted_pair_ascii), utf8_non_ascii)),
944 ),
945 )
946 .parse(input)
947}
948
949fn encoded_word(input: Span) -> IResult<Span, BString> {
951 let (loc, (charset, _language, _, encoding, _, text)) = context(
952 "encoded_word",
953 delimited(
954 tag("=?"),
955 (
956 charset,
957 opt(preceded(tag("*"), language)),
958 tag("?"),
959 encoding,
960 tag("?"),
961 encoded_text,
962 ),
963 tag("?="),
964 ),
965 )
966 .parse(input)?;
967
968 let bytes = match *encoding.fragment() {
969 b"B" | b"b" => data_encoding::BASE64_MIME
970 .decode(text.as_bytes())
971 .map_err(|err| {
972 make_context_error(
973 input,
974 format!("encoded_word: base64 decode failed: {err:#}"),
975 )
976 })?,
977 b"Q" | b"q" => {
978 let munged = text.replace("_", " ");
980 let had_trailing_space = munged.ends_with_str(" ");
983 let mut decoded = quoted_printable::decode(munged, quoted_printable::ParseMode::Robust)
984 .map_err(|err| {
985 make_context_error(
986 input,
987 format!("encoded_word: quoted printable decode failed: {err:#}"),
988 )
989 })?;
990 if had_trailing_space && !decoded.ends_with(b" ") {
991 decoded.push(b' ');
992 }
993 decoded
994 }
995 encoding => {
996 let encoding = BStr::new(encoding);
997 return Err(make_context_error(
998 input,
999 format!(
1000 "encoded_word: invalid encoding '{encoding}', expected one of b, B, q or Q"
1001 ),
1002 ));
1003 }
1004 };
1005
1006 let charset_name = charset.to_str().map_err(|err| {
1007 make_context_error(
1008 input,
1009 format!(
1010 "encoded_word: charset {} is not UTF-8: {err}",
1011 BStr::new(*charset)
1012 ),
1013 )
1014 })?;
1015
1016 let charset = Encoding::by_name(&*charset_name).ok_or_else(|| {
1017 make_context_error(
1018 input,
1019 format!("encoded_word: unsupported charset '{charset_name}'"),
1020 )
1021 })?;
1022
1023 let decoded = charset.decode_simple(&bytes).map_err(|err| {
1024 make_context_error(
1025 input,
1026 format!("encoded_word: failed to decode as '{charset_name}': {err}"),
1027 )
1028 })?;
1029
1030 Ok((loc, decoded.into()))
1031}
1032
1033fn charset(input: Span) -> IResult<Span, Span> {
1035 context("charset", take_while1(|c| c != b'*' && is_token(c))).parse(input)
1036}
1037
1038fn language(input: Span) -> IResult<Span, Span> {
1040 context("language", take_while1(|c| c != b'*' && is_token(c))).parse(input)
1041}
1042
1043fn encoding(input: Span) -> IResult<Span, Span> {
1045 context("encoding", take_while1(|c| c != b'*' && is_token(c))).parse(input)
1046}
1047
1048fn encoded_text(input: Span) -> IResult<Span, Span> {
1050 context(
1051 "encoded_text",
1052 recognize(many1(alt((
1053 take_while1(|c| is_vchar_ascii(c) && c != b' ' && c != b'?'),
1054 utf8_non_ascii,
1055 )))),
1056 )
1057 .parse(input)
1058}
1059
1060fn quoted_string(input: Span) -> IResult<Span, BString> {
1062 let (loc, (bits, trailer)) = context(
1063 "quoted_string",
1064 delimited(
1065 opt(cfws),
1066 delimited(
1067 tag("\""),
1068 (many0((opt(fws), qcontent)), opt(fws)),
1069 tag("\""),
1070 ),
1071 opt(cfws),
1072 ),
1073 )
1074 .parse(input)?;
1075
1076 let mut result = BString::default();
1077 for (a, b) in bits {
1078 if let Some(a) = a {
1079 result.push_str(&a);
1080 }
1081 result.push_str(b);
1082 }
1083 if let Some(t) = trailer {
1084 result.push_str(&t);
1085 }
1086 Ok((loc, result))
1087}
1088
1089fn qcontent(input: Span) -> IResult<Span, Span> {
1091 context(
1092 "qcontent",
1093 alt((
1094 take_while_m_n(1, 1, is_qtext_ascii),
1095 utf8_non_ascii,
1096 quoted_pair,
1097 )),
1098 )
1099 .parse(input)
1100}
1101
1102fn content_id(input: Span) -> IResult<Span, MessageID> {
1103 let (loc, id) = context("content_id", msg_id).parse(input)?;
1104 Ok((loc, id))
1105}
1106
1107fn msg_id(input: Span) -> IResult<Span, MessageID> {
1108 let (loc, id) = context("msg_id", alt((strict_msg_id, relaxed_msg_id))).parse(input)?;
1109 Ok((loc, id))
1110}
1111
1112fn relaxed_msg_id(input: Span) -> IResult<Span, MessageID> {
1113 let (loc, id) = context(
1114 "msg_id",
1115 delimited(
1116 preceded(opt(cfws), tag("<")),
1117 many0(take_while_m_n(1, 1, not_angle)),
1118 preceded(tag(">"), opt(cfws)),
1119 ),
1120 )
1121 .parse(input)?;
1122
1123 let mut result = BString::default();
1124 for item in id.into_iter() {
1125 result.push_str(*item);
1126 }
1127
1128 Ok((loc, MessageID(result)))
1129}
1130
1131fn msg_id_list(input: Span) -> IResult<Span, Vec<MessageID>> {
1133 context("msg_id_list", many1(msg_id)).parse(input)
1134}
1135
1136fn id_left(input: Span) -> IResult<Span, BString> {
1139 context("id_left", alt((dot_atom_text, local_part))).parse(input)
1140}
1141
1142fn id_right(input: Span) -> IResult<Span, BString> {
1145 context("id_right", alt((dot_atom_text, no_fold_literal, domain))).parse(input)
1146}
1147
1148fn no_fold_literal(input: Span) -> IResult<Span, BString> {
1150 context(
1151 "no_fold_literal",
1152 map(
1153 recognize((
1154 tag("["),
1155 recognize(many0(alt((take_while1(is_dtext_ascii), utf8_non_ascii)))),
1156 tag("]"),
1157 )),
1158 |s: Span| (*s).into(),
1159 ),
1160 )
1161 .parse(input)
1162}
1163
1164fn strict_msg_id(input: Span) -> IResult<Span, MessageID> {
1166 let (loc, (left, _, right)) = context(
1167 "msg_id",
1168 delimited(
1169 preceded(opt(cfws), tag("<")),
1170 (id_left, tag("@"), id_right),
1171 preceded(tag(">"), opt(cfws)),
1172 ),
1173 )
1174 .parse(input)?;
1175
1176 let mut result: BString = left.into();
1177 result.push_char('@');
1178 result.push_str(right);
1179
1180 Ok((loc, MessageID(result)))
1181}
1182
1183fn unstructured(input: Span) -> IResult<Span, BString> {
1185 #[derive(Debug)]
1186 enum Word {
1187 Encoded(BString),
1188 UText(BString),
1189 Fws,
1190 }
1191
1192 let (loc, words) = context(
1193 "unstructured",
1194 many0(alt((
1195 preceded(
1196 map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws),
1197 terminated(
1198 alt((
1199 map(encoded_word, Word::Encoded),
1200 map(obs_utext, |s| Word::UText((*s).into())),
1201 )),
1202 map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws),
1203 ),
1204 ),
1205 map(fws, |_| Word::Fws),
1206 ))),
1207 )
1208 .parse(input)?;
1209
1210 #[derive(Debug)]
1211 enum ProcessedWord {
1212 Encoded(BString),
1213 Text(BString),
1214 Fws,
1215 }
1216 let mut processed = vec![];
1217 for w in words {
1218 match w {
1219 Word::Encoded(p) => {
1220 if processed.len() >= 2
1221 && matches!(processed.last(), Some(ProcessedWord::Fws))
1222 && matches!(processed[processed.len() - 2], ProcessedWord::Encoded(_))
1223 {
1224 processed.pop();
1226 }
1227 processed.push(ProcessedWord::Encoded(p));
1228 }
1229 Word::Fws => {
1230 if !matches!(processed.last(), Some(ProcessedWord::Fws)) {
1232 processed.push(ProcessedWord::Fws);
1233 }
1234 }
1235 Word::UText(c) => match processed.last_mut() {
1236 Some(ProcessedWord::Text(prior)) => prior.push_str(c),
1237 _ => processed.push(ProcessedWord::Text(c)),
1238 },
1239 }
1240 }
1241
1242 let mut result = BString::default();
1243 for word in processed {
1244 match word {
1245 ProcessedWord::Encoded(s) | ProcessedWord::Text(s) => {
1246 result.push_str(&s);
1247 }
1248 ProcessedWord::Fws => {
1249 result.push(b' ');
1250 }
1251 }
1252 }
1253
1254 Ok((loc, result))
1255}
1256
1257fn arc_authentication_results(input: Span) -> IResult<Span, ARCAuthenticationResults> {
1258 context(
1259 "arc_authentication_results",
1260 map(
1261 (
1262 preceded(opt(cfws), tag("i")),
1263 preceded(opt(cfws), tag("=")),
1264 preceded(opt(cfws), nom::character::complete::u8),
1265 preceded(opt(cfws), tag(";")),
1266 preceded(opt(cfws), value),
1267 opt(preceded(cfws, nom::character::complete::u32)),
1268 alt((no_result, many1(resinfo))),
1269 opt(cfws),
1270 ),
1271 |(_i, _eq, instance, _semic, serv_id, version, results, _)| ARCAuthenticationResults {
1272 instance,
1273 serv_id: serv_id.into(),
1274 version,
1275 results,
1276 },
1277 ),
1278 )
1279 .parse(input)
1280}
1281
1282fn authentication_results(input: Span) -> IResult<Span, AuthenticationResults> {
1283 context(
1284 "authentication_results",
1285 map(
1286 (
1287 preceded(opt(cfws), value),
1288 opt(preceded(cfws, nom::character::complete::u32)),
1289 alt((no_result, many1(resinfo))),
1290 opt(cfws),
1291 ),
1292 |(serv_id, version, results, _)| AuthenticationResults {
1293 serv_id: serv_id.into(),
1294 version,
1295 results,
1296 },
1297 ),
1298 )
1299 .parse(input)
1300}
1301
1302fn no_result(input: Span) -> IResult<Span, Vec<AuthenticationResult>> {
1303 context(
1304 "no_result",
1305 map((opt(cfws), tag(";"), opt(cfws), tag("none")), |_| vec![]),
1306 )
1307 .parse(input)
1308}
1309
1310fn resinfo(input: Span) -> IResult<Span, AuthenticationResult> {
1311 context(
1312 "resinfo",
1313 map(
1314 (
1315 opt(cfws),
1316 tag(";"),
1317 methodspec,
1318 opt(preceded(cfws, reasonspec)),
1319 opt(many1(propspec)),
1320 ),
1321 |(_, _, (method, method_version, result), reason, props)| AuthenticationResult {
1322 method,
1323 method_version,
1324 result,
1325 reason: reason.map(Into::into),
1326 props: match props {
1327 None => BTreeMap::default(),
1328 Some(props) => props.into_iter().collect(),
1329 },
1330 },
1331 ),
1332 )
1333 .parse(input)
1334}
1335
1336fn methodspec(input: Span) -> IResult<Span, (String, Option<u32>, String)> {
1337 context(
1338 "methodspec",
1339 map(
1340 (
1341 opt(cfws),
1342 (keyword, opt(methodversion)),
1343 opt(cfws),
1344 tag("="),
1345 opt(cfws),
1346 keyword,
1347 ),
1348 |(_, (method, methodversion), _, _, _, result)| (method, methodversion, result),
1349 ),
1350 )
1351 .parse(input)
1352}
1353
1354fn keyword(input: Span) -> IResult<Span, String> {
1359 context(
1360 "keyword",
1361 map(
1362 take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'-'),
1363 |s: Span| String::from_utf8((*s).into()).expect("keyword is ASCII-only"),
1365 ),
1366 )
1367 .parse(input)
1368}
1369
1370fn methodversion(input: Span) -> IResult<Span, u32> {
1371 context(
1372 "methodversion",
1373 preceded(
1374 (opt(cfws), tag("/"), opt(cfws)),
1375 nom::character::complete::u32,
1376 ),
1377 )
1378 .parse(input)
1379}
1380
1381fn reasonspec(input: Span) -> IResult<Span, BString> {
1382 context(
1383 "reason",
1384 map(
1385 (tag("reason"), opt(cfws), tag("="), opt(cfws), value),
1386 |(_, _, _, _, value)| value,
1387 ),
1388 )
1389 .parse(input)
1390}
1391
1392fn propspec(input: Span) -> IResult<Span, (String, BString)> {
1393 context(
1394 "propspec",
1395 map(
1396 (
1397 opt(cfws),
1402 keyword,
1403 opt(cfws),
1404 tag("."),
1405 opt(cfws),
1406 keyword,
1407 opt(cfws),
1408 tag("="),
1409 opt(cfws),
1410 alt((
1415 map(preceded(tag("@"), domain), |d| {
1416 let mut at_dom = BString::from("@");
1417 at_dom.push_str(d);
1418 at_dom
1419 }),
1420 map(separated_pair(local_part, tag("@"), domain), |(u, d)| {
1421 let mut result: BString = u.into();
1422 result.push(b'@');
1423 result.push_str(d);
1424 result
1425 }),
1426 quoted_string,
1427 domain,
1428 map(mime_token, |s: Span| (*s).into()),
1429 )),
1430 opt(cfws),
1431 ),
1432 |(_, ptype, _, _, _, property, _, _, _, value, _)| {
1433 (format!("{ptype}.{property}"), value)
1434 },
1435 ),
1436 )
1437 .parse(input)
1438}
1439
1440fn obs_utext(input: Span) -> IResult<Span, Span> {
1442 context(
1443 "obs_utext",
1444 alt((
1445 take_while_m_n(1, 1, |c| {
1446 c == 0x00 || is_obs_no_ws_ctl(c) || is_vchar_ascii(c)
1447 }),
1448 utf8_non_ascii,
1449 )),
1450 )
1451 .parse(input)
1452}
1453
1454fn is_mime_token(c: u8) -> bool {
1455 is_char(c) && c != b' ' && !is_ctl(c) && !is_tspecial(c)
1456}
1457
1458fn mime_token(input: Span) -> IResult<Span, Span> {
1461 context(
1462 "mime_token",
1463 recognize(many1(alt((take_while1(is_mime_token), utf8_non_ascii)))),
1464 )
1465 .parse(input)
1466}
1467
1468fn content_type(input: Span) -> IResult<Span, MimeParameters> {
1473 let (loc, (mime_type, _, _, _, mime_subtype, _, parameters)) = context(
1474 "content_type",
1475 preceded(
1476 opt(cfws),
1477 (
1478 mime_token,
1479 opt(cfws),
1480 tag("/"),
1481 opt(cfws),
1482 mime_token,
1483 opt(cfws),
1484 many0(preceded(
1485 preceded(opt(tag(";")), opt(cfws)),
1493 terminated(parameter, opt(cfws)),
1494 )),
1495 ),
1496 ),
1497 )
1498 .parse(input)?;
1499
1500 let mut value: BString = (*mime_type).into();
1501 value.push_char('/');
1502 value.push_str(mime_subtype);
1503
1504 Ok((loc, MimeParameters { value, parameters }))
1505}
1506
1507fn content_transfer_encoding(input: Span) -> IResult<Span, MimeParameters> {
1508 let (loc, (value, _, parameters)) = context(
1509 "content_transfer_encoding",
1510 preceded(
1511 opt(cfws),
1512 (
1513 mime_token,
1514 opt(cfws),
1515 many0(preceded(
1516 preceded(opt(tag(";")), opt(cfws)),
1524 terminated(parameter, opt(cfws)),
1525 )),
1526 ),
1527 ),
1528 )
1529 .parse(input)?;
1530
1531 Ok((
1532 loc,
1533 MimeParameters {
1534 value: value.as_bytes().into(),
1535 parameters,
1536 },
1537 ))
1538}
1539
1540fn parameter(input: Span) -> IResult<Span, MimeParameter> {
1542 context(
1543 "parameter",
1544 alt((
1545 param_with_unquoted_rfc2047,
1550 param_with_quoted_rfc2047,
1551 regular_parameter,
1552 extended_param_with_charset,
1553 extended_param_no_charset,
1554 )),
1555 )
1556 .parse(input)
1557}
1558
1559fn param_with_unquoted_rfc2047(input: Span) -> IResult<Span, MimeParameter> {
1560 context(
1561 "param_with_unquoted_rfc2047",
1562 map(
1563 (attribute, opt(cfws), tag("="), opt(cfws), encoded_word),
1564 |(name, _, _, _, value)| MimeParameter {
1565 name: name.as_bytes().into(),
1566 value: value.as_bytes().into(),
1567 section: None,
1568 encoding: MimeParameterEncoding::UnquotedRfc2047,
1569 mime_charset: None,
1570 mime_language: None,
1571 },
1572 ),
1573 )
1574 .parse(input)
1575}
1576
1577fn param_with_quoted_rfc2047(input: Span) -> IResult<Span, MimeParameter> {
1578 context(
1579 "param_with_quoted_rfc2047",
1580 map(
1581 (
1582 attribute,
1583 opt(cfws),
1584 tag("="),
1585 opt(cfws),
1586 delimited(tag("\""), encoded_word, tag("\"")),
1587 ),
1588 |(name, _, _, _, value)| MimeParameter {
1589 name: name.as_bytes().into(),
1590 value: value.as_bytes().into(),
1591 section: None,
1592 encoding: MimeParameterEncoding::QuotedRfc2047,
1593 mime_charset: None,
1594 mime_language: None,
1595 },
1596 ),
1597 )
1598 .parse(input)
1599}
1600
1601fn extended_param_with_charset(input: Span) -> IResult<Span, MimeParameter> {
1602 context(
1603 "extended_param_with_charset",
1604 map(
1605 (
1606 attribute,
1607 opt(section),
1608 tag("*"),
1609 opt(cfws),
1610 tag("="),
1611 opt(cfws),
1612 opt(mime_charset),
1613 tag("'"),
1614 opt(mime_language),
1615 tag("'"),
1616 map(
1617 recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))),
1618 |s: Span| (*s).into(),
1619 ),
1620 ),
1621 |(name, section, _, _, _, _, mime_charset, _, mime_language, _, value)| MimeParameter {
1622 name: name.as_bytes().into(),
1623 section,
1624 mime_charset: mime_charset.map(|s| s.as_bytes().into()),
1625 mime_language: mime_language.map(|s| s.as_bytes().into()),
1626 encoding: MimeParameterEncoding::Rfc2231,
1627 value,
1628 },
1629 ),
1630 )
1631 .parse(input)
1632}
1633
1634fn extended_param_no_charset(input: Span) -> IResult<Span, MimeParameter> {
1635 context(
1636 "extended_param_no_charset",
1637 map(
1638 (
1639 attribute,
1640 opt(section),
1641 opt(tag("*")),
1642 opt(cfws),
1643 tag("="),
1644 opt(cfws),
1645 alt((
1646 quoted_string,
1647 map(
1648 recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))),
1649 |s: Span| (*s).into(),
1650 ),
1651 )),
1652 ),
1653 |(name, section, star, _, _, _, value)| MimeParameter {
1654 name: name.as_bytes().into(),
1655 section,
1656 mime_charset: None,
1657 mime_language: None,
1658 encoding: if star.is_some() {
1659 MimeParameterEncoding::Rfc2231
1660 } else {
1661 MimeParameterEncoding::None
1662 },
1663 value,
1664 },
1665 ),
1666 )
1667 .parse(input)
1668}
1669
1670fn mime_charset(input: Span) -> IResult<Span, Span> {
1671 context(
1672 "mime_charset",
1673 take_while1(|c| is_mime_token(c) && c != b'\''),
1674 )
1675 .parse(input)
1676}
1677
1678fn mime_language(input: Span) -> IResult<Span, Span> {
1679 context(
1680 "mime_language",
1681 take_while1(|c| is_mime_token(c) && c != b'\''),
1682 )
1683 .parse(input)
1684}
1685
1686fn ext_octet(input: Span) -> IResult<Span, Span> {
1687 context(
1688 "ext_octet",
1689 recognize((
1690 tag("%"),
1691 take_while_m_n(2, 2, |b: u8| b.is_ascii_hexdigit()),
1692 )),
1693 )
1694 .parse(input)
1695}
1696
1697fn section(input: Span) -> IResult<Span, u32> {
1699 context("section", preceded(tag("*"), nom::character::complete::u32)).parse(input)
1700}
1701
1702fn regular_parameter(input: Span) -> IResult<Span, MimeParameter> {
1704 context(
1705 "regular_parameter",
1706 map(
1707 (attribute, opt(cfws), tag("="), opt(cfws), value),
1708 |(name, _, _, _, value)| MimeParameter {
1709 name: name.as_bytes().into(),
1710 value: value.as_bytes().into(),
1711 section: None,
1712 encoding: MimeParameterEncoding::None,
1713 mime_charset: None,
1714 mime_language: None,
1715 },
1716 ),
1717 )
1718 .parse(input)
1719}
1720
1721fn attribute(input: Span) -> IResult<Span, Span> {
1724 context("attribute", take_while1(is_attribute_char)).parse(input)
1725}
1726
1727fn value(input: Span) -> IResult<Span, BString> {
1728 context(
1729 "value",
1730 alt((map(mime_token, |s: Span| (*s).into()), quoted_string)),
1731 )
1732 .parse(input)
1733}
1734
1735pub struct Parser;
1736
1737impl Parser {
1738 pub fn parse_mailbox_list_header(text: &[u8]) -> Result<MailboxList> {
1739 parse_with(text, mailbox_list)
1740 }
1741
1742 pub fn parse_mailbox_header(text: &[u8]) -> Result<Mailbox> {
1743 parse_with(text, mailbox)
1744 }
1745
1746 pub fn parse_address_list_header(text: &[u8]) -> Result<AddressList> {
1747 parse_with(text, address_list)
1748 }
1749
1750 pub fn parse_msg_id_header(text: &[u8]) -> Result<MessageID> {
1751 parse_with(text, msg_id)
1752 }
1753
1754 pub fn parse_msg_id_header_list(text: &[u8]) -> Result<Vec<MessageID>> {
1755 parse_with(text, msg_id_list)
1756 }
1757
1758 pub fn parse_content_id_header(text: &[u8]) -> Result<MessageID> {
1759 parse_with(text, content_id)
1760 }
1761
1762 pub fn parse_content_type_header(text: &[u8]) -> Result<MimeParameters> {
1763 parse_with(text, content_type)
1764 }
1765
1766 pub fn parse_content_transfer_encoding_header(text: &[u8]) -> Result<MimeParameters> {
1767 parse_with(text, content_transfer_encoding)
1768 }
1769
1770 pub fn parse_unstructured_header(text: &[u8]) -> Result<BString> {
1771 parse_with(text, unstructured)
1772 }
1773
1774 pub fn parse_authentication_results_header(text: &[u8]) -> Result<AuthenticationResults> {
1775 parse_with(text, authentication_results)
1776 }
1777
1778 pub fn parse_arc_authentication_results_header(
1779 text: &[u8],
1780 ) -> Result<ARCAuthenticationResults> {
1781 parse_with(text, arc_authentication_results)
1782 }
1783}
1784
1785#[serde_as]
1786#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1787#[serde(deny_unknown_fields)]
1788pub struct ARCAuthenticationResults {
1789 pub instance: u8,
1790 #[serde_as(as = "BStringUtf8")]
1791 pub serv_id: BString,
1792 pub version: Option<u32>,
1793 pub results: Vec<AuthenticationResult>,
1794}
1795
1796impl EncodeHeaderValue for ARCAuthenticationResults {
1797 fn encode_value(&self) -> SharedString<'static> {
1798 let mut result = format!("i={}; ", self.instance).into_bytes();
1799
1800 emit_value_token(&self.serv_id, &mut result);
1801 if let Some(v) = self.version {
1802 result.push_str(&format!(" {v}"));
1803 }
1804
1805 if self.results.is_empty() {
1806 result.push_str("; none");
1807 } else {
1808 for res in &self.results {
1809 result.push_str(";\r\n\t");
1810 emit_value_token(res.method.as_bytes(), &mut result);
1811 if let Some(v) = res.method_version {
1812 result.push_str(&format!("/{v}"));
1813 }
1814 result.push(b'=');
1815 emit_value_token(res.result.as_bytes(), &mut result);
1816 if let Some(reason) = &res.reason {
1817 result.push_str(" reason=");
1818 emit_value_token(reason.as_bytes(), &mut result);
1819 }
1820 for (k, v) in &res.props {
1821 result.push_str(&format!("\r\n\t{k}="));
1822 emit_value_token(v.as_bytes(), &mut result);
1823 }
1824 }
1825 }
1826
1827 result.into()
1828 }
1829}
1830
1831#[serde_as]
1832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1833#[serde(deny_unknown_fields)]
1834pub struct AuthenticationResults {
1835 #[serde_as(as = "BStringUtf8")]
1836 pub serv_id: BString,
1837 #[serde(default)]
1838 pub version: Option<u32>,
1839 #[serde(default)]
1840 pub results: Vec<AuthenticationResult>,
1841}
1842
1843fn emit_value_token(value: &[u8], target: &mut Vec<u8>) {
1845 let use_quoted_string = !value.iter().all(|&c| is_mime_token(c) || c == b'@');
1847 if use_quoted_string {
1848 target.push(b'"');
1849 for (start, end, c) in value.char_indices() {
1850 if c == '"' || c == '\\' {
1851 target.push(b'\\');
1852 }
1853 target.push_str(&value[start..end]);
1854 }
1855 target.push(b'"');
1856 } else {
1857 target.push_str(value);
1858 }
1859}
1860
1861impl EncodeHeaderValue for AuthenticationResults {
1862 fn encode_value(&self) -> SharedString<'static> {
1863 let mut result = Vec::new();
1864 emit_value_token(&self.serv_id, &mut result);
1865 if let Some(v) = self.version {
1866 result.push_str(&format!(" {v}"));
1867 }
1868 if self.results.is_empty() {
1869 result.push_str("; none");
1870 } else {
1871 for res in &self.results {
1872 result.push_str(";\r\n\t");
1873 emit_value_token(res.method.as_bytes(), &mut result);
1874 if let Some(v) = res.method_version {
1875 result.push_str(&format!("/{v}"));
1876 }
1877 result.push(b'=');
1878 emit_value_token(res.result.as_bytes(), &mut result);
1879 if let Some(reason) = &res.reason {
1880 result.push_str(" reason=");
1881 emit_value_token(reason.as_bytes(), &mut result);
1882 }
1883 for (k, v) in &res.props {
1884 result.push_str(&format!("\r\n\t{k}="));
1885 emit_value_token(v.as_bytes(), &mut result);
1886 }
1887 }
1888 }
1889
1890 result.into()
1891 }
1892}
1893
1894#[serde_as]
1895#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1896#[serde(deny_unknown_fields)]
1897pub struct AuthenticationResult {
1898 pub method: String,
1899 #[serde(default)]
1900 pub method_version: Option<u32>,
1901 pub result: String,
1902 #[serde_as(as = "Option<BStringUtf8>")]
1903 #[serde(default)]
1904 pub reason: Option<BString>,
1905 #[serde_as(as = "BTreeMap<_, BStringUtf8>")]
1906 #[serde(default)]
1907 pub props: BTreeMap<String, BString>,
1908}
1909
1910#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1911#[serde(deny_unknown_fields)]
1912pub struct AddrSpec {
1913 pub local_part: String,
1914 pub domain: String,
1915}
1916
1917impl AddrSpec {
1918 pub fn new(local_part: &str, domain: &str) -> Self {
1919 Self {
1920 local_part: local_part.into(),
1921 domain: domain.into(),
1922 }
1923 }
1924
1925 pub fn parse(email: &str) -> Result<Self> {
1926 parse_with(email.as_bytes(), addr_spec)
1927 }
1928}
1929
1930impl EncodeHeaderValue for AddrSpec {
1931 fn encode_value(&self) -> SharedString<'static> {
1932 let mut result: Vec<u8> = vec![];
1933
1934 let needs_quoting = !self
1935 .local_part
1936 .as_bytes()
1937 .iter()
1938 .all(|&c| is_atext(c) || c == b'.');
1939 if needs_quoting {
1940 result.push(b'"');
1941 for &c in self.local_part.as_bytes().iter() {
1946 if c == b'"' || c == b'\\' {
1947 result.push(b'\\');
1948 }
1949 result.push(c);
1950 }
1951 result.push(b'"');
1952 } else {
1953 result.push_str(&self.local_part);
1954 }
1955 result.push(b'@');
1956 result.push_str(&self.domain);
1957
1958 result.into()
1959 }
1960}
1961
1962#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1963#[serde(untagged)]
1964pub enum Address {
1965 Mailbox(Mailbox),
1966 Group { name: String, entries: MailboxList },
1967}
1968
1969#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1970#[serde(deny_unknown_fields, transparent)]
1971pub struct AddressList(pub Vec<Address>);
1972
1973impl std::ops::Deref for AddressList {
1974 type Target = Vec<Address>;
1975 fn deref(&self) -> &Vec<Address> {
1976 &self.0
1977 }
1978}
1979
1980impl AddressList {
1981 pub fn extract_first_mailbox(&self) -> Option<&Mailbox> {
1982 let address = self.0.first()?;
1983 match address {
1984 Address::Mailbox(mailbox) => Some(mailbox),
1985 Address::Group { entries, .. } => entries.extract_first_mailbox(),
1986 }
1987 }
1988}
1989
1990#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1991#[serde(deny_unknown_fields, transparent)]
1992pub struct MailboxList(pub Vec<Mailbox>);
1993
1994impl std::ops::Deref for MailboxList {
1995 type Target = Vec<Mailbox>;
1996 fn deref(&self) -> &Vec<Mailbox> {
1997 &self.0
1998 }
1999}
2000
2001impl MailboxList {
2002 pub fn extract_first_mailbox(&self) -> Option<&Mailbox> {
2003 self.0.first()
2004 }
2005}
2006
2007#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2008#[serde(deny_unknown_fields)]
2009pub struct Mailbox {
2010 pub name: Option<String>,
2011 pub address: AddrSpec,
2012}
2013
2014#[serde_as]
2015#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2016#[serde(transparent)]
2017pub struct MessageID(#[serde_as(as = "BStringUtf8")] pub BString);
2018
2019impl EncodeHeaderValue for MessageID {
2020 fn encode_value(&self) -> SharedString<'static> {
2021 let mut result = Vec::<u8>::with_capacity(self.0.len() + 2);
2022 result.push(b'<');
2023 result.push_str(&self.0);
2024 result.push(b'>');
2025 result.into()
2026 }
2027}
2028
2029impl EncodeHeaderValue for Vec<MessageID> {
2030 fn encode_value(&self) -> SharedString<'static> {
2031 let mut result = BString::default();
2032 for id in self {
2033 if !result.is_empty() {
2034 result.push_str("\r\n\t");
2035 }
2036 result.push(b'<');
2037 result.push_str(&id.0);
2038 result.push(b'>');
2039 }
2040 result.into()
2041 }
2042}
2043
2044#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2053pub(crate) enum MimeParameterEncoding {
2054 None,
2055 Rfc2231,
2056 UnquotedRfc2047,
2057 QuotedRfc2047,
2058}
2059
2060#[derive(Debug, Clone, PartialEq, Eq)]
2061struct MimeParameter {
2062 pub name: BString,
2063 pub section: Option<u32>,
2064 pub mime_charset: Option<BString>,
2065 pub mime_language: Option<BString>,
2066 pub encoding: MimeParameterEncoding,
2067 pub value: BString,
2068}
2069
2070#[derive(Debug, Clone, PartialEq, Eq)]
2071pub struct MimeParameters {
2072 pub value: BString,
2073 parameters: Vec<MimeParameter>,
2074}
2075
2076impl MimeParameters {
2077 pub fn new(value: impl AsRef<[u8]>) -> Self {
2078 Self {
2079 value: value.as_ref().into(),
2080 parameters: vec![],
2081 }
2082 }
2083
2084 pub fn parameter_map(&self) -> BTreeMap<BString, BString> {
2089 let mut map = BTreeMap::new();
2090
2091 fn contains_key_ignore_case(map: &BTreeMap<BString, BString>, key: &[u8]) -> bool {
2092 for k in map.keys() {
2093 if k.eq_ignore_ascii_case(key) {
2094 return true;
2095 }
2096 }
2097 false
2098 }
2099
2100 for entry in &self.parameters {
2101 let name = entry.name.as_bytes();
2102 if !contains_key_ignore_case(&map, name) {
2103 if let Some(value) = self.get(name) {
2104 map.insert(name.into(), value);
2105 }
2106 }
2107 }
2108
2109 map
2110 }
2111
2112 pub fn get(&self, name: impl AsRef<[u8]>) -> Option<BString> {
2118 let name = name.as_ref();
2119 let mut elements: Vec<_> = self
2120 .parameters
2121 .iter()
2122 .filter(|p| p.name.eq_ignore_ascii_case(name.as_bytes()))
2123 .collect();
2124 if elements.is_empty() {
2125 return None;
2126 }
2127 elements.sort_by(|a, b| a.section.cmp(&b.section));
2128
2129 let mut mime_charset = None;
2130 let mut result: Vec<u8> = vec![];
2131
2132 for ele in elements {
2133 if let Some(cset) = ele.mime_charset.as_ref().and_then(|b| b.to_str().ok()) {
2134 mime_charset = Encoding::by_name(&*cset);
2135 }
2136
2137 match ele.encoding {
2138 MimeParameterEncoding::Rfc2231 => {
2139 if let Some(charset) = mime_charset.as_ref() {
2140 let mut chars = ele.value.chars();
2141 let mut bytes: Vec<u8> = vec![];
2142
2143 fn char_to_bytes(c: char, bytes: &mut Vec<u8>) {
2144 let mut buf = [0u8; 8];
2145 let s = c.encode_utf8(&mut buf);
2146 for b in s.bytes() {
2147 bytes.push(b);
2148 }
2149 }
2150
2151 'next_char: while let Some(c) = chars.next() {
2152 match c {
2153 '%' => {
2154 let mut value = 0u8;
2155 for _ in 0..2 {
2156 match chars.next() {
2157 Some(n) => match n {
2158 '0'..='9' => {
2159 value <<= 4;
2160 value |= n as u32 as u8 - b'0';
2161 }
2162 'a'..='f' => {
2163 value <<= 4;
2164 value |= (n as u32 as u8 - b'a') + 10;
2165 }
2166 'A'..='F' => {
2167 value <<= 4;
2168 value |= (n as u32 as u8 - b'A') + 10;
2169 }
2170 _ => {
2171 char_to_bytes('%', &mut bytes);
2172 char_to_bytes(n, &mut bytes);
2173 break 'next_char;
2174 }
2175 },
2176 None => {
2177 char_to_bytes('%', &mut bytes);
2178 break 'next_char;
2179 }
2180 }
2181 }
2182
2183 bytes.push(value);
2184 }
2185 c => {
2186 char_to_bytes(c, &mut bytes);
2187 }
2188 }
2189 }
2190
2191 if let Ok(decoded) = charset.decode_simple(&bytes) {
2192 result.push_str(&decoded);
2193 }
2194 } else {
2195 result.push_str(&ele.value);
2196 }
2197 }
2198 MimeParameterEncoding::UnquotedRfc2047
2199 | MimeParameterEncoding::QuotedRfc2047
2200 | MimeParameterEncoding::None => {
2201 result.push_str(&ele.value);
2202 }
2203 }
2204 }
2205
2206 Some(result.into())
2207 }
2208
2209 pub fn remove(&mut self, name: impl AsRef<[u8]>) {
2211 let name = name.as_ref();
2212 self.parameters
2213 .retain(|p| !p.name.eq_ignore_ascii_case(name));
2214 }
2215
2216 pub fn set(&mut self, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
2217 self.set_with_encoding(name, value, MimeParameterEncoding::None)
2218 }
2219
2220 pub(crate) fn set_with_encoding(
2221 &mut self,
2222 name: impl AsRef<[u8]>,
2223 value: impl AsRef<[u8]>,
2224 encoding: MimeParameterEncoding,
2225 ) {
2226 self.remove(name.as_ref());
2227
2228 self.parameters.push(MimeParameter {
2229 name: name.as_ref().into(),
2230 value: value.as_ref().into(),
2231 section: None,
2232 mime_charset: None,
2233 mime_language: None,
2234 encoding,
2235 });
2236 }
2237
2238 pub fn is_multipart(&self) -> bool {
2239 self.value.starts_with_str("message/") || self.value.starts_with_str("multipart/")
2240 }
2241
2242 pub fn is_text(&self) -> bool {
2243 self.value.starts_with_str("text/")
2244 }
2245}
2246
2247impl EncodeHeaderValue for MimeParameters {
2248 fn encode_value(&self) -> SharedString<'static> {
2249 let mut result = self.value.clone();
2250 let names: BTreeMap<&BStr, MimeParameterEncoding> = self
2251 .parameters
2252 .iter()
2253 .map(|p| (p.name.as_bstr(), p.encoding))
2254 .collect();
2255
2256 for (name, stated_encoding) in names {
2257 let value = self.get(name).expect("name to be present");
2258
2259 match stated_encoding {
2260 MimeParameterEncoding::UnquotedRfc2047 => {
2261 let encoded = qp_encode(&value);
2262 result.push_str(&format!(";\r\n\t{name}={encoded}"));
2263 }
2264 MimeParameterEncoding::QuotedRfc2047 => {
2265 let encoded = qp_encode(&value);
2266 result.push_str(&format!(";\r\n\t{name}=\"{encoded}\""));
2267 }
2268 MimeParameterEncoding::None | MimeParameterEncoding::Rfc2231 => {
2269 let needs_encoding = value.iter().any(|&c| !is_mime_token(c) || !c.is_ascii());
2270 let use_quoted_string = value
2273 .iter()
2274 .all(|&c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii());
2275
2276 let mut params = vec![];
2277 let mut chars = value.char_indices().peekable();
2278 while chars.peek().is_some() {
2279 let count = params.len();
2280 let is_first = count == 0;
2281 let prefix = if use_quoted_string {
2282 "\""
2283 } else if is_first && needs_encoding {
2284 "UTF-8''"
2285 } else {
2286 ""
2287 };
2288 let limit = 74 - (name.len() + 4 + prefix.len());
2289
2290 let mut encoded: Vec<u8> = vec![];
2291
2292 while encoded.len() < limit {
2293 let Some((start, end, c)) = chars.next() else {
2294 break;
2295 };
2296 let s = &value[start..end];
2297
2298 if use_quoted_string {
2299 if c == '"' || c == '\\' {
2300 encoded.push(b'\\');
2301 }
2302 encoded.push_str(s);
2303 } else if (c as u32) <= 0xff
2304 && is_mime_token(c as u32 as u8)
2305 && (!needs_encoding || c != '%')
2306 {
2307 encoded.push_str(s);
2308 } else {
2309 for b in s.bytes() {
2310 encoded.push(b'%');
2311 encoded.push(HEX_CHARS[(b as usize) >> 4]);
2312 encoded.push(HEX_CHARS[(b as usize) & 0x0f]);
2313 }
2314 }
2315 }
2316
2317 if use_quoted_string {
2318 encoded.push(b'"');
2319 }
2320
2321 params.push(MimeParameter {
2322 name: name.into(),
2323 section: Some(count as u32),
2324 mime_charset: if is_first { Some("UTF-8".into()) } else { None },
2325 mime_language: None,
2326 encoding: if needs_encoding {
2327 MimeParameterEncoding::Rfc2231
2328 } else {
2329 MimeParameterEncoding::None
2330 },
2331 value: encoded.into(),
2332 })
2333 }
2334 if params.len() == 1 {
2335 params.last_mut().map(|p| p.section = None);
2336 }
2337 for p in params {
2338 result.push_str(";\r\n\t");
2339 let charset_tick = if !use_quoted_string
2340 && (p.mime_charset.is_some() || p.mime_language.is_some())
2341 {
2342 "'"
2343 } else {
2344 ""
2345 };
2346 let lang_tick = if !use_quoted_string
2347 && (p.mime_language.is_some() || p.mime_charset.is_some())
2348 {
2349 "'"
2350 } else {
2351 ""
2352 };
2353
2354 let section = p
2355 .section
2356 .map(|s| format!("*{s}"))
2357 .unwrap_or_else(String::new);
2358
2359 let uses_encoding =
2360 if !use_quoted_string && p.encoding == MimeParameterEncoding::Rfc2231 {
2361 "*"
2362 } else {
2363 ""
2364 };
2365 let charset = if use_quoted_string {
2366 BStr::new("\"")
2367 } else {
2368 p.mime_charset
2369 .as_ref()
2370 .map(|b| b.as_bstr())
2371 .unwrap_or(BStr::new(""))
2372 };
2373 let lang = p
2374 .mime_language
2375 .as_ref()
2376 .map(|b| b.as_bstr())
2377 .unwrap_or(BStr::new(""));
2378
2379 let line = format!(
2380 "{name}{section}{uses_encoding}={charset}{charset_tick}{lang}{lang_tick}{value}",
2381 name = &p.name,
2382 value = &p.value
2383 );
2384 result.push_str(&line);
2385 }
2386 }
2387 }
2388 }
2389 result.into()
2390 }
2391}
2392
2393static HEX_CHARS: &[u8] = b"0123456789ABCDEF";
2394
2395pub(crate) fn qp_encode(s: &[u8]) -> String {
2396 let prefix = b"=?UTF-8?q?";
2397 let suffix = b"?=";
2398 let limit = 72 - (prefix.len() + suffix.len());
2399
2400 let mut result = Vec::with_capacity(s.len());
2401
2402 result.extend_from_slice(prefix);
2403 let mut line_length = 0;
2404
2405 enum Bytes<'a> {
2406 Passthru(&'a [u8]),
2407 Encode(&'a [u8]),
2408 }
2409
2410 for (start, end, c) in s.char_indices() {
2413 let bytes = &s[start..end];
2414
2415 let b = if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
2416 && c != '?'
2417 && c != '='
2418 && c != ' '
2419 && c != '\t'
2420 {
2421 Bytes::Passthru(bytes)
2422 } else if c == ' ' {
2423 Bytes::Passthru(b"_")
2424 } else {
2425 Bytes::Encode(bytes)
2426 };
2427
2428 let need_len = match b {
2429 Bytes::Passthru(b) => b.len(),
2430 Bytes::Encode(b) => b.len() * 3,
2431 };
2432
2433 if need_len > limit - line_length {
2434 result.extend_from_slice(suffix);
2436 result.extend_from_slice(b"\r\n\t");
2437 result.extend_from_slice(prefix);
2438 line_length = 0;
2439 }
2440
2441 match b {
2442 Bytes::Passthru(c) => {
2443 result.extend_from_slice(c);
2444 }
2445 Bytes::Encode(bytes) => {
2446 for &c in bytes {
2447 result.push(b'=');
2448 result.push(HEX_CHARS[(c as usize) >> 4]);
2449 result.push(HEX_CHARS[(c as usize) & 0x0f]);
2450 }
2451 }
2452 }
2453
2454 line_length += need_len;
2455 }
2456
2457 if line_length > 0 {
2458 result.extend_from_slice(suffix);
2459 }
2460
2461 unsafe { String::from_utf8_unchecked(result) }
2464}
2465
2466#[cfg(test)]
2467#[test]
2468fn test_qp_encode() {
2469 let encoded = qp_encode(
2470 b"hello, I am a line that is this long, or maybe a little \
2471 bit longer than this, and that should get wrapped by the encoder",
2472 );
2473 k9::snapshot!(
2474 encoded,
2475 r#"
2476=?UTF-8?q?hello,_I_am_a_line_that_is_this_long,_or_maybe_a_little_bit_?=\r
2477\t=?UTF-8?q?longer_than_this,_and_that_should_get_wrapped_by_the_encoder?=
2478"#
2479 );
2480}
2481
2482fn quote_string(s: impl AsRef<[u8]>) -> BString {
2486 let s = s.as_ref();
2487
2488 if s.iter().any(|&c| !is_atext(c)) {
2489 let mut result = Vec::<u8>::with_capacity(s.len() + 4);
2490 result.push(b'"');
2491 for (start, end, c) in s.char_indices() {
2492 let c = c as u32;
2493 if c <= 0xff {
2494 let c = c as u8;
2495 if !c.is_ascii_whitespace() && !is_qtext(c) && !is_atext(c) {
2496 result.push(b'\\');
2497 }
2498 }
2499 result.push_str(&s[start..end]);
2500 }
2501 result.push(b'"');
2502 result.into()
2503 } else {
2504 s.into()
2505 }
2506}
2507
2508#[cfg(test)]
2509#[test]
2510fn test_quote_string() {
2511 k9::snapshot!(
2512 quote_string("TEST [ne_pas_repondre]"),
2513 r#""TEST [ne_pas_repondre]""#
2514 );
2515 k9::snapshot!(quote_string("hello"), "hello");
2516 k9::snapshot!(quote_string("hello there"), r#""hello there""#);
2517 k9::snapshot!(quote_string("hello, there"), "\"hello, there\"");
2518 k9::snapshot!(quote_string("hello \"there\""), r#""hello \\"there\\"""#);
2519 k9::snapshot!(
2520 quote_string("hello c:\\backslash"),
2521 r#""hello c:\\\\backslash""#
2522 );
2523 k9::assert_equal!(quote_string("hello\n there"), "\"hello\n there\"");
2524}
2525
2526impl EncodeHeaderValue for Mailbox {
2527 fn encode_value(&self) -> SharedString<'static> {
2528 match &self.name {
2529 Some(name) => {
2530 let mut value: Vec<u8> = if name.is_ascii() {
2531 quote_string(name).into()
2532 } else {
2533 qp_encode(name.as_bytes()).into_bytes()
2534 };
2535
2536 value.push_str(" <");
2537 value.push_str(self.address.encode_value().as_bytes());
2538 value.push(b'>');
2539 value.into()
2540 }
2541 None => {
2542 let mut result: Vec<u8> = vec![];
2543 result.push(b'<');
2544 result.push_str(self.address.encode_value().as_bytes());
2545 result.push(b'>');
2546 result.into()
2547 }
2548 }
2549 }
2550}
2551
2552impl EncodeHeaderValue for MailboxList {
2553 fn encode_value(&self) -> SharedString<'static> {
2554 let mut result: Vec<u8> = vec![];
2555 for mailbox in &self.0 {
2556 if !result.is_empty() {
2557 result.push_str(",\r\n\t");
2558 }
2559 result.push_str(mailbox.encode_value().as_bytes());
2560 }
2561 result.into()
2562 }
2563}
2564
2565impl EncodeHeaderValue for Address {
2566 fn encode_value(&self) -> SharedString<'static> {
2567 match self {
2568 Self::Mailbox(mbox) => mbox.encode_value(),
2569 Self::Group { name, entries } => {
2570 let mut result: Vec<u8> = vec![];
2571 result.push_str(name);
2572 result.push(b':');
2573 result.push_str(entries.encode_value().as_bytes());
2574 result.push(b';');
2575 result.into()
2576 }
2577 }
2578 }
2579}
2580
2581impl EncodeHeaderValue for AddressList {
2582 fn encode_value(&self) -> SharedString<'static> {
2583 let mut result: Vec<u8> = vec![];
2584 for address in &self.0 {
2585 if !result.is_empty() {
2586 result.push_str(",\r\n\t");
2587 }
2588 result.push_str(address.encode_value().as_bytes());
2589 }
2590 result.into()
2591 }
2592}
2593
2594#[cfg(test)]
2595mod test {
2596 use super::*;
2597 use crate::{Header, MessageConformance, MimePart};
2598
2599 #[test]
2600 fn mailbox_encodes_at() {
2601 let mbox = Mailbox {
2602 name: Some("foo@bar.com".into()),
2603 address: AddrSpec {
2604 local_part: "foo".into(),
2605 domain: "bar.com".into(),
2606 },
2607 };
2608 assert_eq!(mbox.encode_value(), "\"foo@bar.com\" <foo@bar.com>");
2609 }
2610
2611 #[test]
2612 fn mailbox_list_singular() {
2613 let message = concat!(
2614 "From: Someone (hello) <someone@example.com>, other@example.com,\n",
2615 " \"John \\\"Smith\\\"\" (comment) \"More Quotes\" (more comment) <someone(another comment)@crazy.example.com(woot)>\n",
2616 "\n",
2617 "I am the body"
2618 );
2619 let msg = MimePart::parse(message).unwrap();
2620 let list = match msg.headers().from() {
2621 Err(err) => panic!("Doh.\n{err:#}"),
2622 Ok(list) => list,
2623 };
2624
2625 k9::snapshot!(
2626 list,
2627 r#"
2628Some(
2629 MailboxList(
2630 [
2631 Mailbox {
2632 name: Some(
2633 "Someone",
2634 ),
2635 address: AddrSpec {
2636 local_part: "someone",
2637 domain: "example.com",
2638 },
2639 },
2640 Mailbox {
2641 name: None,
2642 address: AddrSpec {
2643 local_part: "other",
2644 domain: "example.com",
2645 },
2646 },
2647 Mailbox {
2648 name: Some(
2649 "John "Smith" More Quotes",
2650 ),
2651 address: AddrSpec {
2652 local_part: "someone",
2653 domain: "crazy.example.com",
2654 },
2655 },
2656 ],
2657 ),
2658)
2659"#
2660 );
2661 }
2662
2663 #[test]
2664 fn docomo_non_compliant_localpart() {
2665 let message = "Sender: hello..there@docomo.ne.jp\n\n\n";
2666 let msg = MimePart::parse(message).unwrap();
2667 let err = msg.headers().sender().unwrap_err();
2668 k9::snapshot!(
2669 err,
2670 r#"
2671InvalidHeaderValueDuringGet {
2672 header_name: "Sender",
2673 error: HeaderParse(
2674 "Error at line 1, expected "@" but found ".":
2675hello..there@docomo.ne.jp
2676 ^___________________
2677
2678while parsing addr_spec
2679while parsing mailbox
2680",
2681 ),
2682}
2683"#
2684 );
2685 }
2686
2687 #[test]
2688 fn sender() {
2689 let message = "Sender: someone@[127.0.0.1]\n\n\n";
2690 let msg = MimePart::parse(message).unwrap();
2691 let list = match msg.headers().sender() {
2692 Err(err) => panic!("Doh.\n{err:#}"),
2693 Ok(list) => list,
2694 };
2695 k9::snapshot!(
2696 list,
2697 r#"
2698Some(
2699 Mailbox {
2700 name: None,
2701 address: AddrSpec {
2702 local_part: "someone",
2703 domain: "[127.0.0.1]",
2704 },
2705 },
2706)
2707"#
2708 );
2709 }
2710
2711 #[test]
2712 fn domain_literal() {
2713 let message = "From: someone@[127.0.0.1]\n\n\n";
2714 let msg = MimePart::parse(message).unwrap();
2715 let list = match msg.headers().from() {
2716 Err(err) => panic!("Doh.\n{err:#}"),
2717 Ok(list) => list,
2718 };
2719 k9::snapshot!(
2720 list,
2721 r#"
2722Some(
2723 MailboxList(
2724 [
2725 Mailbox {
2726 name: None,
2727 address: AddrSpec {
2728 local_part: "someone",
2729 domain: "[127.0.0.1]",
2730 },
2731 },
2732 ],
2733 ),
2734)
2735"#
2736 );
2737 }
2738
2739 #[test]
2740 fn rfc6532() {
2741 let message = concat!(
2742 "From: Keith Moore <moore@cs.utk.edu>\n",
2743 "To: Keld Jørn Simonsen <keld@dkuug.dk>\n",
2744 "CC: André Pirard <PIRARD@vm1.ulg.ac.be>\n",
2745 "Subject: Hello André\n",
2746 "\n\n"
2747 );
2748 let msg = MimePart::parse(message).unwrap();
2749 let list = match msg.headers().from() {
2750 Err(err) => panic!("Doh.\n{err:#}"),
2751 Ok(list) => list,
2752 };
2753 k9::snapshot!(
2754 list,
2755 r#"
2756Some(
2757 MailboxList(
2758 [
2759 Mailbox {
2760 name: Some(
2761 "Keith Moore",
2762 ),
2763 address: AddrSpec {
2764 local_part: "moore",
2765 domain: "cs.utk.edu",
2766 },
2767 },
2768 ],
2769 ),
2770)
2771"#
2772 );
2773
2774 let list = match msg.headers().to() {
2775 Err(err) => panic!("Doh.\n{err:#}"),
2776 Ok(list) => list,
2777 };
2778 k9::snapshot!(
2779 list,
2780 r#"
2781Some(
2782 AddressList(
2783 [
2784 Mailbox(
2785 Mailbox {
2786 name: Some(
2787 "Keld Jørn Simonsen",
2788 ),
2789 address: AddrSpec {
2790 local_part: "keld",
2791 domain: "dkuug.dk",
2792 },
2793 },
2794 ),
2795 ],
2796 ),
2797)
2798"#
2799 );
2800
2801 let list = match msg.headers().cc() {
2802 Err(err) => panic!("Doh.\n{err:#}"),
2803 Ok(list) => list,
2804 };
2805 k9::snapshot!(
2806 list,
2807 r#"
2808Some(
2809 AddressList(
2810 [
2811 Mailbox(
2812 Mailbox {
2813 name: Some(
2814 "André Pirard",
2815 ),
2816 address: AddrSpec {
2817 local_part: "PIRARD",
2818 domain: "vm1.ulg.ac.be",
2819 },
2820 },
2821 ),
2822 ],
2823 ),
2824)
2825"#
2826 );
2827 let list = match msg.headers().subject() {
2828 Err(err) => panic!("Doh.\n{err:#}"),
2829 Ok(list) => list,
2830 };
2831 k9::snapshot!(
2832 list,
2833 r#"
2834Some(
2835 "Hello André",
2836)
2837"#
2838 );
2839 }
2840
2841 #[test]
2842 fn unstructured_bare_non_ascii() {
2843 let message = "Subject: Héllo wörld äöü\n\n\n";
2846 let msg = MimePart::parse(message).unwrap();
2847 k9::snapshot!(
2848 msg.headers().subject().unwrap(),
2849 r#"
2850Some(
2851 "Héllo wörld äöü",
2852)
2853"#
2854 );
2855
2856 let message = "Subject: 件名テスト\n\n\n";
2858 let msg = MimePart::parse(message).unwrap();
2859 k9::snapshot!(
2860 msg.headers().subject().unwrap(),
2861 r#"
2862Some(
2863 "件名テスト",
2864)
2865"#
2866 );
2867 }
2868
2869 #[test]
2870 fn unstructured_raw_shift_jis() {
2871 let message = b"Subject: \x83\x65\x83\x58\x83\x67\n\n\n";
2878
2879 let msg = MimePart::parse(message.as_slice()).unwrap();
2882 let subject_header = msg.headers().get_first("Subject").unwrap();
2883 k9::assert_equal!(
2884 subject_header.get_raw_value(),
2885 b"\x83\x65\x83\x58\x83\x67".as_slice()
2886 );
2887
2888 k9::snapshot!(
2891 msg.headers().subject(),
2892 r#"
2893Err(
2894 InvalidHeaderValueDuringGet {
2895 header_name: "Subject",
2896 error: HeaderParse(
2897 "Error at line 1, in Eof:
2898\\x83e\\x83X\\x83g
2899^_____
2900
2901",
2902 ),
2903 },
2904)
2905"#
2906 );
2907 }
2908
2909 #[test]
2910 fn rfc2047_bogus() {
2911 let message = concat!(
2912 "From: =?US-OSCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2913 "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2914 "CC: =?ISO-8859-1?Q?Andr=E?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2915 "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?=\n",
2916 " =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2917 "\n\n"
2918 );
2919 let msg = MimePart::parse(message).unwrap();
2920
2921 k9::assert_equal!(
2924 msg.headers().from().unwrap().unwrap().0[0]
2925 .name
2926 .as_ref()
2927 .unwrap(),
2928 "=?US-OSCII?Q?Keith_Moore?="
2929 );
2930
2931 match &msg.headers().cc().unwrap().unwrap().0[0] {
2932 Address::Mailbox(mbox) => {
2933 k9::assert_equal!(mbox.name.as_ref().unwrap(), "Andr=E Pirard");
2937 }
2938 wat => panic!("should not have {wat:?}"),
2939 }
2940
2941 k9::assert_equal!(
2944 msg.headers().subject().unwrap().unwrap(),
2945 "Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?= u understand the example."
2946 );
2947 }
2948
2949 #[test]
2950 fn attachment_filename_mess_totally_bogus() {
2951 let message = concat!("Content-Disposition: attachment; filename=@\n", "\n\n");
2952 let msg = MimePart::parse(message).unwrap();
2953 eprintln!("{msg:#?}");
2954
2955 assert!(msg
2956 .conformance()
2957 .contains(MessageConformance::INVALID_MIME_HEADERS));
2958 msg.headers().content_disposition().unwrap_err();
2959
2960 let rebuilt = msg.rebuild(None).unwrap();
2963 k9::assert_equal!(rebuilt.headers().content_disposition(), Ok(None));
2964 }
2965
2966 #[test]
2967 fn attachment_filename_mess_aberrant() {
2968 let message = concat!(
2969 "Content-Disposition: attachment; filename= =?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\n",
2970 "\n\n"
2971 );
2972 let msg = MimePart::parse(message).unwrap();
2973
2974 let cd = msg.headers().content_disposition().unwrap().unwrap();
2975 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
2976
2977 let encoded = cd.encode_value();
2978 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?=");
2979 }
2980
2981 #[test]
2982 fn attachment_filename_mess_gmail() {
2983 let message = concat!(
2984 "Content-Disposition: attachment; filename=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
2985 "Content-Type: text/plain;\n",
2986 " name=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
2987 "\n\n"
2988 );
2989 let msg = MimePart::parse(message).unwrap();
2990
2991 let cd = msg.headers().content_disposition().unwrap().unwrap();
2992 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
2993 let encoded = cd.encode_value();
2994 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?=\"");
2995
2996 let ct = msg.headers().content_type().unwrap().unwrap();
2997 k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付");
2998 }
2999
3000 #[test]
3001 fn attachment_filename_mess_fastmail() {
3002 let message = concat!(
3003 "Content-Disposition: attachment;\n",
3004 " filename*0*=utf-8''%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E6%B7%BB%E4%BB%98;\n",
3005 " filename*1*=.txt\n",
3006 "Content-Type: text/plain;\n",
3007 " name=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=\"\n",
3008 " 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",
3009 "\n\n"
3010 );
3011 let msg = MimePart::parse(message).unwrap();
3012
3013 let cd = msg.headers().content_disposition().unwrap().unwrap();
3014 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付.txt");
3015
3016 let ct = msg.headers().content_type().unwrap().unwrap();
3017 eprintln!("{ct:#?}");
3018 k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付.txt");
3019 k9::assert_equal!(
3020 ct.get("x-name").unwrap(),
3021 "=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=bork"
3022 );
3023 }
3024
3025 #[test]
3026 fn rfc2047() {
3027 let message = concat!(
3028 "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
3029 "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
3030 "CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
3031 "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n",
3032 " =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
3033 "\n\n"
3034 );
3035 let msg = MimePart::parse(message).unwrap();
3036 let list = match msg.headers().from() {
3037 Err(err) => panic!("Doh.\n{err:#}"),
3038 Ok(list) => list,
3039 };
3040 k9::snapshot!(
3041 list,
3042 r#"
3043Some(
3044 MailboxList(
3045 [
3046 Mailbox {
3047 name: Some(
3048 "Keith Moore",
3049 ),
3050 address: AddrSpec {
3051 local_part: "moore",
3052 domain: "cs.utk.edu",
3053 },
3054 },
3055 ],
3056 ),
3057)
3058"#
3059 );
3060
3061 let list = match msg.headers().to() {
3062 Err(err) => panic!("Doh.\n{err:#}"),
3063 Ok(list) => list,
3064 };
3065 k9::snapshot!(
3066 list,
3067 r#"
3068Some(
3069 AddressList(
3070 [
3071 Mailbox(
3072 Mailbox {
3073 name: Some(
3074 "Keld Jørn Simonsen",
3075 ),
3076 address: AddrSpec {
3077 local_part: "keld",
3078 domain: "dkuug.dk",
3079 },
3080 },
3081 ),
3082 ],
3083 ),
3084)
3085"#
3086 );
3087
3088 let list = match msg.headers().cc() {
3089 Err(err) => panic!("Doh.\n{err:#}"),
3090 Ok(list) => list,
3091 };
3092 k9::snapshot!(
3093 list,
3094 r#"
3095Some(
3096 AddressList(
3097 [
3098 Mailbox(
3099 Mailbox {
3100 name: Some(
3101 "André Pirard",
3102 ),
3103 address: AddrSpec {
3104 local_part: "PIRARD",
3105 domain: "vm1.ulg.ac.be",
3106 },
3107 },
3108 ),
3109 ],
3110 ),
3111)
3112"#
3113 );
3114 let list = match msg.headers().subject() {
3115 Err(err) => panic!("Doh.\n{err:#}"),
3116 Ok(list) => list,
3117 };
3118 k9::snapshot!(
3119 list,
3120 r#"
3121Some(
3122 "Hello If you can read this you understand the example.",
3123)
3124"#
3125 );
3126
3127 k9::snapshot!(
3128 BString::from(msg.rebuild(None).unwrap().to_message_bytes()),
3129 r#"
3130Content-Type: text/plain;\r
3131\tcharset="us-ascii"\r
3132Content-Transfer-Encoding: quoted-printable\r
3133From: "Keith Moore" <moore@cs.utk.edu>\r
3134To: =?UTF-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r
3135Cc: =?UTF-8?q?Andr=C3=A9_Pirard?= <PIRARD@vm1.ulg.ac.be>\r
3136Subject: Hello If you can read this you understand the example.\r
3137\r
3138=0A\r
3139
3140"#
3141 );
3142 }
3143
3144 #[test]
3145 fn group_addresses() {
3146 let message = concat!(
3147 "To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;\n",
3148 "Cc: Undisclosed recipients:;\n",
3149 "\n\n\n"
3150 );
3151 let msg = MimePart::parse(message).unwrap();
3152 let list = match msg.headers().to() {
3153 Err(err) => panic!("Doh.\n{err:#}"),
3154 Ok(list) => list.unwrap(),
3155 };
3156
3157 k9::snapshot!(
3158 list.encode_value(),
3159 r#"
3160A Group:"Ed Jones" <c@a.test>,\r
3161\t<joe@where.test>,\r
3162\tJohn <jdoe@one.test>;
3163"#
3164 );
3165
3166 let round_trip = Header::new("To", list.clone());
3167 k9::assert_equal!(list, round_trip.as_address_list().unwrap());
3168
3169 k9::snapshot!(
3170 list,
3171 r#"
3172AddressList(
3173 [
3174 Group {
3175 name: "A Group",
3176 entries: MailboxList(
3177 [
3178 Mailbox {
3179 name: Some(
3180 "Ed Jones",
3181 ),
3182 address: AddrSpec {
3183 local_part: "c",
3184 domain: "a.test",
3185 },
3186 },
3187 Mailbox {
3188 name: None,
3189 address: AddrSpec {
3190 local_part: "joe",
3191 domain: "where.test",
3192 },
3193 },
3194 Mailbox {
3195 name: Some(
3196 "John",
3197 ),
3198 address: AddrSpec {
3199 local_part: "jdoe",
3200 domain: "one.test",
3201 },
3202 },
3203 ],
3204 ),
3205 },
3206 ],
3207)
3208"#
3209 );
3210
3211 let list = match msg.headers().cc() {
3212 Err(err) => panic!("Doh.\n{err:#}"),
3213 Ok(list) => list,
3214 };
3215 k9::snapshot!(
3216 list,
3217 r#"
3218Some(
3219 AddressList(
3220 [
3221 Group {
3222 name: "Undisclosed recipients",
3223 entries: MailboxList(
3224 [],
3225 ),
3226 },
3227 ],
3228 ),
3229)
3230"#
3231 );
3232 }
3233
3234 #[test]
3235 fn message_id() {
3236 let message = concat!(
3237 "Message-Id: <foo@example.com>\n",
3238 "References: <a@example.com> <b@example.com>\n",
3239 " <\"legacy\"@example.com>\n",
3240 " <literal@[127.0.0.1]>\n",
3241 "\n\n\n"
3242 );
3243 let msg = MimePart::parse(message).unwrap();
3244 let list = match msg.headers().message_id() {
3245 Err(err) => panic!("Doh.\n{err:#}"),
3246 Ok(list) => list,
3247 };
3248 k9::snapshot!(
3249 list,
3250 r#"
3251Some(
3252 MessageID(
3253 "foo@example.com",
3254 ),
3255)
3256"#
3257 );
3258
3259 let list = match msg.headers().references() {
3260 Err(err) => panic!("Doh.\n{err:#}"),
3261 Ok(list) => list,
3262 };
3263 k9::snapshot!(
3264 list,
3265 r#"
3266Some(
3267 [
3268 MessageID(
3269 "a@example.com",
3270 ),
3271 MessageID(
3272 "b@example.com",
3273 ),
3274 MessageID(
3275 "legacy@example.com",
3276 ),
3277 MessageID(
3278 "literal@[127.0.0.1]",
3279 ),
3280 ],
3281)
3282"#
3283 );
3284 }
3285
3286 #[test]
3287 fn content_type() {
3288 let message = "Content-Type: text/plain\n\n\n\n";
3289 let msg = MimePart::parse(message).unwrap();
3290 let params = match msg.headers().content_type() {
3291 Err(err) => panic!("Doh.\n{err:#}"),
3292 Ok(params) => params,
3293 };
3294 k9::snapshot!(
3295 params,
3296 r#"
3297Some(
3298 MimeParameters {
3299 value: "text/plain",
3300 parameters: [],
3301 },
3302)
3303"#
3304 );
3305
3306 let message = "Content-Type: text/plain; charset=us-ascii\n\n\n\n";
3307 let msg = MimePart::parse(message).unwrap();
3308 let params = match msg.headers().content_type() {
3309 Err(err) => panic!("Doh.\n{err:#}"),
3310 Ok(params) => params.unwrap(),
3311 };
3312
3313 k9::snapshot!(
3314 params.get("charset"),
3315 r#"
3316Some(
3317 "us-ascii",
3318)
3319"#
3320 );
3321 k9::snapshot!(
3322 params,
3323 r#"
3324MimeParameters {
3325 value: "text/plain",
3326 parameters: [
3327 MimeParameter {
3328 name: "charset",
3329 section: None,
3330 mime_charset: None,
3331 mime_language: None,
3332 encoding: None,
3333 value: "us-ascii",
3334 },
3335 ],
3336}
3337"#
3338 );
3339
3340 let message = "Content-Type: text/plain; charset=\"us-ascii\"\n\n\n\n";
3341 let msg = MimePart::parse(message).unwrap();
3342 let params = match msg.headers().content_type() {
3343 Err(err) => panic!("Doh.\n{err:#}"),
3344 Ok(params) => params,
3345 };
3346 k9::snapshot!(
3347 params,
3348 r#"
3349Some(
3350 MimeParameters {
3351 value: "text/plain",
3352 parameters: [
3353 MimeParameter {
3354 name: "charset",
3355 section: None,
3356 mime_charset: None,
3357 mime_language: None,
3358 encoding: None,
3359 value: "us-ascii",
3360 },
3361 ],
3362 },
3363)
3364"#
3365 );
3366 }
3367
3368 #[test]
3369 fn content_type_rfc2231() {
3370 let message = concat!(
3373 "Content-Type: application/x-stuff;\n",
3374 "\ttitle*0*=us-ascii'en'This%20is%20even%20more%20;\n",
3375 "\ttitle*1*=%2A%2A%2Afun%2A%2A%2A%20;\n",
3376 "\ttitle*2=\"isn't it!\"\n",
3377 "\n\n\n"
3378 );
3379 let msg = MimePart::parse(message).unwrap();
3380 let mut params = match msg.headers().content_type() {
3381 Err(err) => panic!("Doh.\n{err:#}"),
3382 Ok(params) => params.unwrap(),
3383 };
3384
3385 let original_title = params.get("title");
3386 k9::snapshot!(
3387 &original_title,
3388 r#"
3389Some(
3390 "This is even more ***fun*** isn't it!",
3391)
3392"#
3393 );
3394
3395 k9::snapshot!(
3396 ¶ms,
3397 r#"
3398MimeParameters {
3399 value: "application/x-stuff",
3400 parameters: [
3401 MimeParameter {
3402 name: "title",
3403 section: Some(
3404 0,
3405 ),
3406 mime_charset: Some(
3407 "us-ascii",
3408 ),
3409 mime_language: Some(
3410 "en",
3411 ),
3412 encoding: Rfc2231,
3413 value: "This%20is%20even%20more%20",
3414 },
3415 MimeParameter {
3416 name: "title",
3417 section: Some(
3418 1,
3419 ),
3420 mime_charset: None,
3421 mime_language: None,
3422 encoding: Rfc2231,
3423 value: "%2A%2A%2Afun%2A%2A%2A%20",
3424 },
3425 MimeParameter {
3426 name: "title",
3427 section: Some(
3428 2,
3429 ),
3430 mime_charset: None,
3431 mime_language: None,
3432 encoding: None,
3433 value: "isn't it!",
3434 },
3435 ],
3436}
3437"#
3438 );
3439
3440 k9::snapshot!(
3441 params.encode_value(),
3442 r#"
3443application/x-stuff;\r
3444\ttitle="This is even more ***fun*** isn't it!"
3445"#
3446 );
3447
3448 params.set("foo", "bar 💩");
3449
3450 params.set(
3451 "long",
3452 "this is some text that should wrap because \
3453 it should be a good bit longer than our target maximum \
3454 length for this sort of thing, and hopefully we see at \
3455 least three lines produced as a result of setting \
3456 this value in this way",
3457 );
3458
3459 params.set(
3460 "longernnamethananyoneshouldreallyuse",
3461 "this is some text that should wrap because \
3462 it should be a good bit longer than our target maximum \
3463 length for this sort of thing, and hopefully we see at \
3464 least three lines produced as a result of setting \
3465 this value in this way",
3466 );
3467
3468 k9::snapshot!(
3469 params.encode_value(),
3470 r#"
3471application/x-stuff;\r
3472\tfoo*=UTF-8''bar%20%F0%9F%92%A9;\r
3473\tlong*0="this is some text that should wrap because it should be a good bi";\r
3474\tlong*1="t longer than our target maximum length for this sort of thing, a";\r
3475\tlong*2="nd hopefully we see at least three lines produced as a result of ";\r
3476\tlong*3="setting this value in this way";\r
3477\tlongernnamethananyoneshouldreallyuse*0="this is some text that should wra";\r
3478\tlongernnamethananyoneshouldreallyuse*1="p because it should be a good bit";\r
3479\tlongernnamethananyoneshouldreallyuse*2=" longer than our target maximum l";\r
3480\tlongernnamethananyoneshouldreallyuse*3="ength for this sort of thing, and";\r
3481\tlongernnamethananyoneshouldreallyuse*4=" hopefully we see at least three ";\r
3482\tlongernnamethananyoneshouldreallyuse*5="lines produced as a result of set";\r
3483\tlongernnamethananyoneshouldreallyuse*6="ting this value in this way";\r
3484\ttitle="This is even more ***fun*** isn't it!"
3485"#
3486 );
3487 }
3488
3489 #[test]
3491 fn authentication_results_b_2() {
3492 let ar = Header::with_name_value("Authentication-Results", "example.org 1; none");
3493 let ar = ar.as_authentication_results().unwrap();
3494 k9::snapshot!(
3495 &ar,
3496 r#"
3497AuthenticationResults {
3498 serv_id: "example.org",
3499 version: Some(
3500 1,
3501 ),
3502 results: [],
3503}
3504"#
3505 );
3506
3507 k9::snapshot!(ar.encode_value(), "example.org 1; none");
3508 }
3509
3510 #[test]
3512 fn authentication_results_b_3() {
3513 let ar = Header::with_name_value(
3514 "Authentication-Results",
3515 "example.com; spf=pass smtp.mailfrom=example.net",
3516 );
3517 k9::snapshot!(
3518 ar.as_authentication_results(),
3519 r#"
3520Ok(
3521 AuthenticationResults {
3522 serv_id: "example.com",
3523 version: None,
3524 results: [
3525 AuthenticationResult {
3526 method: "spf",
3527 method_version: None,
3528 result: "pass",
3529 reason: None,
3530 props: {
3531 "smtp.mailfrom": "example.net",
3532 },
3533 },
3534 ],
3535 },
3536)
3537"#
3538 );
3539 }
3540
3541 #[test]
3543 fn authentication_results_b_4() {
3544 let ar = Header::with_name_value(
3545 "Authentication-Results",
3546 concat!(
3547 "example.com;\n",
3548 "\tauth=pass (cram-md5) smtp.auth=sender@example.net;\n",
3549 "\tspf=pass smtp.mailfrom=example.net"
3550 ),
3551 );
3552 k9::snapshot!(
3553 ar.as_authentication_results(),
3554 r#"
3555Ok(
3556 AuthenticationResults {
3557 serv_id: "example.com",
3558 version: None,
3559 results: [
3560 AuthenticationResult {
3561 method: "auth",
3562 method_version: None,
3563 result: "pass",
3564 reason: None,
3565 props: {
3566 "smtp.auth": "sender@example.net",
3567 },
3568 },
3569 AuthenticationResult {
3570 method: "spf",
3571 method_version: None,
3572 result: "pass",
3573 reason: None,
3574 props: {
3575 "smtp.mailfrom": "example.net",
3576 },
3577 },
3578 ],
3579 },
3580)
3581"#
3582 );
3583
3584 let ar = Header::with_name_value(
3585 "Authentication-Results",
3586 "example.com; iprev=pass\n\tpolicy.iprev=192.0.2.200",
3587 );
3588 k9::snapshot!(
3589 ar.as_authentication_results(),
3590 r#"
3591Ok(
3592 AuthenticationResults {
3593 serv_id: "example.com",
3594 version: None,
3595 results: [
3596 AuthenticationResult {
3597 method: "iprev",
3598 method_version: None,
3599 result: "pass",
3600 reason: None,
3601 props: {
3602 "policy.iprev": "192.0.2.200",
3603 },
3604 },
3605 ],
3606 },
3607)
3608"#
3609 );
3610 }
3611
3612 #[test]
3614 fn authentication_results_b_5() {
3615 let ar = Header::with_name_value(
3616 "Authentication-Results",
3617 "example.com;\n\tdkim=pass (good signature) header.d=example.com",
3618 );
3619 k9::snapshot!(
3620 ar.as_authentication_results(),
3621 r#"
3622Ok(
3623 AuthenticationResults {
3624 serv_id: "example.com",
3625 version: None,
3626 results: [
3627 AuthenticationResult {
3628 method: "dkim",
3629 method_version: None,
3630 result: "pass",
3631 reason: None,
3632 props: {
3633 "header.d": "example.com",
3634 },
3635 },
3636 ],
3637 },
3638)
3639"#
3640 );
3641
3642 let ar = Header::with_name_value(
3643 "Authentication-Results",
3644 "example.com;\n\tauth=pass (cram-md5) smtp.auth=sender@example.com;\n\tspf=fail smtp.mailfrom=example.com"
3645 );
3646 let ar = ar.as_authentication_results().unwrap();
3647 k9::snapshot!(
3648 &ar,
3649 r#"
3650AuthenticationResults {
3651 serv_id: "example.com",
3652 version: None,
3653 results: [
3654 AuthenticationResult {
3655 method: "auth",
3656 method_version: None,
3657 result: "pass",
3658 reason: None,
3659 props: {
3660 "smtp.auth": "sender@example.com",
3661 },
3662 },
3663 AuthenticationResult {
3664 method: "spf",
3665 method_version: None,
3666 result: "fail",
3667 reason: None,
3668 props: {
3669 "smtp.mailfrom": "example.com",
3670 },
3671 },
3672 ],
3673}
3674"#
3675 );
3676
3677 k9::snapshot!(
3678 ar.encode_value(),
3679 r#"
3680example.com;\r
3681\tauth=pass\r
3682\tsmtp.auth=sender@example.com;\r
3683\tspf=fail\r
3684\tsmtp.mailfrom=example.com
3685"#
3686 );
3687 }
3688
3689 #[test]
3691 fn authentication_results_b_6() {
3692 let ar = Header::with_name_value(
3693 "Authentication-Results",
3694 concat!(
3695 "example.com;\n",
3696 "\tdkim=pass reason=\"good signature\"\n",
3697 "\theader.i=@mail-router.example.net;\n",
3698 "\tdkim=fail reason=\"bad signature\"\n",
3699 "\theader.i=@newyork.example.com"
3700 ),
3701 );
3702 let ar = match ar.as_authentication_results() {
3703 Err(err) => panic!("\n{err}"),
3704 Ok(ar) => ar,
3705 };
3706
3707 k9::snapshot!(
3708 &ar,
3709 r#"
3710AuthenticationResults {
3711 serv_id: "example.com",
3712 version: None,
3713 results: [
3714 AuthenticationResult {
3715 method: "dkim",
3716 method_version: None,
3717 result: "pass",
3718 reason: Some(
3719 "good signature",
3720 ),
3721 props: {
3722 "header.i": "@mail-router.example.net",
3723 },
3724 },
3725 AuthenticationResult {
3726 method: "dkim",
3727 method_version: None,
3728 result: "fail",
3729 reason: Some(
3730 "bad signature",
3731 ),
3732 props: {
3733 "header.i": "@newyork.example.com",
3734 },
3735 },
3736 ],
3737}
3738"#
3739 );
3740
3741 k9::snapshot!(
3742 ar.encode_value(),
3743 r#"
3744example.com;\r
3745\tdkim=pass reason="good signature"\r
3746\theader.i=@mail-router.example.net;\r
3747\tdkim=fail reason="bad signature"\r
3748\theader.i=@newyork.example.com
3749"#
3750 );
3751
3752 let ar = Header::with_name_value(
3753 "Authentication-Results",
3754 concat!(
3755 "example.net;\n",
3756 "\tdkim=pass (good signature) header.i=@newyork.example.com"
3757 ),
3758 );
3759 let ar = match ar.as_authentication_results() {
3760 Err(err) => panic!("\n{err}"),
3761 Ok(ar) => ar,
3762 };
3763
3764 k9::snapshot!(
3765 &ar,
3766 r#"
3767AuthenticationResults {
3768 serv_id: "example.net",
3769 version: None,
3770 results: [
3771 AuthenticationResult {
3772 method: "dkim",
3773 method_version: None,
3774 result: "pass",
3775 reason: None,
3776 props: {
3777 "header.i": "@newyork.example.com",
3778 },
3779 },
3780 ],
3781}
3782"#
3783 );
3784
3785 k9::snapshot!(
3786 ar.encode_value(),
3787 r#"
3788example.net;\r
3789\tdkim=pass\r
3790\theader.i=@newyork.example.com
3791"#
3792 );
3793 }
3794
3795 #[test]
3797 fn authentication_results_b_7() {
3798 let ar = Header::with_name_value(
3799 "Authentication-Results",
3800 concat!(
3801 "foo.example.net (foobar) 1 (baz);\n",
3802 "\tdkim (Because I like it) / 1 (One yay) = (wait for it) fail\n",
3803 "\tpolicy (A dot can go here) . (like that) expired\n",
3804 "\t(this surprised me) = (as I wasn't expecting it) 1362471462"
3805 ),
3806 );
3807 let ar = match ar.as_authentication_results() {
3808 Err(err) => panic!("\n{err}"),
3809 Ok(ar) => ar,
3810 };
3811
3812 k9::snapshot!(
3813 &ar,
3814 r#"
3815AuthenticationResults {
3816 serv_id: "foo.example.net",
3817 version: Some(
3818 1,
3819 ),
3820 results: [
3821 AuthenticationResult {
3822 method: "dkim",
3823 method_version: Some(
3824 1,
3825 ),
3826 result: "fail",
3827 reason: None,
3828 props: {
3829 "policy.expired": "1362471462",
3830 },
3831 },
3832 ],
3833}
3834"#
3835 );
3836
3837 k9::snapshot!(
3838 ar.encode_value(),
3839 r#"
3840foo.example.net 1;\r
3841\tdkim/1=fail\r
3842\tpolicy.expired=1362471462
3843"#
3844 );
3845 }
3846
3847 #[test]
3848 fn arc_authentication_results_1() {
3849 let ar = Header::with_name_value(
3850 "ARC-Authentication-Results",
3851 "i=3; clochette.example.org; spf=fail
3852 smtp.from=jqd@d1.example; dkim=fail (512-bit key)
3853 header.i=@d1.example; dmarc=fail; arc=pass (as.2.gmail.example=pass,
3854 ams.2.gmail.example=pass, as.1.lists.example.org=pass,
3855 ams.1.lists.example.org=fail (message has been altered))",
3856 );
3857 let ar = match ar.as_arc_authentication_results() {
3858 Err(err) => panic!("\n{err}"),
3859 Ok(ar) => ar,
3860 };
3861
3862 k9::snapshot!(
3863 &ar,
3864 r#"
3865ARCAuthenticationResults {
3866 instance: 3,
3867 serv_id: "clochette.example.org",
3868 version: None,
3869 results: [
3870 AuthenticationResult {
3871 method: "spf",
3872 method_version: None,
3873 result: "fail",
3874 reason: None,
3875 props: {
3876 "smtp.from": "jqd@d1.example",
3877 },
3878 },
3879 AuthenticationResult {
3880 method: "dkim",
3881 method_version: None,
3882 result: "fail",
3883 reason: None,
3884 props: {
3885 "header.i": "@d1.example",
3886 },
3887 },
3888 AuthenticationResult {
3889 method: "dmarc",
3890 method_version: None,
3891 result: "fail",
3892 reason: None,
3893 props: {},
3894 },
3895 AuthenticationResult {
3896 method: "arc",
3897 method_version: None,
3898 result: "pass",
3899 reason: None,
3900 props: {},
3901 },
3902 ],
3903}
3904"#
3905 );
3906 }
3907
3908 #[test]
3909 fn bstring_utf8_serializes_utf8_as_string() {
3910 let mid = MessageID(BString::from("abc123@example.com"));
3912 let json = serde_json::to_string(&mid).unwrap();
3913 k9::assert_equal!(json, r#""abc123@example.com""#);
3914 }
3915
3916 #[test]
3917 fn bstring_utf8_serializes_non_utf8_as_array() {
3918 let mid = MessageID(BString::from(&b"hello\x80world"[..]));
3920 let json = serde_json::to_string(&mid).unwrap();
3921 k9::assert_equal!(json, "[104,101,108,108,111,128,119,111,114,108,100]");
3922 }
3923
3924 #[test]
3925 fn bstring_utf8_round_trip_utf8() {
3926 let mid = MessageID(BString::from("test@example.com"));
3927 let json = serde_json::to_string(&mid).unwrap();
3928 let restored: MessageID = serde_json::from_str(&json).unwrap();
3929 k9::assert_equal!(restored, mid);
3930 }
3931
3932 #[test]
3933 fn bstring_utf8_round_trip_non_utf8() {
3934 let mid = MessageID(BString::from(&b"\xff\xfe"[..]));
3935 let json = serde_json::to_string(&mid).unwrap();
3936 let restored: MessageID = serde_json::from_str(&json).unwrap();
3937 k9::assert_equal!(restored, mid);
3938 }
3939
3940 #[test]
3941 fn authentication_results_serialize_as_strings() {
3942 let ar = AuthenticationResults {
3943 serv_id: BString::from("example.com"),
3944 version: None,
3945 results: vec![AuthenticationResult {
3946 method: "dkim".into(),
3947 method_version: None,
3948 result: "pass".into(),
3949 reason: Some(BString::from("good signature")),
3950 props: BTreeMap::from([
3951 ("header.d".into(), BString::from("example.com")),
3952 ("header.s".into(), BString::from("selector1")),
3953 ]),
3954 }],
3955 };
3956 let json = serde_json::to_string_pretty(&ar).unwrap();
3957 k9::assert_equal!(
3959 json,
3960 r#"{
3961 "serv_id": "example.com",
3962 "version": null,
3963 "results": [
3964 {
3965 "method": "dkim",
3966 "method_version": null,
3967 "result": "pass",
3968 "reason": "good signature",
3969 "props": {
3970 "header.d": "example.com",
3971 "header.s": "selector1"
3972 }
3973 }
3974 ]
3975}"#
3976 );
3977 }
3978
3979 #[test]
3980 fn authentication_results_round_trip() {
3981 let ar = AuthenticationResults {
3982 serv_id: BString::from("mx.example.org"),
3983 version: Some(1),
3984 results: vec![AuthenticationResult {
3985 method: "spf".into(),
3986 method_version: None,
3987 result: "pass".into(),
3988 reason: None,
3989 props: BTreeMap::from([(
3990 "smtp.mailfrom".into(),
3991 BString::from("sender@example.com"),
3992 )]),
3993 }],
3994 };
3995 let json = serde_json::to_string(&ar).unwrap();
3996 let restored: AuthenticationResults = serde_json::from_str(&json).unwrap();
3997 k9::assert_equal!(restored, ar);
3998 }
3999
4000 #[test]
4001 fn authentication_result_non_utf8_reason() {
4002 let ar = AuthenticationResult {
4003 method: "dkim".into(),
4004 method_version: None,
4005 result: "temperror".into(),
4006 reason: Some(BString::from(&b"bad\x80data"[..])),
4007 props: BTreeMap::new(),
4008 };
4009 let json = serde_json::to_string(&ar).unwrap();
4010 assert!(json.contains(r#""reason":[98,97,100,128,100,97,116,97]"#));
4012 let restored: AuthenticationResult = serde_json::from_str(&json).unwrap();
4013 k9::assert_equal!(restored, ar);
4014 }
4015
4016 #[test]
4017 fn authentication_results_encode_value_with_binary() {
4018 let ar = AuthenticationResults {
4021 serv_id: BString::from(&b"mx.ex\x80mple.com"[..]),
4022 version: None,
4023 results: vec![AuthenticationResult {
4024 method: "spf".into(),
4025 method_version: None,
4026 result: "pass".into(),
4027 reason: Some(BString::from(&b"good\xffsig"[..])),
4028 props: BTreeMap::from([(
4029 "smtp.mailfrom".into(),
4030 BString::from(&b"user@\xfehost"[..]),
4031 )]),
4032 }],
4033 };
4034 let encoded = ar.encode_value();
4035 k9::snapshot!(
4036 encoded,
4037 r#"
4038"mx.ex\x80mple.com";\r
4039\tspf=pass reason="good\xffsig"\r
4040\tsmtp.mailfrom="user@\xfehost"
4041"#
4042 );
4043 }
4044
4045 #[test]
4046 fn authentication_results_serv_id_quoting() {
4047 let ar = AuthenticationResults {
4049 serv_id: BString::from("mx example.com"),
4050 version: None,
4051 results: vec![],
4052 };
4053 let encoded = ar.encode_value();
4054 k9::snapshot!(encoded, r#""mx example.com"; none"#);
4055
4056 let ar2 = AuthenticationResults {
4058 serv_id: BString::from("mx.example.com"),
4059 version: Some(1),
4060 results: vec![],
4061 };
4062 let encoded2 = ar2.encode_value();
4063 k9::snapshot!(&encoded2, "mx.example.com 1; none");
4064 let parsed = Parser::parse_authentication_results_header(encoded2.as_bytes()).unwrap();
4066 k9::assert_equal!(parsed.serv_id, ar2.serv_id);
4067 k9::assert_equal!(parsed.version, Some(1));
4068 }
4069
4070 #[test]
4071 fn arc_authentication_results_serialize_as_strings() {
4072 let arc = ARCAuthenticationResults {
4073 instance: 1,
4074 serv_id: BString::from("mx.example.com"),
4075 version: None,
4076 results: vec![],
4077 };
4078 let json = serde_json::to_string(&arc).unwrap();
4079 k9::assert_equal!(
4080 json,
4081 r#"{"instance":1,"serv_id":"mx.example.com","version":null,"results":[]}"#
4082 );
4083 }
4084}