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
922pub(crate) fn strip_cfws(input: &str) -> Option<String> {
926 fn token(input: Span) -> IResult<Span, Span> {
927 take_while1(|c| !matches!(c, b' ' | b'\t' | b'\r' | b'\n' | b'(')).parse(input)
928 }
929 fn tokens(input: Span) -> IResult<Span, Vec<Option<Span>>> {
930 many0(alt((map(cfws, |_| None), map(token, Some)))).parse(input)
931 }
932
933 let mut out: Vec<u8> = Vec::new();
934 for token in parse_with(input.as_bytes(), tokens)
935 .ok()?
936 .into_iter()
937 .flatten()
938 {
939 if !out.is_empty() {
940 out.push(b' ');
941 }
942 out.extend_from_slice(token.fragment());
943 }
944 String::from_utf8(out).ok()
945}
946
947#[cfg(test)]
948#[test]
949fn test_strip_cfws() {
950 k9::assert_equal!(
951 strip_cfws("Thu, 02 Jul 26 18:55:38 UTC (Coordinated)").unwrap(),
952 "Thu, 02 Jul 26 18:55:38 UTC"
953 );
954 k9::assert_equal!(
955 strip_cfws("Thu, 02 Jul 26 (comment) 18:55:38 UTC").unwrap(),
956 "Thu, 02 Jul 26 18:55:38 UTC"
957 );
958 k9::assert_equal!(strip_cfws("a (b (c) \\) d) e").unwrap(), "a e");
960}
961
962fn is_quoted_pair_ascii(c: u8) -> bool {
963 match c {
964 0x00 | b'\r' | b'\n' | b' ' => true,
965 c => is_obs_no_ws_ctl(c) || is_vchar_ascii(c),
966 }
967}
968
969fn is_quoted_pair(c: u8) -> bool {
973 is_quoted_pair_ascii(c) || c >= 0x80
974}
975
976fn quoted_pair(input: Span) -> IResult<Span, Span> {
979 context(
980 "quoted_pair",
981 preceded(
982 tag("\\"),
983 alt((take_while_m_n(1, 1, is_quoted_pair_ascii), utf8_non_ascii)),
984 ),
985 )
986 .parse(input)
987}
988
989fn encoded_word(input: Span) -> IResult<Span, BString> {
991 let (loc, (charset, _language, _, encoding, _, text)) = context(
992 "encoded_word",
993 delimited(
994 tag("=?"),
995 (
996 charset,
997 opt(preceded(tag("*"), language)),
998 tag("?"),
999 encoding,
1000 tag("?"),
1001 encoded_text,
1002 ),
1003 tag("?="),
1004 ),
1005 )
1006 .parse(input)?;
1007
1008 let bytes = match *encoding.fragment() {
1009 b"B" | b"b" => data_encoding::BASE64_MIME
1010 .decode(text.as_bytes())
1011 .map_err(|err| {
1012 make_context_error(
1013 input,
1014 format!("encoded_word: base64 decode failed: {err:#}"),
1015 )
1016 })?,
1017 b"Q" | b"q" => {
1018 let munged = text.replace("_", " ");
1020 let had_trailing_space = munged.ends_with_str(" ");
1023 let mut decoded = quoted_printable::decode(munged, quoted_printable::ParseMode::Robust)
1024 .map_err(|err| {
1025 make_context_error(
1026 input,
1027 format!("encoded_word: quoted printable decode failed: {err:#}"),
1028 )
1029 })?;
1030 if had_trailing_space && !decoded.ends_with(b" ") {
1031 decoded.push(b' ');
1032 }
1033 decoded
1034 }
1035 encoding => {
1036 let encoding = BStr::new(encoding);
1037 return Err(make_context_error(
1038 input,
1039 format!(
1040 "encoded_word: invalid encoding '{encoding}', expected one of b, B, q or Q"
1041 ),
1042 ));
1043 }
1044 };
1045
1046 let charset_name = charset.to_str().map_err(|err| {
1047 make_context_error(
1048 input,
1049 format!(
1050 "encoded_word: charset {} is not UTF-8: {err}",
1051 BStr::new(*charset)
1052 ),
1053 )
1054 })?;
1055
1056 let charset = Encoding::by_name(&*charset_name).ok_or_else(|| {
1057 make_context_error(
1058 input,
1059 format!("encoded_word: unsupported charset '{charset_name}'"),
1060 )
1061 })?;
1062
1063 let decoded = charset.decode_simple(&bytes).map_err(|err| {
1064 make_context_error(
1065 input,
1066 format!("encoded_word: failed to decode as '{charset_name}': {err}"),
1067 )
1068 })?;
1069
1070 Ok((loc, decoded.into()))
1071}
1072
1073fn charset(input: Span) -> IResult<Span, Span> {
1075 context("charset", take_while1(|c| c != b'*' && is_token(c))).parse(input)
1076}
1077
1078fn language(input: Span) -> IResult<Span, Span> {
1080 context("language", take_while1(|c| c != b'*' && is_token(c))).parse(input)
1081}
1082
1083fn encoding(input: Span) -> IResult<Span, Span> {
1085 context("encoding", take_while1(|c| c != b'*' && is_token(c))).parse(input)
1086}
1087
1088fn encoded_text(input: Span) -> IResult<Span, Span> {
1090 context(
1091 "encoded_text",
1092 recognize(many1(alt((
1093 take_while1(|c| is_vchar_ascii(c) && c != b' ' && c != b'?'),
1094 utf8_non_ascii,
1095 )))),
1096 )
1097 .parse(input)
1098}
1099
1100fn quoted_string(input: Span) -> IResult<Span, BString> {
1102 let (loc, (bits, trailer)) = context(
1103 "quoted_string",
1104 delimited(
1105 opt(cfws),
1106 delimited(
1107 tag("\""),
1108 (many0((opt(fws), qcontent)), opt(fws)),
1109 tag("\""),
1110 ),
1111 opt(cfws),
1112 ),
1113 )
1114 .parse(input)?;
1115
1116 let mut result = BString::default();
1117 for (a, b) in bits {
1118 if let Some(a) = a {
1119 result.push_str(&a);
1120 }
1121 result.push_str(b);
1122 }
1123 if let Some(t) = trailer {
1124 result.push_str(&t);
1125 }
1126 Ok((loc, result))
1127}
1128
1129fn qcontent(input: Span) -> IResult<Span, Span> {
1131 context(
1132 "qcontent",
1133 alt((
1134 take_while_m_n(1, 1, is_qtext_ascii),
1135 utf8_non_ascii,
1136 quoted_pair,
1137 )),
1138 )
1139 .parse(input)
1140}
1141
1142fn content_id(input: Span) -> IResult<Span, MessageID> {
1143 let (loc, id) = context("content_id", msg_id).parse(input)?;
1144 Ok((loc, id))
1145}
1146
1147fn msg_id(input: Span) -> IResult<Span, MessageID> {
1148 let (loc, id) = context("msg_id", alt((strict_msg_id, relaxed_msg_id))).parse(input)?;
1149 Ok((loc, id))
1150}
1151
1152fn relaxed_msg_id(input: Span) -> IResult<Span, MessageID> {
1153 let (loc, id) = context(
1154 "msg_id",
1155 delimited(
1156 preceded(opt(cfws), tag("<")),
1157 many0(take_while_m_n(1, 1, not_angle)),
1158 preceded(tag(">"), opt(cfws)),
1159 ),
1160 )
1161 .parse(input)?;
1162
1163 let mut result = BString::default();
1164 for item in id.into_iter() {
1165 result.push_str(*item);
1166 }
1167
1168 Ok((loc, MessageID(result)))
1169}
1170
1171fn msg_id_list(input: Span) -> IResult<Span, Vec<MessageID>> {
1173 context("msg_id_list", many1(msg_id)).parse(input)
1174}
1175
1176fn id_left(input: Span) -> IResult<Span, BString> {
1179 context("id_left", alt((dot_atom_text, local_part))).parse(input)
1180}
1181
1182fn id_right(input: Span) -> IResult<Span, BString> {
1185 context("id_right", alt((dot_atom_text, no_fold_literal, domain))).parse(input)
1186}
1187
1188fn no_fold_literal(input: Span) -> IResult<Span, BString> {
1190 context(
1191 "no_fold_literal",
1192 map(
1193 recognize((
1194 tag("["),
1195 recognize(many0(alt((take_while1(is_dtext_ascii), utf8_non_ascii)))),
1196 tag("]"),
1197 )),
1198 |s: Span| (*s).into(),
1199 ),
1200 )
1201 .parse(input)
1202}
1203
1204fn strict_msg_id(input: Span) -> IResult<Span, MessageID> {
1206 let (loc, (left, _, right)) = context(
1207 "msg_id",
1208 delimited(
1209 preceded(opt(cfws), tag("<")),
1210 (id_left, tag("@"), id_right),
1211 preceded(tag(">"), opt(cfws)),
1212 ),
1213 )
1214 .parse(input)?;
1215
1216 let mut result: BString = left.into();
1217 result.push_char('@');
1218 result.push_str(right);
1219
1220 Ok((loc, MessageID(result)))
1221}
1222
1223fn unstructured(input: Span) -> IResult<Span, BString> {
1225 #[derive(Debug)]
1226 enum Word {
1227 Encoded(BString),
1228 UText(BString),
1229 Fws,
1230 }
1231
1232 let (loc, words) = context(
1233 "unstructured",
1234 many0(alt((
1235 preceded(
1236 map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws),
1237 terminated(
1238 alt((
1239 map(encoded_word, Word::Encoded),
1240 map(obs_utext, |s| Word::UText((*s).into())),
1241 )),
1242 map(take_while(|c| c == b'\r' || c == b'\n'), |_| Word::Fws),
1243 ),
1244 ),
1245 map(fws, |_| Word::Fws),
1246 ))),
1247 )
1248 .parse(input)?;
1249
1250 #[derive(Debug)]
1251 enum ProcessedWord {
1252 Encoded(BString),
1253 Text(BString),
1254 Fws,
1255 }
1256 let mut processed = vec![];
1257 for w in words {
1258 match w {
1259 Word::Encoded(p) => {
1260 if processed.len() >= 2
1261 && matches!(processed.last(), Some(ProcessedWord::Fws))
1262 && matches!(processed[processed.len() - 2], ProcessedWord::Encoded(_))
1263 {
1264 processed.pop();
1266 }
1267 processed.push(ProcessedWord::Encoded(p));
1268 }
1269 Word::Fws => {
1270 if !matches!(processed.last(), Some(ProcessedWord::Fws)) {
1272 processed.push(ProcessedWord::Fws);
1273 }
1274 }
1275 Word::UText(c) => match processed.last_mut() {
1276 Some(ProcessedWord::Text(prior)) => prior.push_str(c),
1277 _ => processed.push(ProcessedWord::Text(c)),
1278 },
1279 }
1280 }
1281
1282 let mut result = BString::default();
1283 for word in processed {
1284 match word {
1285 ProcessedWord::Encoded(s) | ProcessedWord::Text(s) => {
1286 result.push_str(&s);
1287 }
1288 ProcessedWord::Fws => {
1289 result.push(b' ');
1290 }
1291 }
1292 }
1293
1294 Ok((loc, result))
1295}
1296
1297fn arc_authentication_results(input: Span) -> IResult<Span, ARCAuthenticationResults> {
1298 context(
1299 "arc_authentication_results",
1300 map(
1301 (
1302 preceded(opt(cfws), tag("i")),
1303 preceded(opt(cfws), tag("=")),
1304 preceded(opt(cfws), nom::character::complete::u8),
1305 preceded(opt(cfws), tag(";")),
1306 preceded(opt(cfws), value),
1307 opt(preceded(cfws, nom::character::complete::u32)),
1308 alt((no_result, many1(resinfo))),
1309 opt(cfws),
1310 ),
1311 |(_i, _eq, instance, _semic, serv_id, version, results, _)| ARCAuthenticationResults {
1312 instance,
1313 serv_id: serv_id.into(),
1314 version,
1315 results,
1316 },
1317 ),
1318 )
1319 .parse(input)
1320}
1321
1322fn authentication_results(input: Span) -> IResult<Span, AuthenticationResults> {
1323 context(
1324 "authentication_results",
1325 map(
1326 (
1327 preceded(opt(cfws), value),
1328 opt(preceded(cfws, nom::character::complete::u32)),
1329 alt((no_result, many1(resinfo))),
1330 opt(cfws),
1331 ),
1332 |(serv_id, version, results, _)| AuthenticationResults {
1333 serv_id: serv_id.into(),
1334 version,
1335 results,
1336 },
1337 ),
1338 )
1339 .parse(input)
1340}
1341
1342fn no_result(input: Span) -> IResult<Span, Vec<AuthenticationResult>> {
1343 context(
1344 "no_result",
1345 map((opt(cfws), tag(";"), opt(cfws), tag("none")), |_| vec![]),
1346 )
1347 .parse(input)
1348}
1349
1350fn resinfo(input: Span) -> IResult<Span, AuthenticationResult> {
1351 context(
1352 "resinfo",
1353 map(
1354 (
1355 opt(cfws),
1356 tag(";"),
1357 methodspec,
1358 opt(preceded(cfws, reasonspec)),
1359 opt(many1(propspec)),
1360 ),
1361 |(_, _, (method, method_version, result), reason, props)| AuthenticationResult {
1362 method,
1363 method_version,
1364 result,
1365 reason: reason.map(Into::into),
1366 props: match props {
1367 None => BTreeMap::default(),
1368 Some(props) => props.into_iter().collect(),
1369 },
1370 },
1371 ),
1372 )
1373 .parse(input)
1374}
1375
1376fn methodspec(input: Span) -> IResult<Span, (String, Option<u32>, String)> {
1377 context(
1378 "methodspec",
1379 map(
1380 (
1381 opt(cfws),
1382 (keyword, opt(methodversion)),
1383 opt(cfws),
1384 tag("="),
1385 opt(cfws),
1386 keyword,
1387 ),
1388 |(_, (method, methodversion), _, _, _, result)| (method, methodversion, result),
1389 ),
1390 )
1391 .parse(input)
1392}
1393
1394fn keyword(input: Span) -> IResult<Span, String> {
1399 context(
1400 "keyword",
1401 map(
1402 take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'-'),
1403 |s: Span| String::from_utf8((*s).into()).expect("keyword is ASCII-only"),
1405 ),
1406 )
1407 .parse(input)
1408}
1409
1410fn methodversion(input: Span) -> IResult<Span, u32> {
1411 context(
1412 "methodversion",
1413 preceded(
1414 (opt(cfws), tag("/"), opt(cfws)),
1415 nom::character::complete::u32,
1416 ),
1417 )
1418 .parse(input)
1419}
1420
1421fn reasonspec(input: Span) -> IResult<Span, BString> {
1422 context(
1423 "reason",
1424 map(
1425 (tag("reason"), opt(cfws), tag("="), opt(cfws), value),
1426 |(_, _, _, _, value)| value,
1427 ),
1428 )
1429 .parse(input)
1430}
1431
1432fn propspec(input: Span) -> IResult<Span, (String, BString)> {
1433 context(
1434 "propspec",
1435 map(
1436 (
1437 opt(cfws),
1442 keyword,
1443 opt(cfws),
1444 tag("."),
1445 opt(cfws),
1446 keyword,
1447 opt(cfws),
1448 tag("="),
1449 opt(cfws),
1450 alt((
1455 map(preceded(tag("@"), domain), |d| {
1456 let mut at_dom = BString::from("@");
1457 at_dom.push_str(d);
1458 at_dom
1459 }),
1460 map(separated_pair(local_part, tag("@"), domain), |(u, d)| {
1461 let mut result: BString = u.into();
1462 result.push(b'@');
1463 result.push_str(d);
1464 result
1465 }),
1466 quoted_string,
1467 domain,
1468 map(mime_token, |s: Span| (*s).into()),
1469 )),
1470 opt(cfws),
1471 ),
1472 |(_, ptype, _, _, _, property, _, _, _, value, _)| {
1473 (format!("{ptype}.{property}"), value)
1474 },
1475 ),
1476 )
1477 .parse(input)
1478}
1479
1480fn obs_utext(input: Span) -> IResult<Span, Span> {
1482 context(
1483 "obs_utext",
1484 alt((
1485 take_while_m_n(1, 1, |c| {
1486 c == 0x00 || is_obs_no_ws_ctl(c) || is_vchar_ascii(c)
1487 }),
1488 utf8_non_ascii,
1489 )),
1490 )
1491 .parse(input)
1492}
1493
1494fn is_mime_token(c: u8) -> bool {
1495 is_char(c) && c != b' ' && !is_ctl(c) && !is_tspecial(c)
1496}
1497
1498fn mime_token(input: Span) -> IResult<Span, Span> {
1501 context(
1502 "mime_token",
1503 recognize(many1(alt((take_while1(is_mime_token), utf8_non_ascii)))),
1504 )
1505 .parse(input)
1506}
1507
1508fn content_type(input: Span) -> IResult<Span, MimeParameters> {
1513 let (loc, (mime_type, _, _, _, mime_subtype, _, parameters)) = context(
1514 "content_type",
1515 preceded(
1516 opt(cfws),
1517 (
1518 mime_token,
1519 opt(cfws),
1520 tag("/"),
1521 opt(cfws),
1522 mime_token,
1523 opt(cfws),
1524 many0(preceded(
1525 preceded(opt(tag(";")), opt(cfws)),
1533 terminated(parameter, opt(cfws)),
1534 )),
1535 ),
1536 ),
1537 )
1538 .parse(input)?;
1539
1540 let mut value: BString = (*mime_type).into();
1541 value.push_char('/');
1542 value.push_str(mime_subtype);
1543
1544 Ok((loc, MimeParameters { value, parameters }))
1545}
1546
1547fn content_transfer_encoding(input: Span) -> IResult<Span, MimeParameters> {
1548 let (loc, (value, _, parameters)) = context(
1549 "content_transfer_encoding",
1550 preceded(
1551 opt(cfws),
1552 (
1553 mime_token,
1554 opt(cfws),
1555 many0(preceded(
1556 preceded(opt(tag(";")), opt(cfws)),
1564 terminated(parameter, opt(cfws)),
1565 )),
1566 ),
1567 ),
1568 )
1569 .parse(input)?;
1570
1571 Ok((
1572 loc,
1573 MimeParameters {
1574 value: value.as_bytes().into(),
1575 parameters,
1576 },
1577 ))
1578}
1579
1580fn parameter(input: Span) -> IResult<Span, MimeParameter> {
1582 context(
1583 "parameter",
1584 alt((
1585 param_with_unquoted_rfc2047,
1590 param_with_quoted_rfc2047,
1591 regular_parameter,
1592 extended_param_with_charset,
1593 extended_param_no_charset,
1594 )),
1595 )
1596 .parse(input)
1597}
1598
1599fn param_with_unquoted_rfc2047(input: Span) -> IResult<Span, MimeParameter> {
1600 context(
1601 "param_with_unquoted_rfc2047",
1602 map(
1603 (attribute, opt(cfws), tag("="), opt(cfws), encoded_word),
1604 |(name, _, _, _, value)| MimeParameter {
1605 name: name.as_bytes().into(),
1606 value: value.as_bytes().into(),
1607 section: None,
1608 encoding: MimeParameterEncoding::UnquotedRfc2047,
1609 mime_charset: None,
1610 mime_language: None,
1611 },
1612 ),
1613 )
1614 .parse(input)
1615}
1616
1617fn param_with_quoted_rfc2047(input: Span) -> IResult<Span, MimeParameter> {
1618 context(
1619 "param_with_quoted_rfc2047",
1620 map(
1621 (
1622 attribute,
1623 opt(cfws),
1624 tag("="),
1625 opt(cfws),
1626 delimited(tag("\""), encoded_word, tag("\"")),
1627 ),
1628 |(name, _, _, _, value)| MimeParameter {
1629 name: name.as_bytes().into(),
1630 value: value.as_bytes().into(),
1631 section: None,
1632 encoding: MimeParameterEncoding::QuotedRfc2047,
1633 mime_charset: None,
1634 mime_language: None,
1635 },
1636 ),
1637 )
1638 .parse(input)
1639}
1640
1641fn extended_param_with_charset(input: Span) -> IResult<Span, MimeParameter> {
1642 context(
1643 "extended_param_with_charset",
1644 map(
1645 (
1646 attribute,
1647 opt(section),
1648 tag("*"),
1649 opt(cfws),
1650 tag("="),
1651 opt(cfws),
1652 opt(mime_charset),
1653 tag("'"),
1654 opt(mime_language),
1655 tag("'"),
1656 map(
1657 recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))),
1658 |s: Span| (*s).into(),
1659 ),
1660 ),
1661 |(name, section, _, _, _, _, mime_charset, _, mime_language, _, value)| MimeParameter {
1662 name: name.as_bytes().into(),
1663 section,
1664 mime_charset: mime_charset.map(|s| s.as_bytes().into()),
1665 mime_language: mime_language.map(|s| s.as_bytes().into()),
1666 encoding: MimeParameterEncoding::Rfc2231,
1667 value,
1668 },
1669 ),
1670 )
1671 .parse(input)
1672}
1673
1674fn extended_param_no_charset(input: Span) -> IResult<Span, MimeParameter> {
1675 context(
1676 "extended_param_no_charset",
1677 map(
1678 (
1679 attribute,
1680 opt(section),
1681 opt(tag("*")),
1682 opt(cfws),
1683 tag("="),
1684 opt(cfws),
1685 alt((
1686 quoted_string,
1687 map(
1688 recognize(many0(alt((ext_octet, take_while1(is_attribute_char))))),
1689 |s: Span| (*s).into(),
1690 ),
1691 )),
1692 ),
1693 |(name, section, star, _, _, _, value)| MimeParameter {
1694 name: name.as_bytes().into(),
1695 section,
1696 mime_charset: None,
1697 mime_language: None,
1698 encoding: if star.is_some() {
1699 MimeParameterEncoding::Rfc2231
1700 } else {
1701 MimeParameterEncoding::None
1702 },
1703 value,
1704 },
1705 ),
1706 )
1707 .parse(input)
1708}
1709
1710fn mime_charset(input: Span) -> IResult<Span, Span> {
1711 context(
1712 "mime_charset",
1713 take_while1(|c| is_mime_token(c) && c != b'\''),
1714 )
1715 .parse(input)
1716}
1717
1718fn mime_language(input: Span) -> IResult<Span, Span> {
1719 context(
1720 "mime_language",
1721 take_while1(|c| is_mime_token(c) && c != b'\''),
1722 )
1723 .parse(input)
1724}
1725
1726fn ext_octet(input: Span) -> IResult<Span, Span> {
1727 context(
1728 "ext_octet",
1729 recognize((
1730 tag("%"),
1731 take_while_m_n(2, 2, |b: u8| b.is_ascii_hexdigit()),
1732 )),
1733 )
1734 .parse(input)
1735}
1736
1737fn section(input: Span) -> IResult<Span, u32> {
1739 context("section", preceded(tag("*"), nom::character::complete::u32)).parse(input)
1740}
1741
1742fn regular_parameter(input: Span) -> IResult<Span, MimeParameter> {
1744 context(
1745 "regular_parameter",
1746 map(
1747 (attribute, opt(cfws), tag("="), opt(cfws), value),
1748 |(name, _, _, _, value)| MimeParameter {
1749 name: name.as_bytes().into(),
1750 value: value.as_bytes().into(),
1751 section: None,
1752 encoding: MimeParameterEncoding::None,
1753 mime_charset: None,
1754 mime_language: None,
1755 },
1756 ),
1757 )
1758 .parse(input)
1759}
1760
1761fn attribute(input: Span) -> IResult<Span, Span> {
1764 context("attribute", take_while1(is_attribute_char)).parse(input)
1765}
1766
1767fn value(input: Span) -> IResult<Span, BString> {
1768 context(
1769 "value",
1770 alt((map(mime_token, |s: Span| (*s).into()), quoted_string)),
1771 )
1772 .parse(input)
1773}
1774
1775pub struct Parser;
1776
1777impl Parser {
1778 pub fn parse_mailbox_list_header(text: &[u8]) -> Result<MailboxList> {
1779 parse_with(text, mailbox_list)
1780 }
1781
1782 pub fn parse_mailbox_header(text: &[u8]) -> Result<Mailbox> {
1783 parse_with(text, mailbox)
1784 }
1785
1786 pub fn parse_address_list_header(text: &[u8]) -> Result<AddressList> {
1787 parse_with(text, address_list)
1788 }
1789
1790 pub fn parse_msg_id_header(text: &[u8]) -> Result<MessageID> {
1791 parse_with(text, msg_id)
1792 }
1793
1794 pub fn parse_msg_id_header_list(text: &[u8]) -> Result<Vec<MessageID>> {
1795 parse_with(text, msg_id_list)
1796 }
1797
1798 pub fn parse_content_id_header(text: &[u8]) -> Result<MessageID> {
1799 parse_with(text, content_id)
1800 }
1801
1802 pub fn parse_content_type_header(text: &[u8]) -> Result<MimeParameters> {
1803 parse_with(text, content_type)
1804 }
1805
1806 pub fn parse_content_transfer_encoding_header(text: &[u8]) -> Result<MimeParameters> {
1807 parse_with(text, content_transfer_encoding)
1808 }
1809
1810 pub fn parse_unstructured_header(text: &[u8]) -> Result<BString> {
1811 parse_with(text, unstructured)
1812 }
1813
1814 pub fn parse_authentication_results_header(text: &[u8]) -> Result<AuthenticationResults> {
1815 parse_with(text, authentication_results)
1816 }
1817
1818 pub fn parse_arc_authentication_results_header(
1819 text: &[u8],
1820 ) -> Result<ARCAuthenticationResults> {
1821 parse_with(text, arc_authentication_results)
1822 }
1823}
1824
1825#[serde_as]
1826#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1827#[serde(deny_unknown_fields)]
1828pub struct ARCAuthenticationResults {
1829 pub instance: u8,
1830 #[serde_as(as = "BStringUtf8")]
1831 pub serv_id: BString,
1832 pub version: Option<u32>,
1833 pub results: Vec<AuthenticationResult>,
1834}
1835
1836impl EncodeHeaderValue for ARCAuthenticationResults {
1837 fn encode_value(&self) -> SharedString<'static> {
1838 let mut result = format!("i={}; ", self.instance).into_bytes();
1839
1840 emit_value_token(&self.serv_id, &mut result);
1841 if let Some(v) = self.version {
1842 result.push_str(&format!(" {v}"));
1843 }
1844
1845 if self.results.is_empty() {
1846 result.push_str("; none");
1847 } else {
1848 for res in &self.results {
1849 result.push_str(";\r\n\t");
1850 emit_value_token(res.method.as_bytes(), &mut result);
1851 if let Some(v) = res.method_version {
1852 result.push_str(&format!("/{v}"));
1853 }
1854 result.push(b'=');
1855 emit_value_token(res.result.as_bytes(), &mut result);
1856 if let Some(reason) = &res.reason {
1857 result.push_str(" reason=");
1858 emit_value_token(reason.as_bytes(), &mut result);
1859 }
1860 for (k, v) in &res.props {
1861 result.push_str(&format!("\r\n\t{k}="));
1862 emit_value_token(v.as_bytes(), &mut result);
1863 }
1864 }
1865 }
1866
1867 result.into()
1868 }
1869}
1870
1871#[serde_as]
1872#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1873#[serde(deny_unknown_fields)]
1874pub struct AuthenticationResults {
1875 #[serde_as(as = "BStringUtf8")]
1876 pub serv_id: BString,
1877 #[serde(default)]
1878 pub version: Option<u32>,
1879 #[serde(default)]
1880 pub results: Vec<AuthenticationResult>,
1881}
1882
1883fn emit_value_token(value: &[u8], target: &mut Vec<u8>) {
1885 let use_quoted_string = !value.iter().all(|&c| is_mime_token(c) || c == b'@');
1887 if use_quoted_string {
1888 target.push(b'"');
1889 for (start, end, c) in value.char_indices() {
1890 if c == '"' || c == '\\' {
1891 target.push(b'\\');
1892 }
1893 target.push_str(&value[start..end]);
1894 }
1895 target.push(b'"');
1896 } else {
1897 target.push_str(value);
1898 }
1899}
1900
1901impl EncodeHeaderValue for AuthenticationResults {
1902 fn encode_value(&self) -> SharedString<'static> {
1903 let mut result = Vec::new();
1904 emit_value_token(&self.serv_id, &mut result);
1905 if let Some(v) = self.version {
1906 result.push_str(&format!(" {v}"));
1907 }
1908 if self.results.is_empty() {
1909 result.push_str("; none");
1910 } else {
1911 for res in &self.results {
1912 result.push_str(";\r\n\t");
1913 emit_value_token(res.method.as_bytes(), &mut result);
1914 if let Some(v) = res.method_version {
1915 result.push_str(&format!("/{v}"));
1916 }
1917 result.push(b'=');
1918 emit_value_token(res.result.as_bytes(), &mut result);
1919 if let Some(reason) = &res.reason {
1920 result.push_str(" reason=");
1921 emit_value_token(reason.as_bytes(), &mut result);
1922 }
1923 for (k, v) in &res.props {
1924 result.push_str(&format!("\r\n\t{k}="));
1925 emit_value_token(v.as_bytes(), &mut result);
1926 }
1927 }
1928 }
1929
1930 result.into()
1931 }
1932}
1933
1934#[serde_as]
1935#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1936#[serde(deny_unknown_fields)]
1937pub struct AuthenticationResult {
1938 pub method: String,
1939 #[serde(default)]
1940 pub method_version: Option<u32>,
1941 pub result: String,
1942 #[serde_as(as = "Option<BStringUtf8>")]
1943 #[serde(default)]
1944 pub reason: Option<BString>,
1945 #[serde_as(as = "BTreeMap<_, BStringUtf8>")]
1946 #[serde(default)]
1947 pub props: BTreeMap<String, BString>,
1948}
1949
1950#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1951#[serde(deny_unknown_fields)]
1952pub struct AddrSpec {
1953 pub local_part: String,
1954 pub domain: String,
1955}
1956
1957impl AddrSpec {
1958 pub fn new(local_part: &str, domain: &str) -> Self {
1959 Self {
1960 local_part: local_part.into(),
1961 domain: domain.into(),
1962 }
1963 }
1964
1965 pub fn parse(email: &str) -> Result<Self> {
1966 parse_with(email.as_bytes(), addr_spec)
1967 }
1968}
1969
1970impl EncodeHeaderValue for AddrSpec {
1971 fn encode_value(&self) -> SharedString<'static> {
1972 let mut result: Vec<u8> = vec![];
1973
1974 let needs_quoting = !self
1975 .local_part
1976 .as_bytes()
1977 .iter()
1978 .all(|&c| is_atext(c) || c == b'.');
1979 if needs_quoting {
1980 result.push(b'"');
1981 for &c in self.local_part.as_bytes().iter() {
1986 if c == b'"' || c == b'\\' {
1987 result.push(b'\\');
1988 }
1989 result.push(c);
1990 }
1991 result.push(b'"');
1992 } else {
1993 result.push_str(&self.local_part);
1994 }
1995 result.push(b'@');
1996 result.push_str(&self.domain);
1997
1998 result.into()
1999 }
2000}
2001
2002#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2003#[serde(untagged)]
2004pub enum Address {
2005 Mailbox(Mailbox),
2006 Group { name: String, entries: MailboxList },
2007}
2008
2009#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2010#[serde(deny_unknown_fields, transparent)]
2011pub struct AddressList(pub Vec<Address>);
2012
2013impl std::ops::Deref for AddressList {
2014 type Target = Vec<Address>;
2015 fn deref(&self) -> &Vec<Address> {
2016 &self.0
2017 }
2018}
2019
2020impl AddressList {
2021 pub fn extract_first_mailbox(&self) -> Option<&Mailbox> {
2022 let address = self.0.first()?;
2023 match address {
2024 Address::Mailbox(mailbox) => Some(mailbox),
2025 Address::Group { entries, .. } => entries.extract_first_mailbox(),
2026 }
2027 }
2028}
2029
2030#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2031#[serde(deny_unknown_fields, transparent)]
2032pub struct MailboxList(pub Vec<Mailbox>);
2033
2034impl std::ops::Deref for MailboxList {
2035 type Target = Vec<Mailbox>;
2036 fn deref(&self) -> &Vec<Mailbox> {
2037 &self.0
2038 }
2039}
2040
2041impl MailboxList {
2042 pub fn extract_first_mailbox(&self) -> Option<&Mailbox> {
2043 self.0.first()
2044 }
2045}
2046
2047#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2048#[serde(deny_unknown_fields)]
2049pub struct Mailbox {
2050 pub name: Option<String>,
2051 pub address: AddrSpec,
2052}
2053
2054#[serde_as]
2055#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2056#[serde(transparent)]
2057pub struct MessageID(#[serde_as(as = "BStringUtf8")] pub BString);
2058
2059impl EncodeHeaderValue for MessageID {
2060 fn encode_value(&self) -> SharedString<'static> {
2061 let mut result = Vec::<u8>::with_capacity(self.0.len() + 2);
2062 result.push(b'<');
2063 result.push_str(&self.0);
2064 result.push(b'>');
2065 result.into()
2066 }
2067}
2068
2069impl EncodeHeaderValue for Vec<MessageID> {
2070 fn encode_value(&self) -> SharedString<'static> {
2071 let mut result = BString::default();
2072 for id in self {
2073 if !result.is_empty() {
2074 result.push_str("\r\n\t");
2075 }
2076 result.push(b'<');
2077 result.push_str(&id.0);
2078 result.push(b'>');
2079 }
2080 result.into()
2081 }
2082}
2083
2084#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2093pub(crate) enum MimeParameterEncoding {
2094 None,
2095 Rfc2231,
2096 UnquotedRfc2047,
2097 QuotedRfc2047,
2098}
2099
2100#[derive(Debug, Clone, PartialEq, Eq)]
2101struct MimeParameter {
2102 pub name: BString,
2103 pub section: Option<u32>,
2104 pub mime_charset: Option<BString>,
2105 pub mime_language: Option<BString>,
2106 pub encoding: MimeParameterEncoding,
2107 pub value: BString,
2108}
2109
2110#[derive(Debug, Clone, PartialEq, Eq)]
2111pub struct MimeParameters {
2112 pub value: BString,
2113 parameters: Vec<MimeParameter>,
2114}
2115
2116impl MimeParameters {
2117 pub fn new(value: impl AsRef<[u8]>) -> Self {
2118 Self {
2119 value: value.as_ref().into(),
2120 parameters: vec![],
2121 }
2122 }
2123
2124 pub fn parameter_map(&self) -> BTreeMap<BString, BString> {
2129 let mut map = BTreeMap::new();
2130
2131 fn contains_key_ignore_case(map: &BTreeMap<BString, BString>, key: &[u8]) -> bool {
2132 for k in map.keys() {
2133 if k.eq_ignore_ascii_case(key) {
2134 return true;
2135 }
2136 }
2137 false
2138 }
2139
2140 for entry in &self.parameters {
2141 let name = entry.name.as_bytes();
2142 if !contains_key_ignore_case(&map, name) {
2143 if let Some(value) = self.get(name) {
2144 map.insert(name.into(), value);
2145 }
2146 }
2147 }
2148
2149 map
2150 }
2151
2152 pub fn get(&self, name: impl AsRef<[u8]>) -> Option<BString> {
2158 let name = name.as_ref();
2159 let mut elements: Vec<_> = self
2160 .parameters
2161 .iter()
2162 .filter(|p| p.name.eq_ignore_ascii_case(name.as_bytes()))
2163 .collect();
2164 if elements.is_empty() {
2165 return None;
2166 }
2167 elements.sort_by(|a, b| a.section.cmp(&b.section));
2168
2169 let mut mime_charset = None;
2170 let mut result: Vec<u8> = vec![];
2171
2172 for ele in elements {
2173 if let Some(cset) = ele.mime_charset.as_ref().and_then(|b| b.to_str().ok()) {
2174 mime_charset = Encoding::by_name(&*cset);
2175 }
2176
2177 match ele.encoding {
2178 MimeParameterEncoding::Rfc2231 => {
2179 if let Some(charset) = mime_charset.as_ref() {
2180 let mut chars = ele.value.chars();
2181 let mut bytes: Vec<u8> = vec![];
2182
2183 fn char_to_bytes(c: char, bytes: &mut Vec<u8>) {
2184 let mut buf = [0u8; 8];
2185 let s = c.encode_utf8(&mut buf);
2186 for b in s.bytes() {
2187 bytes.push(b);
2188 }
2189 }
2190
2191 'next_char: while let Some(c) = chars.next() {
2192 match c {
2193 '%' => {
2194 let mut value = 0u8;
2195 for _ in 0..2 {
2196 match chars.next() {
2197 Some(n) => match n {
2198 '0'..='9' => {
2199 value <<= 4;
2200 value |= n as u32 as u8 - b'0';
2201 }
2202 'a'..='f' => {
2203 value <<= 4;
2204 value |= (n as u32 as u8 - b'a') + 10;
2205 }
2206 'A'..='F' => {
2207 value <<= 4;
2208 value |= (n as u32 as u8 - b'A') + 10;
2209 }
2210 _ => {
2211 char_to_bytes('%', &mut bytes);
2212 char_to_bytes(n, &mut bytes);
2213 break 'next_char;
2214 }
2215 },
2216 None => {
2217 char_to_bytes('%', &mut bytes);
2218 break 'next_char;
2219 }
2220 }
2221 }
2222
2223 bytes.push(value);
2224 }
2225 c => {
2226 char_to_bytes(c, &mut bytes);
2227 }
2228 }
2229 }
2230
2231 if let Ok(decoded) = charset.decode_simple(&bytes) {
2232 result.push_str(&decoded);
2233 }
2234 } else {
2235 result.push_str(&ele.value);
2236 }
2237 }
2238 MimeParameterEncoding::UnquotedRfc2047
2239 | MimeParameterEncoding::QuotedRfc2047
2240 | MimeParameterEncoding::None => {
2241 result.push_str(&ele.value);
2242 }
2243 }
2244 }
2245
2246 Some(result.into())
2247 }
2248
2249 pub fn remove(&mut self, name: impl AsRef<[u8]>) {
2251 let name = name.as_ref();
2252 self.parameters
2253 .retain(|p| !p.name.eq_ignore_ascii_case(name));
2254 }
2255
2256 pub fn set(&mut self, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
2257 self.set_with_encoding(name, value, MimeParameterEncoding::None)
2258 }
2259
2260 pub(crate) fn set_with_encoding(
2261 &mut self,
2262 name: impl AsRef<[u8]>,
2263 value: impl AsRef<[u8]>,
2264 encoding: MimeParameterEncoding,
2265 ) {
2266 self.remove(name.as_ref());
2267
2268 self.parameters.push(MimeParameter {
2269 name: name.as_ref().into(),
2270 value: value.as_ref().into(),
2271 section: None,
2272 mime_charset: None,
2273 mime_language: None,
2274 encoding,
2275 });
2276 }
2277
2278 pub fn is_multipart(&self) -> bool {
2279 self.value.starts_with_str("message/") || self.value.starts_with_str("multipart/")
2280 }
2281
2282 pub fn is_text(&self) -> bool {
2283 self.value.starts_with_str("text/")
2284 }
2285}
2286
2287impl EncodeHeaderValue for MimeParameters {
2288 fn encode_value(&self) -> SharedString<'static> {
2289 let mut result = self.value.clone();
2290 let names: BTreeMap<&BStr, MimeParameterEncoding> = self
2291 .parameters
2292 .iter()
2293 .map(|p| (p.name.as_bstr(), p.encoding))
2294 .collect();
2295
2296 for (name, stated_encoding) in names {
2297 let value = self.get(name).expect("name to be present");
2298
2299 match stated_encoding {
2300 MimeParameterEncoding::UnquotedRfc2047 => {
2301 let encoded = qp_encode(&value);
2302 result.push_str(&format!(";\r\n\t{name}={encoded}"));
2303 }
2304 MimeParameterEncoding::QuotedRfc2047 => {
2305 let encoded = qp_encode(&value);
2306 result.push_str(&format!(";\r\n\t{name}=\"{encoded}\""));
2307 }
2308 MimeParameterEncoding::None | MimeParameterEncoding::Rfc2231 => {
2309 let needs_encoding = value.iter().any(|&c| !is_mime_token(c) || !c.is_ascii());
2310 let use_quoted_string = value
2313 .iter()
2314 .all(|&c| (is_qtext(c) || is_quoted_pair(c)) && c.is_ascii());
2315
2316 let mut params = vec![];
2317 let mut chars = value.char_indices().peekable();
2318 while chars.peek().is_some() {
2319 let count = params.len();
2320 let is_first = count == 0;
2321 let prefix = if use_quoted_string {
2322 "\""
2323 } else if is_first && needs_encoding {
2324 "UTF-8''"
2325 } else {
2326 ""
2327 };
2328 let limit = 74 - (name.len() + 4 + prefix.len());
2329
2330 let mut encoded: Vec<u8> = vec![];
2331
2332 while encoded.len() < limit {
2333 let Some((start, end, c)) = chars.next() else {
2334 break;
2335 };
2336 let s = &value[start..end];
2337
2338 if use_quoted_string {
2339 if c == '"' || c == '\\' {
2340 encoded.push(b'\\');
2341 }
2342 encoded.push_str(s);
2343 } else if (c as u32) <= 0xff
2344 && is_mime_token(c as u32 as u8)
2345 && (!needs_encoding || c != '%')
2346 {
2347 encoded.push_str(s);
2348 } else {
2349 for b in s.bytes() {
2350 encoded.push(b'%');
2351 encoded.push(HEX_CHARS[(b as usize) >> 4]);
2352 encoded.push(HEX_CHARS[(b as usize) & 0x0f]);
2353 }
2354 }
2355 }
2356
2357 if use_quoted_string {
2358 encoded.push(b'"');
2359 }
2360
2361 params.push(MimeParameter {
2362 name: name.into(),
2363 section: Some(count as u32),
2364 mime_charset: if is_first { Some("UTF-8".into()) } else { None },
2365 mime_language: None,
2366 encoding: if needs_encoding {
2367 MimeParameterEncoding::Rfc2231
2368 } else {
2369 MimeParameterEncoding::None
2370 },
2371 value: encoded.into(),
2372 })
2373 }
2374 if params.len() == 1 {
2375 params.last_mut().map(|p| p.section = None);
2376 }
2377 for p in params {
2378 result.push_str(";\r\n\t");
2379 let charset_tick = if !use_quoted_string
2380 && (p.mime_charset.is_some() || p.mime_language.is_some())
2381 {
2382 "'"
2383 } else {
2384 ""
2385 };
2386 let lang_tick = if !use_quoted_string
2387 && (p.mime_language.is_some() || p.mime_charset.is_some())
2388 {
2389 "'"
2390 } else {
2391 ""
2392 };
2393
2394 let section = p
2395 .section
2396 .map(|s| format!("*{s}"))
2397 .unwrap_or_else(String::new);
2398
2399 let uses_encoding =
2400 if !use_quoted_string && p.encoding == MimeParameterEncoding::Rfc2231 {
2401 "*"
2402 } else {
2403 ""
2404 };
2405 let charset = if use_quoted_string {
2406 BStr::new("\"")
2407 } else {
2408 p.mime_charset
2409 .as_ref()
2410 .map(|b| b.as_bstr())
2411 .unwrap_or(BStr::new(""))
2412 };
2413 let lang = p
2414 .mime_language
2415 .as_ref()
2416 .map(|b| b.as_bstr())
2417 .unwrap_or(BStr::new(""));
2418
2419 let line = format!(
2420 "{name}{section}{uses_encoding}={charset}{charset_tick}{lang}{lang_tick}{value}",
2421 name = &p.name,
2422 value = &p.value
2423 );
2424 result.push_str(&line);
2425 }
2426 }
2427 }
2428 }
2429 result.into()
2430 }
2431}
2432
2433static HEX_CHARS: &[u8] = b"0123456789ABCDEF";
2434
2435pub(crate) fn qp_encode(s: &[u8]) -> String {
2436 let prefix = b"=?UTF-8?q?";
2437 let suffix = b"?=";
2438 let limit = 72 - (prefix.len() + suffix.len());
2439
2440 let mut result = Vec::with_capacity(s.len());
2441
2442 result.extend_from_slice(prefix);
2443 let mut line_length = 0;
2444
2445 enum Bytes<'a> {
2446 Passthru(&'a [u8]),
2447 Encode(&'a [u8]),
2448 }
2449
2450 for (start, end, c) in s.char_indices() {
2453 let bytes = &s[start..end];
2454
2455 let b = if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
2456 && c != '?'
2457 && c != '='
2458 && c != ' '
2459 && c != '\t'
2460 {
2461 Bytes::Passthru(bytes)
2462 } else if c == ' ' {
2463 Bytes::Passthru(b"_")
2464 } else {
2465 Bytes::Encode(bytes)
2466 };
2467
2468 let need_len = match b {
2469 Bytes::Passthru(b) => b.len(),
2470 Bytes::Encode(b) => b.len() * 3,
2471 };
2472
2473 if need_len > limit - line_length {
2474 result.extend_from_slice(suffix);
2476 result.extend_from_slice(b"\r\n\t");
2477 result.extend_from_slice(prefix);
2478 line_length = 0;
2479 }
2480
2481 match b {
2482 Bytes::Passthru(c) => {
2483 result.extend_from_slice(c);
2484 }
2485 Bytes::Encode(bytes) => {
2486 for &c in bytes {
2487 result.push(b'=');
2488 result.push(HEX_CHARS[(c as usize) >> 4]);
2489 result.push(HEX_CHARS[(c as usize) & 0x0f]);
2490 }
2491 }
2492 }
2493
2494 line_length += need_len;
2495 }
2496
2497 if line_length > 0 {
2498 result.extend_from_slice(suffix);
2499 }
2500
2501 unsafe { String::from_utf8_unchecked(result) }
2504}
2505
2506#[cfg(test)]
2507#[test]
2508fn test_qp_encode() {
2509 let encoded = qp_encode(
2510 b"hello, I am a line that is this long, or maybe a little \
2511 bit longer than this, and that should get wrapped by the encoder",
2512 );
2513 k9::snapshot!(
2514 encoded,
2515 r#"
2516=?UTF-8?q?hello,_I_am_a_line_that_is_this_long,_or_maybe_a_little_bit_?=\r
2517\t=?UTF-8?q?longer_than_this,_and_that_should_get_wrapped_by_the_encoder?=
2518"#
2519 );
2520}
2521
2522fn quote_string(s: impl AsRef<[u8]>) -> BString {
2526 let s = s.as_ref();
2527
2528 if s.iter().any(|&c| !is_atext(c)) {
2529 let mut result = Vec::<u8>::with_capacity(s.len() + 4);
2530 result.push(b'"');
2531 for (start, end, c) in s.char_indices() {
2532 let c = c as u32;
2533 if c <= 0xff {
2534 let c = c as u8;
2535 if !c.is_ascii_whitespace() && !is_qtext(c) && !is_atext(c) {
2536 result.push(b'\\');
2537 }
2538 }
2539 result.push_str(&s[start..end]);
2540 }
2541 result.push(b'"');
2542 result.into()
2543 } else {
2544 s.into()
2545 }
2546}
2547
2548#[cfg(test)]
2549#[test]
2550fn test_quote_string() {
2551 k9::snapshot!(
2552 quote_string("TEST [ne_pas_repondre]"),
2553 r#""TEST [ne_pas_repondre]""#
2554 );
2555 k9::snapshot!(quote_string("hello"), "hello");
2556 k9::snapshot!(quote_string("hello there"), r#""hello there""#);
2557 k9::snapshot!(quote_string("hello, there"), "\"hello, there\"");
2558 k9::snapshot!(quote_string("hello \"there\""), r#""hello \\"there\\"""#);
2559 k9::snapshot!(
2560 quote_string("hello c:\\backslash"),
2561 r#""hello c:\\\\backslash""#
2562 );
2563 k9::assert_equal!(quote_string("hello\n there"), "\"hello\n there\"");
2564}
2565
2566impl EncodeHeaderValue for Mailbox {
2567 fn encode_value(&self) -> SharedString<'static> {
2568 match &self.name {
2569 Some(name) => {
2570 let mut value: Vec<u8> = if name.is_ascii() {
2571 quote_string(name).into()
2572 } else {
2573 qp_encode(name.as_bytes()).into_bytes()
2574 };
2575
2576 value.push_str(" <");
2577 value.push_str(self.address.encode_value().as_bytes());
2578 value.push(b'>');
2579 value.into()
2580 }
2581 None => {
2582 let mut result: Vec<u8> = vec![];
2583 result.push(b'<');
2584 result.push_str(self.address.encode_value().as_bytes());
2585 result.push(b'>');
2586 result.into()
2587 }
2588 }
2589 }
2590}
2591
2592impl EncodeHeaderValue for MailboxList {
2593 fn encode_value(&self) -> SharedString<'static> {
2594 let mut result: Vec<u8> = vec![];
2595 for mailbox in &self.0 {
2596 if !result.is_empty() {
2597 result.push_str(",\r\n\t");
2598 }
2599 result.push_str(mailbox.encode_value().as_bytes());
2600 }
2601 result.into()
2602 }
2603}
2604
2605impl EncodeHeaderValue for Address {
2606 fn encode_value(&self) -> SharedString<'static> {
2607 match self {
2608 Self::Mailbox(mbox) => mbox.encode_value(),
2609 Self::Group { name, entries } => {
2610 let mut result: Vec<u8> = vec![];
2611 result.push_str(name);
2612 result.push(b':');
2613 result.push_str(entries.encode_value().as_bytes());
2614 result.push(b';');
2615 result.into()
2616 }
2617 }
2618 }
2619}
2620
2621impl EncodeHeaderValue for AddressList {
2622 fn encode_value(&self) -> SharedString<'static> {
2623 let mut result: Vec<u8> = vec![];
2624 for address in &self.0 {
2625 if !result.is_empty() {
2626 result.push_str(",\r\n\t");
2627 }
2628 result.push_str(address.encode_value().as_bytes());
2629 }
2630 result.into()
2631 }
2632}
2633
2634#[cfg(test)]
2635mod test {
2636 use super::*;
2637 use crate::{Header, MessageConformance, MimePart};
2638
2639 #[test]
2640 fn mailbox_encodes_at() {
2641 let mbox = Mailbox {
2642 name: Some("foo@bar.com".into()),
2643 address: AddrSpec {
2644 local_part: "foo".into(),
2645 domain: "bar.com".into(),
2646 },
2647 };
2648 assert_eq!(mbox.encode_value(), "\"foo@bar.com\" <foo@bar.com>");
2649 }
2650
2651 #[test]
2652 fn mailbox_list_singular() {
2653 let message = concat!(
2654 "From: Someone (hello) <someone@example.com>, other@example.com,\n",
2655 " \"John \\\"Smith\\\"\" (comment) \"More Quotes\" (more comment) <someone(another comment)@crazy.example.com(woot)>\n",
2656 "\n",
2657 "I am the body"
2658 );
2659 let msg = MimePart::parse(message).unwrap();
2660 let list = match msg.headers().from() {
2661 Err(err) => panic!("Doh.\n{err:#}"),
2662 Ok(list) => list,
2663 };
2664
2665 k9::snapshot!(
2666 list,
2667 r#"
2668Some(
2669 MailboxList(
2670 [
2671 Mailbox {
2672 name: Some(
2673 "Someone",
2674 ),
2675 address: AddrSpec {
2676 local_part: "someone",
2677 domain: "example.com",
2678 },
2679 },
2680 Mailbox {
2681 name: None,
2682 address: AddrSpec {
2683 local_part: "other",
2684 domain: "example.com",
2685 },
2686 },
2687 Mailbox {
2688 name: Some(
2689 "John "Smith" More Quotes",
2690 ),
2691 address: AddrSpec {
2692 local_part: "someone",
2693 domain: "crazy.example.com",
2694 },
2695 },
2696 ],
2697 ),
2698)
2699"#
2700 );
2701 }
2702
2703 #[test]
2704 fn docomo_non_compliant_localpart() {
2705 let message = "Sender: hello..there@docomo.ne.jp\n\n\n";
2706 let msg = MimePart::parse(message).unwrap();
2707 let err = msg.headers().sender().unwrap_err();
2708 k9::snapshot!(
2709 err,
2710 r#"
2711InvalidHeaderValueDuringGet {
2712 header_name: "Sender",
2713 error: HeaderParse(
2714 "Error at line 1, expected "@" but found ".":
2715hello..there@docomo.ne.jp
2716 ^___________________
2717
2718while parsing addr_spec
2719while parsing mailbox
2720",
2721 ),
2722}
2723"#
2724 );
2725 }
2726
2727 #[test]
2728 fn sender() {
2729 let message = "Sender: someone@[127.0.0.1]\n\n\n";
2730 let msg = MimePart::parse(message).unwrap();
2731 let list = match msg.headers().sender() {
2732 Err(err) => panic!("Doh.\n{err:#}"),
2733 Ok(list) => list,
2734 };
2735 k9::snapshot!(
2736 list,
2737 r#"
2738Some(
2739 Mailbox {
2740 name: None,
2741 address: AddrSpec {
2742 local_part: "someone",
2743 domain: "[127.0.0.1]",
2744 },
2745 },
2746)
2747"#
2748 );
2749 }
2750
2751 #[test]
2752 fn domain_literal() {
2753 let message = "From: someone@[127.0.0.1]\n\n\n";
2754 let msg = MimePart::parse(message).unwrap();
2755 let list = match msg.headers().from() {
2756 Err(err) => panic!("Doh.\n{err:#}"),
2757 Ok(list) => list,
2758 };
2759 k9::snapshot!(
2760 list,
2761 r#"
2762Some(
2763 MailboxList(
2764 [
2765 Mailbox {
2766 name: None,
2767 address: AddrSpec {
2768 local_part: "someone",
2769 domain: "[127.0.0.1]",
2770 },
2771 },
2772 ],
2773 ),
2774)
2775"#
2776 );
2777 }
2778
2779 #[test]
2780 fn rfc6532() {
2781 let message = concat!(
2782 "From: Keith Moore <moore@cs.utk.edu>\n",
2783 "To: Keld Jørn Simonsen <keld@dkuug.dk>\n",
2784 "CC: André Pirard <PIRARD@vm1.ulg.ac.be>\n",
2785 "Subject: Hello André\n",
2786 "\n\n"
2787 );
2788 let msg = MimePart::parse(message).unwrap();
2789 let list = match msg.headers().from() {
2790 Err(err) => panic!("Doh.\n{err:#}"),
2791 Ok(list) => list,
2792 };
2793 k9::snapshot!(
2794 list,
2795 r#"
2796Some(
2797 MailboxList(
2798 [
2799 Mailbox {
2800 name: Some(
2801 "Keith Moore",
2802 ),
2803 address: AddrSpec {
2804 local_part: "moore",
2805 domain: "cs.utk.edu",
2806 },
2807 },
2808 ],
2809 ),
2810)
2811"#
2812 );
2813
2814 let list = match msg.headers().to() {
2815 Err(err) => panic!("Doh.\n{err:#}"),
2816 Ok(list) => list,
2817 };
2818 k9::snapshot!(
2819 list,
2820 r#"
2821Some(
2822 AddressList(
2823 [
2824 Mailbox(
2825 Mailbox {
2826 name: Some(
2827 "Keld Jørn Simonsen",
2828 ),
2829 address: AddrSpec {
2830 local_part: "keld",
2831 domain: "dkuug.dk",
2832 },
2833 },
2834 ),
2835 ],
2836 ),
2837)
2838"#
2839 );
2840
2841 let list = match msg.headers().cc() {
2842 Err(err) => panic!("Doh.\n{err:#}"),
2843 Ok(list) => list,
2844 };
2845 k9::snapshot!(
2846 list,
2847 r#"
2848Some(
2849 AddressList(
2850 [
2851 Mailbox(
2852 Mailbox {
2853 name: Some(
2854 "André Pirard",
2855 ),
2856 address: AddrSpec {
2857 local_part: "PIRARD",
2858 domain: "vm1.ulg.ac.be",
2859 },
2860 },
2861 ),
2862 ],
2863 ),
2864)
2865"#
2866 );
2867 let list = match msg.headers().subject() {
2868 Err(err) => panic!("Doh.\n{err:#}"),
2869 Ok(list) => list,
2870 };
2871 k9::snapshot!(
2872 list,
2873 r#"
2874Some(
2875 "Hello André",
2876)
2877"#
2878 );
2879 }
2880
2881 #[test]
2882 fn unstructured_bare_non_ascii() {
2883 let message = "Subject: Héllo wörld äöü\n\n\n";
2886 let msg = MimePart::parse(message).unwrap();
2887 k9::snapshot!(
2888 msg.headers().subject().unwrap(),
2889 r#"
2890Some(
2891 "Héllo wörld äöü",
2892)
2893"#
2894 );
2895
2896 let message = "Subject: 件名テスト\n\n\n";
2898 let msg = MimePart::parse(message).unwrap();
2899 k9::snapshot!(
2900 msg.headers().subject().unwrap(),
2901 r#"
2902Some(
2903 "件名テスト",
2904)
2905"#
2906 );
2907 }
2908
2909 #[test]
2910 fn unstructured_raw_shift_jis() {
2911 let message = b"Subject: \x83\x65\x83\x58\x83\x67\n\n\n";
2918
2919 let msg = MimePart::parse(message.as_slice()).unwrap();
2922 let subject_header = msg.headers().get_first("Subject").unwrap();
2923 k9::assert_equal!(
2924 subject_header.get_raw_value(),
2925 b"\x83\x65\x83\x58\x83\x67".as_slice()
2926 );
2927
2928 k9::snapshot!(
2931 msg.headers().subject(),
2932 r#"
2933Err(
2934 InvalidHeaderValueDuringGet {
2935 header_name: "Subject",
2936 error: HeaderParse(
2937 "Error at line 1, in Eof:
2938\\x83e\\x83X\\x83g
2939^_____
2940
2941",
2942 ),
2943 },
2944)
2945"#
2946 );
2947 }
2948
2949 #[test]
2950 fn rfc2047_bogus() {
2951 let message = concat!(
2952 "From: =?US-OSCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2953 "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2954 "CC: =?ISO-8859-1?Q?Andr=E?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2955 "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?=\n",
2956 " =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2957 "\n\n"
2958 );
2959 let msg = MimePart::parse(message).unwrap();
2960
2961 k9::assert_equal!(
2964 msg.headers().from().unwrap().unwrap().0[0]
2965 .name
2966 .as_ref()
2967 .unwrap(),
2968 "=?US-OSCII?Q?Keith_Moore?="
2969 );
2970
2971 match &msg.headers().cc().unwrap().unwrap().0[0] {
2972 Address::Mailbox(mbox) => {
2973 k9::assert_equal!(mbox.name.as_ref().unwrap(), "Andr=E Pirard");
2977 }
2978 wat => panic!("should not have {wat:?}"),
2979 }
2980
2981 k9::assert_equal!(
2984 msg.headers().subject().unwrap().unwrap(),
2985 "Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?= u understand the example."
2986 );
2987 }
2988
2989 #[test]
2990 fn attachment_filename_mess_totally_bogus() {
2991 let message = concat!("Content-Disposition: attachment; filename=@\n", "\n\n");
2992 let msg = MimePart::parse(message).unwrap();
2993 eprintln!("{msg:#?}");
2994
2995 assert!(msg
2996 .conformance()
2997 .contains(MessageConformance::INVALID_MIME_HEADERS));
2998 msg.headers().content_disposition().unwrap_err();
2999
3000 let rebuilt = msg.rebuild(None).unwrap();
3003 k9::assert_equal!(rebuilt.headers().content_disposition(), Ok(None));
3004 }
3005
3006 #[test]
3007 fn attachment_filename_mess_aberrant() {
3008 let message = concat!(
3009 "Content-Disposition: attachment; filename= =?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\n",
3010 "\n\n"
3011 );
3012 let msg = MimePart::parse(message).unwrap();
3013
3014 let cd = msg.headers().content_disposition().unwrap().unwrap();
3015 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
3016
3017 let encoded = cd.encode_value();
3018 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?=");
3019 }
3020
3021 #[test]
3022 fn attachment_filename_mess_gmail() {
3023 let message = concat!(
3024 "Content-Disposition: attachment; filename=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
3025 "Content-Type: text/plain;\n",
3026 " name=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
3027 "\n\n"
3028 );
3029 let msg = MimePart::parse(message).unwrap();
3030
3031 let cd = msg.headers().content_disposition().unwrap().unwrap();
3032 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
3033 let encoded = cd.encode_value();
3034 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?=\"");
3035
3036 let ct = msg.headers().content_type().unwrap().unwrap();
3037 k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付");
3038 }
3039
3040 #[test]
3041 fn attachment_filename_mess_fastmail() {
3042 let message = concat!(
3043 "Content-Disposition: attachment;\n",
3044 " filename*0*=utf-8''%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E6%B7%BB%E4%BB%98;\n",
3045 " filename*1*=.txt\n",
3046 "Content-Type: text/plain;\n",
3047 " name=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=\"\n",
3048 " 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",
3049 "\n\n"
3050 );
3051 let msg = MimePart::parse(message).unwrap();
3052
3053 let cd = msg.headers().content_disposition().unwrap().unwrap();
3054 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付.txt");
3055
3056 let ct = msg.headers().content_type().unwrap().unwrap();
3057 eprintln!("{ct:#?}");
3058 k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付.txt");
3059 k9::assert_equal!(
3060 ct.get("x-name").unwrap(),
3061 "=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=bork"
3062 );
3063 }
3064
3065 #[test]
3066 fn rfc2047() {
3067 let message = concat!(
3068 "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
3069 "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
3070 "CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
3071 "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n",
3072 " =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
3073 "\n\n"
3074 );
3075 let msg = MimePart::parse(message).unwrap();
3076 let list = match msg.headers().from() {
3077 Err(err) => panic!("Doh.\n{err:#}"),
3078 Ok(list) => list,
3079 };
3080 k9::snapshot!(
3081 list,
3082 r#"
3083Some(
3084 MailboxList(
3085 [
3086 Mailbox {
3087 name: Some(
3088 "Keith Moore",
3089 ),
3090 address: AddrSpec {
3091 local_part: "moore",
3092 domain: "cs.utk.edu",
3093 },
3094 },
3095 ],
3096 ),
3097)
3098"#
3099 );
3100
3101 let list = match msg.headers().to() {
3102 Err(err) => panic!("Doh.\n{err:#}"),
3103 Ok(list) => list,
3104 };
3105 k9::snapshot!(
3106 list,
3107 r#"
3108Some(
3109 AddressList(
3110 [
3111 Mailbox(
3112 Mailbox {
3113 name: Some(
3114 "Keld Jørn Simonsen",
3115 ),
3116 address: AddrSpec {
3117 local_part: "keld",
3118 domain: "dkuug.dk",
3119 },
3120 },
3121 ),
3122 ],
3123 ),
3124)
3125"#
3126 );
3127
3128 let list = match msg.headers().cc() {
3129 Err(err) => panic!("Doh.\n{err:#}"),
3130 Ok(list) => list,
3131 };
3132 k9::snapshot!(
3133 list,
3134 r#"
3135Some(
3136 AddressList(
3137 [
3138 Mailbox(
3139 Mailbox {
3140 name: Some(
3141 "André Pirard",
3142 ),
3143 address: AddrSpec {
3144 local_part: "PIRARD",
3145 domain: "vm1.ulg.ac.be",
3146 },
3147 },
3148 ),
3149 ],
3150 ),
3151)
3152"#
3153 );
3154 let list = match msg.headers().subject() {
3155 Err(err) => panic!("Doh.\n{err:#}"),
3156 Ok(list) => list,
3157 };
3158 k9::snapshot!(
3159 list,
3160 r#"
3161Some(
3162 "Hello If you can read this you understand the example.",
3163)
3164"#
3165 );
3166
3167 k9::snapshot!(
3168 BString::from(msg.rebuild(None).unwrap().to_message_bytes()),
3169 r#"
3170Content-Type: text/plain;\r
3171\tcharset="us-ascii"\r
3172Content-Transfer-Encoding: quoted-printable\r
3173From: "Keith Moore" <moore@cs.utk.edu>\r
3174To: =?UTF-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r
3175Cc: =?UTF-8?q?Andr=C3=A9_Pirard?= <PIRARD@vm1.ulg.ac.be>\r
3176Subject: Hello If you can read this you understand the example.\r
3177\r
3178=0A\r
3179
3180"#
3181 );
3182 }
3183
3184 #[test]
3185 fn group_addresses() {
3186 let message = concat!(
3187 "To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;\n",
3188 "Cc: Undisclosed recipients:;\n",
3189 "\n\n\n"
3190 );
3191 let msg = MimePart::parse(message).unwrap();
3192 let list = match msg.headers().to() {
3193 Err(err) => panic!("Doh.\n{err:#}"),
3194 Ok(list) => list.unwrap(),
3195 };
3196
3197 k9::snapshot!(
3198 list.encode_value(),
3199 r#"
3200A Group:"Ed Jones" <c@a.test>,\r
3201\t<joe@where.test>,\r
3202\tJohn <jdoe@one.test>;
3203"#
3204 );
3205
3206 let round_trip = Header::new("To", list.clone());
3207 k9::assert_equal!(list, round_trip.as_address_list().unwrap());
3208
3209 k9::snapshot!(
3210 list,
3211 r#"
3212AddressList(
3213 [
3214 Group {
3215 name: "A Group",
3216 entries: MailboxList(
3217 [
3218 Mailbox {
3219 name: Some(
3220 "Ed Jones",
3221 ),
3222 address: AddrSpec {
3223 local_part: "c",
3224 domain: "a.test",
3225 },
3226 },
3227 Mailbox {
3228 name: None,
3229 address: AddrSpec {
3230 local_part: "joe",
3231 domain: "where.test",
3232 },
3233 },
3234 Mailbox {
3235 name: Some(
3236 "John",
3237 ),
3238 address: AddrSpec {
3239 local_part: "jdoe",
3240 domain: "one.test",
3241 },
3242 },
3243 ],
3244 ),
3245 },
3246 ],
3247)
3248"#
3249 );
3250
3251 let list = match msg.headers().cc() {
3252 Err(err) => panic!("Doh.\n{err:#}"),
3253 Ok(list) => list,
3254 };
3255 k9::snapshot!(
3256 list,
3257 r#"
3258Some(
3259 AddressList(
3260 [
3261 Group {
3262 name: "Undisclosed recipients",
3263 entries: MailboxList(
3264 [],
3265 ),
3266 },
3267 ],
3268 ),
3269)
3270"#
3271 );
3272 }
3273
3274 #[test]
3275 fn message_id() {
3276 let message = concat!(
3277 "Message-Id: <foo@example.com>\n",
3278 "References: <a@example.com> <b@example.com>\n",
3279 " <\"legacy\"@example.com>\n",
3280 " <literal@[127.0.0.1]>\n",
3281 "\n\n\n"
3282 );
3283 let msg = MimePart::parse(message).unwrap();
3284 let list = match msg.headers().message_id() {
3285 Err(err) => panic!("Doh.\n{err:#}"),
3286 Ok(list) => list,
3287 };
3288 k9::snapshot!(
3289 list,
3290 r#"
3291Some(
3292 MessageID(
3293 "foo@example.com",
3294 ),
3295)
3296"#
3297 );
3298
3299 let list = match msg.headers().references() {
3300 Err(err) => panic!("Doh.\n{err:#}"),
3301 Ok(list) => list,
3302 };
3303 k9::snapshot!(
3304 list,
3305 r#"
3306Some(
3307 [
3308 MessageID(
3309 "a@example.com",
3310 ),
3311 MessageID(
3312 "b@example.com",
3313 ),
3314 MessageID(
3315 "legacy@example.com",
3316 ),
3317 MessageID(
3318 "literal@[127.0.0.1]",
3319 ),
3320 ],
3321)
3322"#
3323 );
3324 }
3325
3326 #[test]
3327 fn content_type() {
3328 let message = "Content-Type: text/plain\n\n\n\n";
3329 let msg = MimePart::parse(message).unwrap();
3330 let params = match msg.headers().content_type() {
3331 Err(err) => panic!("Doh.\n{err:#}"),
3332 Ok(params) => params,
3333 };
3334 k9::snapshot!(
3335 params,
3336 r#"
3337Some(
3338 MimeParameters {
3339 value: "text/plain",
3340 parameters: [],
3341 },
3342)
3343"#
3344 );
3345
3346 let message = "Content-Type: text/plain; charset=us-ascii\n\n\n\n";
3347 let msg = MimePart::parse(message).unwrap();
3348 let params = match msg.headers().content_type() {
3349 Err(err) => panic!("Doh.\n{err:#}"),
3350 Ok(params) => params.unwrap(),
3351 };
3352
3353 k9::snapshot!(
3354 params.get("charset"),
3355 r#"
3356Some(
3357 "us-ascii",
3358)
3359"#
3360 );
3361 k9::snapshot!(
3362 params,
3363 r#"
3364MimeParameters {
3365 value: "text/plain",
3366 parameters: [
3367 MimeParameter {
3368 name: "charset",
3369 section: None,
3370 mime_charset: None,
3371 mime_language: None,
3372 encoding: None,
3373 value: "us-ascii",
3374 },
3375 ],
3376}
3377"#
3378 );
3379
3380 let message = "Content-Type: text/plain; charset=\"us-ascii\"\n\n\n\n";
3381 let msg = MimePart::parse(message).unwrap();
3382 let params = match msg.headers().content_type() {
3383 Err(err) => panic!("Doh.\n{err:#}"),
3384 Ok(params) => params,
3385 };
3386 k9::snapshot!(
3387 params,
3388 r#"
3389Some(
3390 MimeParameters {
3391 value: "text/plain",
3392 parameters: [
3393 MimeParameter {
3394 name: "charset",
3395 section: None,
3396 mime_charset: None,
3397 mime_language: None,
3398 encoding: None,
3399 value: "us-ascii",
3400 },
3401 ],
3402 },
3403)
3404"#
3405 );
3406 }
3407
3408 #[test]
3409 fn content_type_rfc2231() {
3410 let message = concat!(
3413 "Content-Type: application/x-stuff;\n",
3414 "\ttitle*0*=us-ascii'en'This%20is%20even%20more%20;\n",
3415 "\ttitle*1*=%2A%2A%2Afun%2A%2A%2A%20;\n",
3416 "\ttitle*2=\"isn't it!\"\n",
3417 "\n\n\n"
3418 );
3419 let msg = MimePart::parse(message).unwrap();
3420 let mut params = match msg.headers().content_type() {
3421 Err(err) => panic!("Doh.\n{err:#}"),
3422 Ok(params) => params.unwrap(),
3423 };
3424
3425 let original_title = params.get("title");
3426 k9::snapshot!(
3427 &original_title,
3428 r#"
3429Some(
3430 "This is even more ***fun*** isn't it!",
3431)
3432"#
3433 );
3434
3435 k9::snapshot!(
3436 ¶ms,
3437 r#"
3438MimeParameters {
3439 value: "application/x-stuff",
3440 parameters: [
3441 MimeParameter {
3442 name: "title",
3443 section: Some(
3444 0,
3445 ),
3446 mime_charset: Some(
3447 "us-ascii",
3448 ),
3449 mime_language: Some(
3450 "en",
3451 ),
3452 encoding: Rfc2231,
3453 value: "This%20is%20even%20more%20",
3454 },
3455 MimeParameter {
3456 name: "title",
3457 section: Some(
3458 1,
3459 ),
3460 mime_charset: None,
3461 mime_language: None,
3462 encoding: Rfc2231,
3463 value: "%2A%2A%2Afun%2A%2A%2A%20",
3464 },
3465 MimeParameter {
3466 name: "title",
3467 section: Some(
3468 2,
3469 ),
3470 mime_charset: None,
3471 mime_language: None,
3472 encoding: None,
3473 value: "isn't it!",
3474 },
3475 ],
3476}
3477"#
3478 );
3479
3480 k9::snapshot!(
3481 params.encode_value(),
3482 r#"
3483application/x-stuff;\r
3484\ttitle="This is even more ***fun*** isn't it!"
3485"#
3486 );
3487
3488 params.set("foo", "bar 💩");
3489
3490 params.set(
3491 "long",
3492 "this is some text that should wrap because \
3493 it should be a good bit longer than our target maximum \
3494 length for this sort of thing, and hopefully we see at \
3495 least three lines produced as a result of setting \
3496 this value in this way",
3497 );
3498
3499 params.set(
3500 "longernnamethananyoneshouldreallyuse",
3501 "this is some text that should wrap because \
3502 it should be a good bit longer than our target maximum \
3503 length for this sort of thing, and hopefully we see at \
3504 least three lines produced as a result of setting \
3505 this value in this way",
3506 );
3507
3508 k9::snapshot!(
3509 params.encode_value(),
3510 r#"
3511application/x-stuff;\r
3512\tfoo*=UTF-8''bar%20%F0%9F%92%A9;\r
3513\tlong*0="this is some text that should wrap because it should be a good bi";\r
3514\tlong*1="t longer than our target maximum length for this sort of thing, a";\r
3515\tlong*2="nd hopefully we see at least three lines produced as a result of ";\r
3516\tlong*3="setting this value in this way";\r
3517\tlongernnamethananyoneshouldreallyuse*0="this is some text that should wra";\r
3518\tlongernnamethananyoneshouldreallyuse*1="p because it should be a good bit";\r
3519\tlongernnamethananyoneshouldreallyuse*2=" longer than our target maximum l";\r
3520\tlongernnamethananyoneshouldreallyuse*3="ength for this sort of thing, and";\r
3521\tlongernnamethananyoneshouldreallyuse*4=" hopefully we see at least three ";\r
3522\tlongernnamethananyoneshouldreallyuse*5="lines produced as a result of set";\r
3523\tlongernnamethananyoneshouldreallyuse*6="ting this value in this way";\r
3524\ttitle="This is even more ***fun*** isn't it!"
3525"#
3526 );
3527 }
3528
3529 #[test]
3531 fn authentication_results_b_2() {
3532 let ar = Header::with_name_value("Authentication-Results", "example.org 1; none");
3533 let ar = ar.as_authentication_results().unwrap();
3534 k9::snapshot!(
3535 &ar,
3536 r#"
3537AuthenticationResults {
3538 serv_id: "example.org",
3539 version: Some(
3540 1,
3541 ),
3542 results: [],
3543}
3544"#
3545 );
3546
3547 k9::snapshot!(ar.encode_value(), "example.org 1; none");
3548 }
3549
3550 #[test]
3552 fn authentication_results_b_3() {
3553 let ar = Header::with_name_value(
3554 "Authentication-Results",
3555 "example.com; spf=pass smtp.mailfrom=example.net",
3556 );
3557 k9::snapshot!(
3558 ar.as_authentication_results(),
3559 r#"
3560Ok(
3561 AuthenticationResults {
3562 serv_id: "example.com",
3563 version: None,
3564 results: [
3565 AuthenticationResult {
3566 method: "spf",
3567 method_version: None,
3568 result: "pass",
3569 reason: None,
3570 props: {
3571 "smtp.mailfrom": "example.net",
3572 },
3573 },
3574 ],
3575 },
3576)
3577"#
3578 );
3579 }
3580
3581 #[test]
3583 fn authentication_results_b_4() {
3584 let ar = Header::with_name_value(
3585 "Authentication-Results",
3586 concat!(
3587 "example.com;\n",
3588 "\tauth=pass (cram-md5) smtp.auth=sender@example.net;\n",
3589 "\tspf=pass smtp.mailfrom=example.net"
3590 ),
3591 );
3592 k9::snapshot!(
3593 ar.as_authentication_results(),
3594 r#"
3595Ok(
3596 AuthenticationResults {
3597 serv_id: "example.com",
3598 version: None,
3599 results: [
3600 AuthenticationResult {
3601 method: "auth",
3602 method_version: None,
3603 result: "pass",
3604 reason: None,
3605 props: {
3606 "smtp.auth": "sender@example.net",
3607 },
3608 },
3609 AuthenticationResult {
3610 method: "spf",
3611 method_version: None,
3612 result: "pass",
3613 reason: None,
3614 props: {
3615 "smtp.mailfrom": "example.net",
3616 },
3617 },
3618 ],
3619 },
3620)
3621"#
3622 );
3623
3624 let ar = Header::with_name_value(
3625 "Authentication-Results",
3626 "example.com; iprev=pass\n\tpolicy.iprev=192.0.2.200",
3627 );
3628 k9::snapshot!(
3629 ar.as_authentication_results(),
3630 r#"
3631Ok(
3632 AuthenticationResults {
3633 serv_id: "example.com",
3634 version: None,
3635 results: [
3636 AuthenticationResult {
3637 method: "iprev",
3638 method_version: None,
3639 result: "pass",
3640 reason: None,
3641 props: {
3642 "policy.iprev": "192.0.2.200",
3643 },
3644 },
3645 ],
3646 },
3647)
3648"#
3649 );
3650 }
3651
3652 #[test]
3654 fn authentication_results_b_5() {
3655 let ar = Header::with_name_value(
3656 "Authentication-Results",
3657 "example.com;\n\tdkim=pass (good signature) header.d=example.com",
3658 );
3659 k9::snapshot!(
3660 ar.as_authentication_results(),
3661 r#"
3662Ok(
3663 AuthenticationResults {
3664 serv_id: "example.com",
3665 version: None,
3666 results: [
3667 AuthenticationResult {
3668 method: "dkim",
3669 method_version: None,
3670 result: "pass",
3671 reason: None,
3672 props: {
3673 "header.d": "example.com",
3674 },
3675 },
3676 ],
3677 },
3678)
3679"#
3680 );
3681
3682 let ar = Header::with_name_value(
3683 "Authentication-Results",
3684 "example.com;\n\tauth=pass (cram-md5) smtp.auth=sender@example.com;\n\tspf=fail smtp.mailfrom=example.com"
3685 );
3686 let ar = ar.as_authentication_results().unwrap();
3687 k9::snapshot!(
3688 &ar,
3689 r#"
3690AuthenticationResults {
3691 serv_id: "example.com",
3692 version: None,
3693 results: [
3694 AuthenticationResult {
3695 method: "auth",
3696 method_version: None,
3697 result: "pass",
3698 reason: None,
3699 props: {
3700 "smtp.auth": "sender@example.com",
3701 },
3702 },
3703 AuthenticationResult {
3704 method: "spf",
3705 method_version: None,
3706 result: "fail",
3707 reason: None,
3708 props: {
3709 "smtp.mailfrom": "example.com",
3710 },
3711 },
3712 ],
3713}
3714"#
3715 );
3716
3717 k9::snapshot!(
3718 ar.encode_value(),
3719 r#"
3720example.com;\r
3721\tauth=pass\r
3722\tsmtp.auth=sender@example.com;\r
3723\tspf=fail\r
3724\tsmtp.mailfrom=example.com
3725"#
3726 );
3727 }
3728
3729 #[test]
3731 fn authentication_results_b_6() {
3732 let ar = Header::with_name_value(
3733 "Authentication-Results",
3734 concat!(
3735 "example.com;\n",
3736 "\tdkim=pass reason=\"good signature\"\n",
3737 "\theader.i=@mail-router.example.net;\n",
3738 "\tdkim=fail reason=\"bad signature\"\n",
3739 "\theader.i=@newyork.example.com"
3740 ),
3741 );
3742 let ar = match ar.as_authentication_results() {
3743 Err(err) => panic!("\n{err}"),
3744 Ok(ar) => ar,
3745 };
3746
3747 k9::snapshot!(
3748 &ar,
3749 r#"
3750AuthenticationResults {
3751 serv_id: "example.com",
3752 version: None,
3753 results: [
3754 AuthenticationResult {
3755 method: "dkim",
3756 method_version: None,
3757 result: "pass",
3758 reason: Some(
3759 "good signature",
3760 ),
3761 props: {
3762 "header.i": "@mail-router.example.net",
3763 },
3764 },
3765 AuthenticationResult {
3766 method: "dkim",
3767 method_version: None,
3768 result: "fail",
3769 reason: Some(
3770 "bad signature",
3771 ),
3772 props: {
3773 "header.i": "@newyork.example.com",
3774 },
3775 },
3776 ],
3777}
3778"#
3779 );
3780
3781 k9::snapshot!(
3782 ar.encode_value(),
3783 r#"
3784example.com;\r
3785\tdkim=pass reason="good signature"\r
3786\theader.i=@mail-router.example.net;\r
3787\tdkim=fail reason="bad signature"\r
3788\theader.i=@newyork.example.com
3789"#
3790 );
3791
3792 let ar = Header::with_name_value(
3793 "Authentication-Results",
3794 concat!(
3795 "example.net;\n",
3796 "\tdkim=pass (good signature) header.i=@newyork.example.com"
3797 ),
3798 );
3799 let ar = match ar.as_authentication_results() {
3800 Err(err) => panic!("\n{err}"),
3801 Ok(ar) => ar,
3802 };
3803
3804 k9::snapshot!(
3805 &ar,
3806 r#"
3807AuthenticationResults {
3808 serv_id: "example.net",
3809 version: None,
3810 results: [
3811 AuthenticationResult {
3812 method: "dkim",
3813 method_version: None,
3814 result: "pass",
3815 reason: None,
3816 props: {
3817 "header.i": "@newyork.example.com",
3818 },
3819 },
3820 ],
3821}
3822"#
3823 );
3824
3825 k9::snapshot!(
3826 ar.encode_value(),
3827 r#"
3828example.net;\r
3829\tdkim=pass\r
3830\theader.i=@newyork.example.com
3831"#
3832 );
3833 }
3834
3835 #[test]
3837 fn authentication_results_b_7() {
3838 let ar = Header::with_name_value(
3839 "Authentication-Results",
3840 concat!(
3841 "foo.example.net (foobar) 1 (baz);\n",
3842 "\tdkim (Because I like it) / 1 (One yay) = (wait for it) fail\n",
3843 "\tpolicy (A dot can go here) . (like that) expired\n",
3844 "\t(this surprised me) = (as I wasn't expecting it) 1362471462"
3845 ),
3846 );
3847 let ar = match ar.as_authentication_results() {
3848 Err(err) => panic!("\n{err}"),
3849 Ok(ar) => ar,
3850 };
3851
3852 k9::snapshot!(
3853 &ar,
3854 r#"
3855AuthenticationResults {
3856 serv_id: "foo.example.net",
3857 version: Some(
3858 1,
3859 ),
3860 results: [
3861 AuthenticationResult {
3862 method: "dkim",
3863 method_version: Some(
3864 1,
3865 ),
3866 result: "fail",
3867 reason: None,
3868 props: {
3869 "policy.expired": "1362471462",
3870 },
3871 },
3872 ],
3873}
3874"#
3875 );
3876
3877 k9::snapshot!(
3878 ar.encode_value(),
3879 r#"
3880foo.example.net 1;\r
3881\tdkim/1=fail\r
3882\tpolicy.expired=1362471462
3883"#
3884 );
3885 }
3886
3887 #[test]
3888 fn arc_authentication_results_1() {
3889 let ar = Header::with_name_value(
3890 "ARC-Authentication-Results",
3891 "i=3; clochette.example.org; spf=fail
3892 smtp.from=jqd@d1.example; dkim=fail (512-bit key)
3893 header.i=@d1.example; dmarc=fail; arc=pass (as.2.gmail.example=pass,
3894 ams.2.gmail.example=pass, as.1.lists.example.org=pass,
3895 ams.1.lists.example.org=fail (message has been altered))",
3896 );
3897 let ar = match ar.as_arc_authentication_results() {
3898 Err(err) => panic!("\n{err}"),
3899 Ok(ar) => ar,
3900 };
3901
3902 k9::snapshot!(
3903 &ar,
3904 r#"
3905ARCAuthenticationResults {
3906 instance: 3,
3907 serv_id: "clochette.example.org",
3908 version: None,
3909 results: [
3910 AuthenticationResult {
3911 method: "spf",
3912 method_version: None,
3913 result: "fail",
3914 reason: None,
3915 props: {
3916 "smtp.from": "jqd@d1.example",
3917 },
3918 },
3919 AuthenticationResult {
3920 method: "dkim",
3921 method_version: None,
3922 result: "fail",
3923 reason: None,
3924 props: {
3925 "header.i": "@d1.example",
3926 },
3927 },
3928 AuthenticationResult {
3929 method: "dmarc",
3930 method_version: None,
3931 result: "fail",
3932 reason: None,
3933 props: {},
3934 },
3935 AuthenticationResult {
3936 method: "arc",
3937 method_version: None,
3938 result: "pass",
3939 reason: None,
3940 props: {},
3941 },
3942 ],
3943}
3944"#
3945 );
3946 }
3947
3948 #[test]
3949 fn bstring_utf8_serializes_utf8_as_string() {
3950 let mid = MessageID(BString::from("abc123@example.com"));
3952 let json = serde_json::to_string(&mid).unwrap();
3953 k9::assert_equal!(json, r#""abc123@example.com""#);
3954 }
3955
3956 #[test]
3957 fn bstring_utf8_serializes_non_utf8_as_array() {
3958 let mid = MessageID(BString::from(&b"hello\x80world"[..]));
3960 let json = serde_json::to_string(&mid).unwrap();
3961 k9::assert_equal!(json, "[104,101,108,108,111,128,119,111,114,108,100]");
3962 }
3963
3964 #[test]
3965 fn bstring_utf8_round_trip_utf8() {
3966 let mid = MessageID(BString::from("test@example.com"));
3967 let json = serde_json::to_string(&mid).unwrap();
3968 let restored: MessageID = serde_json::from_str(&json).unwrap();
3969 k9::assert_equal!(restored, mid);
3970 }
3971
3972 #[test]
3973 fn bstring_utf8_round_trip_non_utf8() {
3974 let mid = MessageID(BString::from(&b"\xff\xfe"[..]));
3975 let json = serde_json::to_string(&mid).unwrap();
3976 let restored: MessageID = serde_json::from_str(&json).unwrap();
3977 k9::assert_equal!(restored, mid);
3978 }
3979
3980 #[test]
3981 fn authentication_results_serialize_as_strings() {
3982 let ar = AuthenticationResults {
3983 serv_id: BString::from("example.com"),
3984 version: None,
3985 results: vec![AuthenticationResult {
3986 method: "dkim".into(),
3987 method_version: None,
3988 result: "pass".into(),
3989 reason: Some(BString::from("good signature")),
3990 props: BTreeMap::from([
3991 ("header.d".into(), BString::from("example.com")),
3992 ("header.s".into(), BString::from("selector1")),
3993 ]),
3994 }],
3995 };
3996 let json = serde_json::to_string_pretty(&ar).unwrap();
3997 k9::assert_equal!(
3999 json,
4000 r#"{
4001 "serv_id": "example.com",
4002 "version": null,
4003 "results": [
4004 {
4005 "method": "dkim",
4006 "method_version": null,
4007 "result": "pass",
4008 "reason": "good signature",
4009 "props": {
4010 "header.d": "example.com",
4011 "header.s": "selector1"
4012 }
4013 }
4014 ]
4015}"#
4016 );
4017 }
4018
4019 #[test]
4020 fn authentication_results_round_trip() {
4021 let ar = AuthenticationResults {
4022 serv_id: BString::from("mx.example.org"),
4023 version: Some(1),
4024 results: vec![AuthenticationResult {
4025 method: "spf".into(),
4026 method_version: None,
4027 result: "pass".into(),
4028 reason: None,
4029 props: BTreeMap::from([(
4030 "smtp.mailfrom".into(),
4031 BString::from("sender@example.com"),
4032 )]),
4033 }],
4034 };
4035 let json = serde_json::to_string(&ar).unwrap();
4036 let restored: AuthenticationResults = serde_json::from_str(&json).unwrap();
4037 k9::assert_equal!(restored, ar);
4038 }
4039
4040 #[test]
4041 fn authentication_result_non_utf8_reason() {
4042 let ar = AuthenticationResult {
4043 method: "dkim".into(),
4044 method_version: None,
4045 result: "temperror".into(),
4046 reason: Some(BString::from(&b"bad\x80data"[..])),
4047 props: BTreeMap::new(),
4048 };
4049 let json = serde_json::to_string(&ar).unwrap();
4050 assert!(json.contains(r#""reason":[98,97,100,128,100,97,116,97]"#));
4052 let restored: AuthenticationResult = serde_json::from_str(&json).unwrap();
4053 k9::assert_equal!(restored, ar);
4054 }
4055
4056 #[test]
4057 fn authentication_results_encode_value_with_binary() {
4058 let ar = AuthenticationResults {
4061 serv_id: BString::from(&b"mx.ex\x80mple.com"[..]),
4062 version: None,
4063 results: vec![AuthenticationResult {
4064 method: "spf".into(),
4065 method_version: None,
4066 result: "pass".into(),
4067 reason: Some(BString::from(&b"good\xffsig"[..])),
4068 props: BTreeMap::from([(
4069 "smtp.mailfrom".into(),
4070 BString::from(&b"user@\xfehost"[..]),
4071 )]),
4072 }],
4073 };
4074 let encoded = ar.encode_value();
4075 k9::snapshot!(
4076 encoded,
4077 r#"
4078"mx.ex\x80mple.com";\r
4079\tspf=pass reason="good\xffsig"\r
4080\tsmtp.mailfrom="user@\xfehost"
4081"#
4082 );
4083 }
4084
4085 #[test]
4086 fn authentication_results_serv_id_quoting() {
4087 let ar = AuthenticationResults {
4089 serv_id: BString::from("mx example.com"),
4090 version: None,
4091 results: vec![],
4092 };
4093 let encoded = ar.encode_value();
4094 k9::snapshot!(encoded, r#""mx example.com"; none"#);
4095
4096 let ar2 = AuthenticationResults {
4098 serv_id: BString::from("mx.example.com"),
4099 version: Some(1),
4100 results: vec![],
4101 };
4102 let encoded2 = ar2.encode_value();
4103 k9::snapshot!(&encoded2, "mx.example.com 1; none");
4104 let parsed = Parser::parse_authentication_results_header(encoded2.as_bytes()).unwrap();
4106 k9::assert_equal!(parsed.serv_id, ar2.serv_id);
4107 k9::assert_equal!(parsed.version, Some(1));
4108 }
4109
4110 #[test]
4111 fn arc_authentication_results_serialize_as_strings() {
4112 let arc = ARCAuthenticationResults {
4113 instance: 1,
4114 serv_id: BString::from("mx.example.com"),
4115 version: None,
4116 results: vec![],
4117 };
4118 let json = serde_json::to_string(&arc).unwrap();
4119 k9::assert_equal!(
4120 json,
4121 r#"{"instance":1,"serv_id":"mx.example.com","version":null,"results":[]}"#
4122 );
4123 }
4124}