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;
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,
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,
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,
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;
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() || matches!(c, '!' | '*' | '+' | '-' | '/') {
2463 Bytes::Passthru(bytes)
2464 } else if c == ' ' {
2465 Bytes::Passthru(b"_")
2466 } else {
2467 Bytes::Encode(bytes)
2468 };
2469
2470 let need_len = match b {
2471 Bytes::Passthru(b) => b.len(),
2472 Bytes::Encode(b) => b.len() * 3,
2473 };
2474
2475 if need_len > limit - line_length {
2476 result.extend_from_slice(suffix);
2478 result.extend_from_slice(b"\r\n\t");
2479 result.extend_from_slice(prefix);
2480 line_length = 0;
2481 }
2482
2483 match b {
2484 Bytes::Passthru(c) => {
2485 result.extend_from_slice(c);
2486 }
2487 Bytes::Encode(bytes) => {
2488 for &c in bytes {
2489 result.push(b'=');
2490 result.push(HEX_CHARS[(c as usize) >> 4]);
2491 result.push(HEX_CHARS[(c as usize) & 0x0f]);
2492 }
2493 }
2494 }
2495
2496 line_length += need_len;
2497 }
2498
2499 if line_length > 0 {
2500 result.extend_from_slice(suffix);
2501 }
2502
2503 unsafe { String::from_utf8_unchecked(result) }
2506}
2507
2508#[cfg(test)]
2509#[test]
2510fn test_qp_encode() {
2511 let encoded = qp_encode(
2512 b"hello, I am a line that is this long, or maybe a little \
2513 bit longer than this, and that should get wrapped by the encoder",
2514 );
2515 k9::snapshot!(
2516 encoded,
2517 r#"
2518=?UTF-8?q?hello=2C_I_am_a_line_that_is_this_long=2C_or_maybe_a_little_?=\r
2519\t=?UTF-8?q?bit_longer_than_this=2C_and_that_should_get_wrapped_by_the_e?=\r
2520\t=?UTF-8?q?ncoder?=
2521"#
2522 );
2523}
2524
2525#[cfg(test)]
2526#[test]
2527fn test_qp_encode_literal_underscore() {
2528 let encoded = qp_encode("formul\u{e1}rios Word_TEST".as_bytes());
2531 k9::assert_equal!(encoded, "=?UTF-8?q?formul=C3=A1rios_Word=5FTEST?=");
2532}
2533
2534fn quote_string(s: impl AsRef<[u8]>) -> BString {
2538 let s = s.as_ref();
2539
2540 if s.iter().any(|&c| !is_atext(c)) {
2541 let mut result = Vec::<u8>::with_capacity(s.len() + 4);
2542 result.push(b'"');
2543 for (start, end, c) in s.char_indices() {
2544 let c = c as u32;
2545 if c <= 0xff {
2546 let c = c as u8;
2547 if !c.is_ascii_whitespace() && !is_qtext(c) && !is_atext(c) {
2548 result.push(b'\\');
2549 }
2550 }
2551 result.push_str(&s[start..end]);
2552 }
2553 result.push(b'"');
2554 result.into()
2555 } else {
2556 s.into()
2557 }
2558}
2559
2560#[cfg(test)]
2561#[test]
2562fn test_quote_string() {
2563 k9::snapshot!(
2564 quote_string("TEST [ne_pas_repondre]"),
2565 r#""TEST [ne_pas_repondre]""#
2566 );
2567 k9::snapshot!(quote_string("hello"), "hello");
2568 k9::snapshot!(quote_string("hello there"), r#""hello there""#);
2569 k9::snapshot!(quote_string("hello, there"), "\"hello, there\"");
2570 k9::snapshot!(quote_string("hello \"there\""), r#""hello \\"there\\"""#);
2571 k9::snapshot!(
2572 quote_string("hello c:\\backslash"),
2573 r#""hello c:\\\\backslash""#
2574 );
2575 k9::assert_equal!(quote_string("hello\n there"), "\"hello\n there\"");
2576}
2577
2578impl EncodeHeaderValue for Mailbox {
2579 fn encode_value(&self) -> SharedString<'static> {
2580 match &self.name {
2581 Some(name) => {
2582 let mut value: Vec<u8> = if name.is_ascii() {
2583 quote_string(name).into()
2584 } else {
2585 qp_encode(name.as_bytes()).into_bytes()
2586 };
2587
2588 value.push_str(" <");
2589 value.push_str(self.address.encode_value().as_bytes());
2590 value.push(b'>');
2591 value.into()
2592 }
2593 None => {
2594 let mut result: Vec<u8> = vec![];
2595 result.push(b'<');
2596 result.push_str(self.address.encode_value().as_bytes());
2597 result.push(b'>');
2598 result.into()
2599 }
2600 }
2601 }
2602}
2603
2604impl EncodeHeaderValue for MailboxList {
2605 fn encode_value(&self) -> SharedString<'static> {
2606 let mut result: Vec<u8> = vec![];
2607 for mailbox in &self.0 {
2608 if !result.is_empty() {
2609 result.push_str(",\r\n\t");
2610 }
2611 result.push_str(mailbox.encode_value().as_bytes());
2612 }
2613 result.into()
2614 }
2615}
2616
2617impl EncodeHeaderValue for Address {
2618 fn encode_value(&self) -> SharedString<'static> {
2619 match self {
2620 Self::Mailbox(mbox) => mbox.encode_value(),
2621 Self::Group { name, entries } => {
2622 let mut result: Vec<u8> = vec![];
2623 result.push_str(name);
2624 result.push(b':');
2625 result.push_str(entries.encode_value().as_bytes());
2626 result.push(b';');
2627 result.into()
2628 }
2629 }
2630 }
2631}
2632
2633impl EncodeHeaderValue for AddressList {
2634 fn encode_value(&self) -> SharedString<'static> {
2635 let mut result: Vec<u8> = vec![];
2636 for address in &self.0 {
2637 if !result.is_empty() {
2638 result.push_str(",\r\n\t");
2639 }
2640 result.push_str(address.encode_value().as_bytes());
2641 }
2642 result.into()
2643 }
2644}
2645
2646#[cfg(test)]
2647mod test {
2648 use super::*;
2649 use crate::{Header, MessageConformance, MimePart};
2650
2651 #[test]
2652 fn mailbox_encodes_at() {
2653 let mbox = Mailbox {
2654 name: Some("foo@bar.com".into()),
2655 address: AddrSpec {
2656 local_part: "foo".into(),
2657 domain: "bar.com".into(),
2658 },
2659 };
2660 assert_eq!(mbox.encode_value(), "\"foo@bar.com\" <foo@bar.com>");
2661 }
2662
2663 #[test]
2664 fn mailbox_list_singular() {
2665 let message = concat!(
2666 "From: Someone (hello) <someone@example.com>, other@example.com,\n",
2667 " \"John \\\"Smith\\\"\" (comment) \"More Quotes\" (more comment) <someone(another comment)@crazy.example.com(woot)>\n",
2668 "\n",
2669 "I am the body"
2670 );
2671 let msg = MimePart::parse(message).unwrap();
2672 let list = match msg.headers().from() {
2673 Err(err) => panic!("Doh.\n{err:#}"),
2674 Ok(list) => list,
2675 };
2676
2677 k9::snapshot!(
2678 list,
2679 r#"
2680Some(
2681 MailboxList(
2682 [
2683 Mailbox {
2684 name: Some(
2685 "Someone",
2686 ),
2687 address: AddrSpec {
2688 local_part: "someone",
2689 domain: "example.com",
2690 },
2691 },
2692 Mailbox {
2693 name: None,
2694 address: AddrSpec {
2695 local_part: "other",
2696 domain: "example.com",
2697 },
2698 },
2699 Mailbox {
2700 name: Some(
2701 "John "Smith" More Quotes",
2702 ),
2703 address: AddrSpec {
2704 local_part: "someone",
2705 domain: "crazy.example.com",
2706 },
2707 },
2708 ],
2709 ),
2710)
2711"#
2712 );
2713 }
2714
2715 #[test]
2716 fn docomo_non_compliant_localpart() {
2717 let message = "Sender: hello..there@docomo.ne.jp\n\n\n";
2718 let msg = MimePart::parse(message).unwrap();
2719 let err = msg.headers().sender().unwrap_err();
2720 k9::snapshot!(
2721 err,
2722 r#"
2723InvalidHeaderValueDuringGet {
2724 header_name: "Sender",
2725 error: HeaderParse(
2726 "Error at line 1, expected "@" but found ".":
2727hello..there@docomo.ne.jp
2728 ^___________________
2729
2730while parsing addr_spec
2731while parsing mailbox
2732",
2733 ),
2734}
2735"#
2736 );
2737 }
2738
2739 #[test]
2740 fn sender() {
2741 let message = "Sender: someone@[127.0.0.1]\n\n\n";
2742 let msg = MimePart::parse(message).unwrap();
2743 let list = match msg.headers().sender() {
2744 Err(err) => panic!("Doh.\n{err:#}"),
2745 Ok(list) => list,
2746 };
2747 k9::snapshot!(
2748 list,
2749 r#"
2750Some(
2751 Mailbox {
2752 name: None,
2753 address: AddrSpec {
2754 local_part: "someone",
2755 domain: "[127.0.0.1]",
2756 },
2757 },
2758)
2759"#
2760 );
2761 }
2762
2763 #[test]
2764 fn domain_literal() {
2765 let message = "From: someone@[127.0.0.1]\n\n\n";
2766 let msg = MimePart::parse(message).unwrap();
2767 let list = match msg.headers().from() {
2768 Err(err) => panic!("Doh.\n{err:#}"),
2769 Ok(list) => list,
2770 };
2771 k9::snapshot!(
2772 list,
2773 r#"
2774Some(
2775 MailboxList(
2776 [
2777 Mailbox {
2778 name: None,
2779 address: AddrSpec {
2780 local_part: "someone",
2781 domain: "[127.0.0.1]",
2782 },
2783 },
2784 ],
2785 ),
2786)
2787"#
2788 );
2789 }
2790
2791 #[test]
2792 fn rfc6532() {
2793 let message = concat!(
2794 "From: Keith Moore <moore@cs.utk.edu>\n",
2795 "To: Keld Jørn Simonsen <keld@dkuug.dk>\n",
2796 "CC: André Pirard <PIRARD@vm1.ulg.ac.be>\n",
2797 "Subject: Hello André\n",
2798 "\n\n"
2799 );
2800 let msg = MimePart::parse(message).unwrap();
2801 let list = match msg.headers().from() {
2802 Err(err) => panic!("Doh.\n{err:#}"),
2803 Ok(list) => list,
2804 };
2805 k9::snapshot!(
2806 list,
2807 r#"
2808Some(
2809 MailboxList(
2810 [
2811 Mailbox {
2812 name: Some(
2813 "Keith Moore",
2814 ),
2815 address: AddrSpec {
2816 local_part: "moore",
2817 domain: "cs.utk.edu",
2818 },
2819 },
2820 ],
2821 ),
2822)
2823"#
2824 );
2825
2826 let list = match msg.headers().to() {
2827 Err(err) => panic!("Doh.\n{err:#}"),
2828 Ok(list) => list,
2829 };
2830 k9::snapshot!(
2831 list,
2832 r#"
2833Some(
2834 AddressList(
2835 [
2836 Mailbox(
2837 Mailbox {
2838 name: Some(
2839 "Keld Jørn Simonsen",
2840 ),
2841 address: AddrSpec {
2842 local_part: "keld",
2843 domain: "dkuug.dk",
2844 },
2845 },
2846 ),
2847 ],
2848 ),
2849)
2850"#
2851 );
2852
2853 let list = match msg.headers().cc() {
2854 Err(err) => panic!("Doh.\n{err:#}"),
2855 Ok(list) => list,
2856 };
2857 k9::snapshot!(
2858 list,
2859 r#"
2860Some(
2861 AddressList(
2862 [
2863 Mailbox(
2864 Mailbox {
2865 name: Some(
2866 "André Pirard",
2867 ),
2868 address: AddrSpec {
2869 local_part: "PIRARD",
2870 domain: "vm1.ulg.ac.be",
2871 },
2872 },
2873 ),
2874 ],
2875 ),
2876)
2877"#
2878 );
2879 let list = match msg.headers().subject() {
2880 Err(err) => panic!("Doh.\n{err:#}"),
2881 Ok(list) => list,
2882 };
2883 k9::snapshot!(
2884 list,
2885 r#"
2886Some(
2887 "Hello André",
2888)
2889"#
2890 );
2891 }
2892
2893 #[test]
2894 fn unstructured_bare_non_ascii() {
2895 let message = "Subject: Héllo wörld äöü\n\n\n";
2898 let msg = MimePart::parse(message).unwrap();
2899 k9::snapshot!(
2900 msg.headers().subject().unwrap(),
2901 r#"
2902Some(
2903 "Héllo wörld äöü",
2904)
2905"#
2906 );
2907
2908 let message = "Subject: 件名テスト\n\n\n";
2910 let msg = MimePart::parse(message).unwrap();
2911 k9::snapshot!(
2912 msg.headers().subject().unwrap(),
2913 r#"
2914Some(
2915 "件名テスト",
2916)
2917"#
2918 );
2919 }
2920
2921 #[test]
2922 fn unstructured_raw_shift_jis() {
2923 let message = b"Subject: \x83\x65\x83\x58\x83\x67\n\n\n";
2930
2931 let msg = MimePart::parse(message.as_slice()).unwrap();
2934 let subject_header = msg.headers().get_first("Subject").unwrap();
2935 k9::assert_equal!(
2936 subject_header.get_raw_value(),
2937 b"\x83\x65\x83\x58\x83\x67".as_slice()
2938 );
2939
2940 k9::snapshot!(
2943 msg.headers().subject(),
2944 r#"
2945Err(
2946 InvalidHeaderValueDuringGet {
2947 header_name: "Subject",
2948 error: HeaderParse(
2949 "Error at line 1, in Eof:
2950\\x83e\\x83X\\x83g
2951^_____
2952
2953",
2954 ),
2955 },
2956)
2957"#
2958 );
2959 }
2960
2961 #[test]
2962 fn rfc2047_bogus() {
2963 let message = concat!(
2964 "From: =?US-OSCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
2965 "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
2966 "CC: =?ISO-8859-1?Q?Andr=E?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
2967 "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?=\n",
2968 " =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
2969 "\n\n"
2970 );
2971 let msg = MimePart::parse(message).unwrap();
2972
2973 k9::assert_equal!(
2976 msg.headers().from().unwrap().unwrap().0[0]
2977 .name
2978 .as_ref()
2979 .unwrap(),
2980 "=?US-OSCII?Q?Keith_Moore?="
2981 );
2982
2983 match &msg.headers().cc().unwrap().unwrap().0[0] {
2984 Address::Mailbox(mbox) => {
2985 k9::assert_equal!(mbox.name.as_ref().unwrap(), "Andr=E Pirard");
2989 }
2990 wat => panic!("should not have {wat:?}"),
2991 }
2992
2993 k9::assert_equal!(
2996 msg.headers().subject().unwrap().unwrap(),
2997 "Hello =?ISO-8859-1?B?SWYgeW91IGNhb!ByZWFkIHRoaXMgeW8=?= u understand the example."
2998 );
2999 }
3000
3001 #[test]
3002 fn attachment_filename_mess_totally_bogus() {
3003 let message = concat!("Content-Disposition: attachment; filename=@\n", "\n\n");
3004 let msg = MimePart::parse(message).unwrap();
3005 eprintln!("{msg:#?}");
3006
3007 assert!(msg
3008 .conformance()
3009 .contains(MessageConformance::INVALID_MIME_HEADERS));
3010 msg.headers().content_disposition().unwrap_err();
3011
3012 let rebuilt = msg.rebuild(None).unwrap();
3015 k9::assert_equal!(rebuilt.headers().content_disposition(), Ok(None));
3016 }
3017
3018 #[test]
3019 fn attachment_filename_mess_aberrant() {
3020 let message = concat!(
3021 "Content-Disposition: attachment; filename= =?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\n",
3022 "\n\n"
3023 );
3024 let msg = MimePart::parse(message).unwrap();
3025
3026 let cd = msg.headers().content_disposition().unwrap().unwrap();
3027 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
3028
3029 let encoded = cd.encode_value();
3030 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?=");
3031 }
3032
3033 #[test]
3034 fn attachment_filename_mess_gmail() {
3035 let message = concat!(
3036 "Content-Disposition: attachment; filename=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
3037 "Content-Type: text/plain;\n",
3038 " name=\"=?UTF-8?B?5pel5pys6Kqe44Gu5re75LuY?=\"\n",
3039 "\n\n"
3040 );
3041 let msg = MimePart::parse(message).unwrap();
3042
3043 let cd = msg.headers().content_disposition().unwrap().unwrap();
3044 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付");
3045 let encoded = cd.encode_value();
3046 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?=\"");
3047
3048 let ct = msg.headers().content_type().unwrap().unwrap();
3049 k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付");
3050 }
3051
3052 #[test]
3053 fn attachment_filename_mess_fastmail() {
3054 let message = concat!(
3055 "Content-Disposition: attachment;\n",
3056 " filename*0*=utf-8''%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E6%B7%BB%E4%BB%98;\n",
3057 " filename*1*=.txt\n",
3058 "Content-Type: text/plain;\n",
3059 " name=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=\"\n",
3060 " 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",
3061 "\n\n"
3062 );
3063 let msg = MimePart::parse(message).unwrap();
3064
3065 let cd = msg.headers().content_disposition().unwrap().unwrap();
3066 k9::assert_equal!(cd.get("filename").unwrap(), "日本語の添付.txt");
3067
3068 let ct = msg.headers().content_type().unwrap().unwrap();
3069 eprintln!("{ct:#?}");
3070 k9::assert_equal!(ct.get("name").unwrap(), "日本語の添付.txt");
3071 k9::assert_equal!(
3072 ct.get("x-name").unwrap(),
3073 "=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE=E6=B7=BB=E4=BB=98.txt?=bork"
3074 );
3075 }
3076
3077 #[test]
3078 fn rfc2047() {
3079 let message = concat!(
3080 "From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>\n",
3081 "To: =?ISO-8859-1*en-us?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>\n",
3082 "CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>\n",
3083 "Subject: Hello =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\n",
3084 " =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=\n",
3085 "\n\n"
3086 );
3087 let msg = MimePart::parse(message).unwrap();
3088 let list = match msg.headers().from() {
3089 Err(err) => panic!("Doh.\n{err:#}"),
3090 Ok(list) => list,
3091 };
3092 k9::snapshot!(
3093 list,
3094 r#"
3095Some(
3096 MailboxList(
3097 [
3098 Mailbox {
3099 name: Some(
3100 "Keith Moore",
3101 ),
3102 address: AddrSpec {
3103 local_part: "moore",
3104 domain: "cs.utk.edu",
3105 },
3106 },
3107 ],
3108 ),
3109)
3110"#
3111 );
3112
3113 let list = match msg.headers().to() {
3114 Err(err) => panic!("Doh.\n{err:#}"),
3115 Ok(list) => list,
3116 };
3117 k9::snapshot!(
3118 list,
3119 r#"
3120Some(
3121 AddressList(
3122 [
3123 Mailbox(
3124 Mailbox {
3125 name: Some(
3126 "Keld Jørn Simonsen",
3127 ),
3128 address: AddrSpec {
3129 local_part: "keld",
3130 domain: "dkuug.dk",
3131 },
3132 },
3133 ),
3134 ],
3135 ),
3136)
3137"#
3138 );
3139
3140 let list = match msg.headers().cc() {
3141 Err(err) => panic!("Doh.\n{err:#}"),
3142 Ok(list) => list,
3143 };
3144 k9::snapshot!(
3145 list,
3146 r#"
3147Some(
3148 AddressList(
3149 [
3150 Mailbox(
3151 Mailbox {
3152 name: Some(
3153 "André Pirard",
3154 ),
3155 address: AddrSpec {
3156 local_part: "PIRARD",
3157 domain: "vm1.ulg.ac.be",
3158 },
3159 },
3160 ),
3161 ],
3162 ),
3163)
3164"#
3165 );
3166 let list = match msg.headers().subject() {
3167 Err(err) => panic!("Doh.\n{err:#}"),
3168 Ok(list) => list,
3169 };
3170 k9::snapshot!(
3171 list,
3172 r#"
3173Some(
3174 "Hello If you can read this you understand the example.",
3175)
3176"#
3177 );
3178
3179 k9::snapshot!(
3180 BString::from(msg.rebuild(None).unwrap().to_message_bytes()),
3181 r#"
3182Content-Type: text/plain;\r
3183\tcharset="us-ascii"\r
3184Content-Transfer-Encoding: quoted-printable\r
3185From: "Keith Moore" <moore@cs.utk.edu>\r
3186To: =?UTF-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r
3187Cc: =?UTF-8?q?Andr=C3=A9_Pirard?= <PIRARD@vm1.ulg.ac.be>\r
3188Subject: Hello If you can read this you understand the example.\r
3189\r
3190=0A\r
3191
3192"#
3193 );
3194 }
3195
3196 #[test]
3197 fn group_addresses() {
3198 let message = concat!(
3199 "To: A Group:Ed Jones <c@a.test>,joe@where.test,John <jdoe@one.test>;\n",
3200 "Cc: Undisclosed recipients:;\n",
3201 "\n\n\n"
3202 );
3203 let msg = MimePart::parse(message).unwrap();
3204 let list = match msg.headers().to() {
3205 Err(err) => panic!("Doh.\n{err:#}"),
3206 Ok(list) => list.unwrap(),
3207 };
3208
3209 k9::snapshot!(
3210 list.encode_value(),
3211 r#"
3212A Group:"Ed Jones" <c@a.test>,\r
3213\t<joe@where.test>,\r
3214\tJohn <jdoe@one.test>;
3215"#
3216 );
3217
3218 let round_trip = Header::new("To", list.clone());
3219 k9::assert_equal!(list, round_trip.as_address_list().unwrap());
3220
3221 k9::snapshot!(
3222 list,
3223 r#"
3224AddressList(
3225 [
3226 Group {
3227 name: "A Group",
3228 entries: MailboxList(
3229 [
3230 Mailbox {
3231 name: Some(
3232 "Ed Jones",
3233 ),
3234 address: AddrSpec {
3235 local_part: "c",
3236 domain: "a.test",
3237 },
3238 },
3239 Mailbox {
3240 name: None,
3241 address: AddrSpec {
3242 local_part: "joe",
3243 domain: "where.test",
3244 },
3245 },
3246 Mailbox {
3247 name: Some(
3248 "John",
3249 ),
3250 address: AddrSpec {
3251 local_part: "jdoe",
3252 domain: "one.test",
3253 },
3254 },
3255 ],
3256 ),
3257 },
3258 ],
3259)
3260"#
3261 );
3262
3263 let list = match msg.headers().cc() {
3264 Err(err) => panic!("Doh.\n{err:#}"),
3265 Ok(list) => list,
3266 };
3267 k9::snapshot!(
3268 list,
3269 r#"
3270Some(
3271 AddressList(
3272 [
3273 Group {
3274 name: "Undisclosed recipients",
3275 entries: MailboxList(
3276 [],
3277 ),
3278 },
3279 ],
3280 ),
3281)
3282"#
3283 );
3284 }
3285
3286 #[test]
3287 fn message_id() {
3288 let message = concat!(
3289 "Message-Id: <foo@example.com>\n",
3290 "References: <a@example.com> <b@example.com>\n",
3291 " <\"legacy\"@example.com>\n",
3292 " <literal@[127.0.0.1]>\n",
3293 "\n\n\n"
3294 );
3295 let msg = MimePart::parse(message).unwrap();
3296 let list = match msg.headers().message_id() {
3297 Err(err) => panic!("Doh.\n{err:#}"),
3298 Ok(list) => list,
3299 };
3300 k9::snapshot!(
3301 list,
3302 r#"
3303Some(
3304 MessageID(
3305 "foo@example.com",
3306 ),
3307)
3308"#
3309 );
3310
3311 let list = match msg.headers().references() {
3312 Err(err) => panic!("Doh.\n{err:#}"),
3313 Ok(list) => list,
3314 };
3315 k9::snapshot!(
3316 list,
3317 r#"
3318Some(
3319 [
3320 MessageID(
3321 "a@example.com",
3322 ),
3323 MessageID(
3324 "b@example.com",
3325 ),
3326 MessageID(
3327 "legacy@example.com",
3328 ),
3329 MessageID(
3330 "literal@[127.0.0.1]",
3331 ),
3332 ],
3333)
3334"#
3335 );
3336 }
3337
3338 #[test]
3339 fn content_type() {
3340 let message = "Content-Type: text/plain\n\n\n\n";
3341 let msg = MimePart::parse(message).unwrap();
3342 let params = match msg.headers().content_type() {
3343 Err(err) => panic!("Doh.\n{err:#}"),
3344 Ok(params) => params,
3345 };
3346 k9::snapshot!(
3347 params,
3348 r#"
3349Some(
3350 MimeParameters {
3351 value: "text/plain",
3352 parameters: [],
3353 },
3354)
3355"#
3356 );
3357
3358 let message = "Content-Type: text/plain; charset=us-ascii\n\n\n\n";
3359 let msg = MimePart::parse(message).unwrap();
3360 let params = match msg.headers().content_type() {
3361 Err(err) => panic!("Doh.\n{err:#}"),
3362 Ok(params) => params.unwrap(),
3363 };
3364
3365 k9::snapshot!(
3366 params.get("charset"),
3367 r#"
3368Some(
3369 "us-ascii",
3370)
3371"#
3372 );
3373 k9::snapshot!(
3374 params,
3375 r#"
3376MimeParameters {
3377 value: "text/plain",
3378 parameters: [
3379 MimeParameter {
3380 name: "charset",
3381 section: None,
3382 mime_charset: None,
3383 mime_language: None,
3384 encoding: None,
3385 value: "us-ascii",
3386 },
3387 ],
3388}
3389"#
3390 );
3391
3392 let message = "Content-Type: text/plain; charset=\"us-ascii\"\n\n\n\n";
3393 let msg = MimePart::parse(message).unwrap();
3394 let params = match msg.headers().content_type() {
3395 Err(err) => panic!("Doh.\n{err:#}"),
3396 Ok(params) => params,
3397 };
3398 k9::snapshot!(
3399 params,
3400 r#"
3401Some(
3402 MimeParameters {
3403 value: "text/plain",
3404 parameters: [
3405 MimeParameter {
3406 name: "charset",
3407 section: None,
3408 mime_charset: None,
3409 mime_language: None,
3410 encoding: None,
3411 value: "us-ascii",
3412 },
3413 ],
3414 },
3415)
3416"#
3417 );
3418 }
3419
3420 #[test]
3421 fn content_type_rfc2231() {
3422 let message = concat!(
3425 "Content-Type: application/x-stuff;\n",
3426 "\ttitle*0*=us-ascii'en'This%20is%20even%20more%20;\n",
3427 "\ttitle*1*=%2A%2A%2Afun%2A%2A%2A%20;\n",
3428 "\ttitle*2=\"isn't it!\"\n",
3429 "\n\n\n"
3430 );
3431 let msg = MimePart::parse(message).unwrap();
3432 let mut params = match msg.headers().content_type() {
3433 Err(err) => panic!("Doh.\n{err:#}"),
3434 Ok(params) => params.unwrap(),
3435 };
3436
3437 let original_title = params.get("title");
3438 k9::snapshot!(
3439 &original_title,
3440 r#"
3441Some(
3442 "This is even more ***fun*** isn't it!",
3443)
3444"#
3445 );
3446
3447 k9::snapshot!(
3448 ¶ms,
3449 r#"
3450MimeParameters {
3451 value: "application/x-stuff",
3452 parameters: [
3453 MimeParameter {
3454 name: "title",
3455 section: Some(
3456 0,
3457 ),
3458 mime_charset: Some(
3459 "us-ascii",
3460 ),
3461 mime_language: Some(
3462 "en",
3463 ),
3464 encoding: Rfc2231,
3465 value: "This%20is%20even%20more%20",
3466 },
3467 MimeParameter {
3468 name: "title",
3469 section: Some(
3470 1,
3471 ),
3472 mime_charset: None,
3473 mime_language: None,
3474 encoding: Rfc2231,
3475 value: "%2A%2A%2Afun%2A%2A%2A%20",
3476 },
3477 MimeParameter {
3478 name: "title",
3479 section: Some(
3480 2,
3481 ),
3482 mime_charset: None,
3483 mime_language: None,
3484 encoding: None,
3485 value: "isn't it!",
3486 },
3487 ],
3488}
3489"#
3490 );
3491
3492 k9::snapshot!(
3493 params.encode_value(),
3494 r#"
3495application/x-stuff;\r
3496\ttitle="This is even more ***fun*** isn't it!"
3497"#
3498 );
3499
3500 params.set("foo", "bar 💩");
3501
3502 params.set(
3503 "long",
3504 "this is some text that should wrap because \
3505 it should be a good bit longer than our target maximum \
3506 length for this sort of thing, and hopefully we see at \
3507 least three lines produced as a result of setting \
3508 this value in this way",
3509 );
3510
3511 params.set(
3512 "longernnamethananyoneshouldreallyuse",
3513 "this is some text that should wrap because \
3514 it should be a good bit longer than our target maximum \
3515 length for this sort of thing, and hopefully we see at \
3516 least three lines produced as a result of setting \
3517 this value in this way",
3518 );
3519
3520 k9::snapshot!(
3521 params.encode_value(),
3522 r#"
3523application/x-stuff;\r
3524\tfoo*=UTF-8''bar%20%F0%9F%92%A9;\r
3525\tlong*0="this is some text that should wrap because it should be a good bi";\r
3526\tlong*1="t longer than our target maximum length for this sort of thing, a";\r
3527\tlong*2="nd hopefully we see at least three lines produced as a result of ";\r
3528\tlong*3="setting this value in this way";\r
3529\tlongernnamethananyoneshouldreallyuse*0="this is some text that should wra";\r
3530\tlongernnamethananyoneshouldreallyuse*1="p because it should be a good bit";\r
3531\tlongernnamethananyoneshouldreallyuse*2=" longer than our target maximum l";\r
3532\tlongernnamethananyoneshouldreallyuse*3="ength for this sort of thing, and";\r
3533\tlongernnamethananyoneshouldreallyuse*4=" hopefully we see at least three ";\r
3534\tlongernnamethananyoneshouldreallyuse*5="lines produced as a result of set";\r
3535\tlongernnamethananyoneshouldreallyuse*6="ting this value in this way";\r
3536\ttitle="This is even more ***fun*** isn't it!"
3537"#
3538 );
3539 }
3540
3541 #[test]
3543 fn authentication_results_b_2() {
3544 let ar = Header::with_name_value("Authentication-Results", "example.org 1; none");
3545 let ar = ar.as_authentication_results().unwrap();
3546 k9::snapshot!(
3547 &ar,
3548 r#"
3549AuthenticationResults {
3550 serv_id: "example.org",
3551 version: Some(
3552 1,
3553 ),
3554 results: [],
3555}
3556"#
3557 );
3558
3559 k9::snapshot!(ar.encode_value(), "example.org 1; none");
3560 }
3561
3562 #[test]
3564 fn authentication_results_b_3() {
3565 let ar = Header::with_name_value(
3566 "Authentication-Results",
3567 "example.com; spf=pass smtp.mailfrom=example.net",
3568 );
3569 k9::snapshot!(
3570 ar.as_authentication_results(),
3571 r#"
3572Ok(
3573 AuthenticationResults {
3574 serv_id: "example.com",
3575 version: None,
3576 results: [
3577 AuthenticationResult {
3578 method: "spf",
3579 method_version: None,
3580 result: "pass",
3581 reason: None,
3582 props: {
3583 "smtp.mailfrom": "example.net",
3584 },
3585 },
3586 ],
3587 },
3588)
3589"#
3590 );
3591 }
3592
3593 #[test]
3595 fn authentication_results_b_4() {
3596 let ar = Header::with_name_value(
3597 "Authentication-Results",
3598 concat!(
3599 "example.com;\n",
3600 "\tauth=pass (cram-md5) smtp.auth=sender@example.net;\n",
3601 "\tspf=pass smtp.mailfrom=example.net"
3602 ),
3603 );
3604 k9::snapshot!(
3605 ar.as_authentication_results(),
3606 r#"
3607Ok(
3608 AuthenticationResults {
3609 serv_id: "example.com",
3610 version: None,
3611 results: [
3612 AuthenticationResult {
3613 method: "auth",
3614 method_version: None,
3615 result: "pass",
3616 reason: None,
3617 props: {
3618 "smtp.auth": "sender@example.net",
3619 },
3620 },
3621 AuthenticationResult {
3622 method: "spf",
3623 method_version: None,
3624 result: "pass",
3625 reason: None,
3626 props: {
3627 "smtp.mailfrom": "example.net",
3628 },
3629 },
3630 ],
3631 },
3632)
3633"#
3634 );
3635
3636 let ar = Header::with_name_value(
3637 "Authentication-Results",
3638 "example.com; iprev=pass\n\tpolicy.iprev=192.0.2.200",
3639 );
3640 k9::snapshot!(
3641 ar.as_authentication_results(),
3642 r#"
3643Ok(
3644 AuthenticationResults {
3645 serv_id: "example.com",
3646 version: None,
3647 results: [
3648 AuthenticationResult {
3649 method: "iprev",
3650 method_version: None,
3651 result: "pass",
3652 reason: None,
3653 props: {
3654 "policy.iprev": "192.0.2.200",
3655 },
3656 },
3657 ],
3658 },
3659)
3660"#
3661 );
3662 }
3663
3664 #[test]
3666 fn authentication_results_b_5() {
3667 let ar = Header::with_name_value(
3668 "Authentication-Results",
3669 "example.com;\n\tdkim=pass (good signature) header.d=example.com",
3670 );
3671 k9::snapshot!(
3672 ar.as_authentication_results(),
3673 r#"
3674Ok(
3675 AuthenticationResults {
3676 serv_id: "example.com",
3677 version: None,
3678 results: [
3679 AuthenticationResult {
3680 method: "dkim",
3681 method_version: None,
3682 result: "pass",
3683 reason: None,
3684 props: {
3685 "header.d": "example.com",
3686 },
3687 },
3688 ],
3689 },
3690)
3691"#
3692 );
3693
3694 let ar = Header::with_name_value(
3695 "Authentication-Results",
3696 "example.com;\n\tauth=pass (cram-md5) smtp.auth=sender@example.com;\n\tspf=fail smtp.mailfrom=example.com"
3697 );
3698 let ar = ar.as_authentication_results().unwrap();
3699 k9::snapshot!(
3700 &ar,
3701 r#"
3702AuthenticationResults {
3703 serv_id: "example.com",
3704 version: None,
3705 results: [
3706 AuthenticationResult {
3707 method: "auth",
3708 method_version: None,
3709 result: "pass",
3710 reason: None,
3711 props: {
3712 "smtp.auth": "sender@example.com",
3713 },
3714 },
3715 AuthenticationResult {
3716 method: "spf",
3717 method_version: None,
3718 result: "fail",
3719 reason: None,
3720 props: {
3721 "smtp.mailfrom": "example.com",
3722 },
3723 },
3724 ],
3725}
3726"#
3727 );
3728
3729 k9::snapshot!(
3730 ar.encode_value(),
3731 r#"
3732example.com;\r
3733\tauth=pass\r
3734\tsmtp.auth=sender@example.com;\r
3735\tspf=fail\r
3736\tsmtp.mailfrom=example.com
3737"#
3738 );
3739 }
3740
3741 #[test]
3743 fn authentication_results_b_6() {
3744 let ar = Header::with_name_value(
3745 "Authentication-Results",
3746 concat!(
3747 "example.com;\n",
3748 "\tdkim=pass reason=\"good signature\"\n",
3749 "\theader.i=@mail-router.example.net;\n",
3750 "\tdkim=fail reason=\"bad signature\"\n",
3751 "\theader.i=@newyork.example.com"
3752 ),
3753 );
3754 let ar = match ar.as_authentication_results() {
3755 Err(err) => panic!("\n{err}"),
3756 Ok(ar) => ar,
3757 };
3758
3759 k9::snapshot!(
3760 &ar,
3761 r#"
3762AuthenticationResults {
3763 serv_id: "example.com",
3764 version: None,
3765 results: [
3766 AuthenticationResult {
3767 method: "dkim",
3768 method_version: None,
3769 result: "pass",
3770 reason: Some(
3771 "good signature",
3772 ),
3773 props: {
3774 "header.i": "@mail-router.example.net",
3775 },
3776 },
3777 AuthenticationResult {
3778 method: "dkim",
3779 method_version: None,
3780 result: "fail",
3781 reason: Some(
3782 "bad signature",
3783 ),
3784 props: {
3785 "header.i": "@newyork.example.com",
3786 },
3787 },
3788 ],
3789}
3790"#
3791 );
3792
3793 k9::snapshot!(
3794 ar.encode_value(),
3795 r#"
3796example.com;\r
3797\tdkim=pass reason="good signature"\r
3798\theader.i=@mail-router.example.net;\r
3799\tdkim=fail reason="bad signature"\r
3800\theader.i=@newyork.example.com
3801"#
3802 );
3803
3804 let ar = Header::with_name_value(
3805 "Authentication-Results",
3806 concat!(
3807 "example.net;\n",
3808 "\tdkim=pass (good signature) header.i=@newyork.example.com"
3809 ),
3810 );
3811 let ar = match ar.as_authentication_results() {
3812 Err(err) => panic!("\n{err}"),
3813 Ok(ar) => ar,
3814 };
3815
3816 k9::snapshot!(
3817 &ar,
3818 r#"
3819AuthenticationResults {
3820 serv_id: "example.net",
3821 version: None,
3822 results: [
3823 AuthenticationResult {
3824 method: "dkim",
3825 method_version: None,
3826 result: "pass",
3827 reason: None,
3828 props: {
3829 "header.i": "@newyork.example.com",
3830 },
3831 },
3832 ],
3833}
3834"#
3835 );
3836
3837 k9::snapshot!(
3838 ar.encode_value(),
3839 r#"
3840example.net;\r
3841\tdkim=pass\r
3842\theader.i=@newyork.example.com
3843"#
3844 );
3845 }
3846
3847 #[test]
3849 fn authentication_results_b_7() {
3850 let ar = Header::with_name_value(
3851 "Authentication-Results",
3852 concat!(
3853 "foo.example.net (foobar) 1 (baz);\n",
3854 "\tdkim (Because I like it) / 1 (One yay) = (wait for it) fail\n",
3855 "\tpolicy (A dot can go here) . (like that) expired\n",
3856 "\t(this surprised me) = (as I wasn't expecting it) 1362471462"
3857 ),
3858 );
3859 let ar = match ar.as_authentication_results() {
3860 Err(err) => panic!("\n{err}"),
3861 Ok(ar) => ar,
3862 };
3863
3864 k9::snapshot!(
3865 &ar,
3866 r#"
3867AuthenticationResults {
3868 serv_id: "foo.example.net",
3869 version: Some(
3870 1,
3871 ),
3872 results: [
3873 AuthenticationResult {
3874 method: "dkim",
3875 method_version: Some(
3876 1,
3877 ),
3878 result: "fail",
3879 reason: None,
3880 props: {
3881 "policy.expired": "1362471462",
3882 },
3883 },
3884 ],
3885}
3886"#
3887 );
3888
3889 k9::snapshot!(
3890 ar.encode_value(),
3891 r#"
3892foo.example.net 1;\r
3893\tdkim/1=fail\r
3894\tpolicy.expired=1362471462
3895"#
3896 );
3897 }
3898
3899 #[test]
3900 fn arc_authentication_results_1() {
3901 let ar = Header::with_name_value(
3902 "ARC-Authentication-Results",
3903 "i=3; clochette.example.org; spf=fail
3904 smtp.from=jqd@d1.example; dkim=fail (512-bit key)
3905 header.i=@d1.example; dmarc=fail; arc=pass (as.2.gmail.example=pass,
3906 ams.2.gmail.example=pass, as.1.lists.example.org=pass,
3907 ams.1.lists.example.org=fail (message has been altered))",
3908 );
3909 let ar = match ar.as_arc_authentication_results() {
3910 Err(err) => panic!("\n{err}"),
3911 Ok(ar) => ar,
3912 };
3913
3914 k9::snapshot!(
3915 &ar,
3916 r#"
3917ARCAuthenticationResults {
3918 instance: 3,
3919 serv_id: "clochette.example.org",
3920 version: None,
3921 results: [
3922 AuthenticationResult {
3923 method: "spf",
3924 method_version: None,
3925 result: "fail",
3926 reason: None,
3927 props: {
3928 "smtp.from": "jqd@d1.example",
3929 },
3930 },
3931 AuthenticationResult {
3932 method: "dkim",
3933 method_version: None,
3934 result: "fail",
3935 reason: None,
3936 props: {
3937 "header.i": "@d1.example",
3938 },
3939 },
3940 AuthenticationResult {
3941 method: "dmarc",
3942 method_version: None,
3943 result: "fail",
3944 reason: None,
3945 props: {},
3946 },
3947 AuthenticationResult {
3948 method: "arc",
3949 method_version: None,
3950 result: "pass",
3951 reason: None,
3952 props: {},
3953 },
3954 ],
3955}
3956"#
3957 );
3958 }
3959
3960 #[test]
3961 fn bstring_utf8_serializes_utf8_as_string() {
3962 let mid = MessageID(BString::from("abc123@example.com"));
3964 let json = serde_json::to_string(&mid).unwrap();
3965 k9::assert_equal!(json, r#""abc123@example.com""#);
3966 }
3967
3968 #[test]
3969 fn bstring_utf8_serializes_non_utf8_as_array() {
3970 let mid = MessageID(BString::from(&b"hello\x80world"[..]));
3972 let json = serde_json::to_string(&mid).unwrap();
3973 k9::assert_equal!(json, "[104,101,108,108,111,128,119,111,114,108,100]");
3974 }
3975
3976 #[test]
3977 fn bstring_utf8_round_trip_utf8() {
3978 let mid = MessageID(BString::from("test@example.com"));
3979 let json = serde_json::to_string(&mid).unwrap();
3980 let restored: MessageID = serde_json::from_str(&json).unwrap();
3981 k9::assert_equal!(restored, mid);
3982 }
3983
3984 #[test]
3985 fn bstring_utf8_round_trip_non_utf8() {
3986 let mid = MessageID(BString::from(&b"\xff\xfe"[..]));
3987 let json = serde_json::to_string(&mid).unwrap();
3988 let restored: MessageID = serde_json::from_str(&json).unwrap();
3989 k9::assert_equal!(restored, mid);
3990 }
3991
3992 #[test]
3993 fn authentication_results_serialize_as_strings() {
3994 let ar = AuthenticationResults {
3995 serv_id: BString::from("example.com"),
3996 version: None,
3997 results: vec![AuthenticationResult {
3998 method: "dkim".into(),
3999 method_version: None,
4000 result: "pass".into(),
4001 reason: Some(BString::from("good signature")),
4002 props: BTreeMap::from([
4003 ("header.d".into(), BString::from("example.com")),
4004 ("header.s".into(), BString::from("selector1")),
4005 ]),
4006 }],
4007 };
4008 let json = serde_json::to_string_pretty(&ar).unwrap();
4009 k9::assert_equal!(
4011 json,
4012 r#"{
4013 "serv_id": "example.com",
4014 "version": null,
4015 "results": [
4016 {
4017 "method": "dkim",
4018 "method_version": null,
4019 "result": "pass",
4020 "reason": "good signature",
4021 "props": {
4022 "header.d": "example.com",
4023 "header.s": "selector1"
4024 }
4025 }
4026 ]
4027}"#
4028 );
4029 }
4030
4031 #[test]
4032 fn authentication_results_round_trip() {
4033 let ar = AuthenticationResults {
4034 serv_id: BString::from("mx.example.org"),
4035 version: Some(1),
4036 results: vec![AuthenticationResult {
4037 method: "spf".into(),
4038 method_version: None,
4039 result: "pass".into(),
4040 reason: None,
4041 props: BTreeMap::from([(
4042 "smtp.mailfrom".into(),
4043 BString::from("sender@example.com"),
4044 )]),
4045 }],
4046 };
4047 let json = serde_json::to_string(&ar).unwrap();
4048 let restored: AuthenticationResults = serde_json::from_str(&json).unwrap();
4049 k9::assert_equal!(restored, ar);
4050 }
4051
4052 #[test]
4053 fn authentication_result_non_utf8_reason() {
4054 let ar = AuthenticationResult {
4055 method: "dkim".into(),
4056 method_version: None,
4057 result: "temperror".into(),
4058 reason: Some(BString::from(&b"bad\x80data"[..])),
4059 props: BTreeMap::new(),
4060 };
4061 let json = serde_json::to_string(&ar).unwrap();
4062 assert!(json.contains(r#""reason":[98,97,100,128,100,97,116,97]"#));
4064 let restored: AuthenticationResult = serde_json::from_str(&json).unwrap();
4065 k9::assert_equal!(restored, ar);
4066 }
4067
4068 #[test]
4069 fn authentication_results_encode_value_with_binary() {
4070 let ar = AuthenticationResults {
4073 serv_id: BString::from(&b"mx.ex\x80mple.com"[..]),
4074 version: None,
4075 results: vec![AuthenticationResult {
4076 method: "spf".into(),
4077 method_version: None,
4078 result: "pass".into(),
4079 reason: Some(BString::from(&b"good\xffsig"[..])),
4080 props: BTreeMap::from([(
4081 "smtp.mailfrom".into(),
4082 BString::from(&b"user@\xfehost"[..]),
4083 )]),
4084 }],
4085 };
4086 let encoded = ar.encode_value();
4087 k9::snapshot!(
4088 encoded,
4089 r#"
4090"mx.ex\x80mple.com";\r
4091\tspf=pass reason="good\xffsig"\r
4092\tsmtp.mailfrom="user@\xfehost"
4093"#
4094 );
4095 }
4096
4097 #[test]
4098 fn authentication_results_serv_id_quoting() {
4099 let ar = AuthenticationResults {
4101 serv_id: BString::from("mx example.com"),
4102 version: None,
4103 results: vec![],
4104 };
4105 let encoded = ar.encode_value();
4106 k9::snapshot!(encoded, r#""mx example.com"; none"#);
4107
4108 let ar2 = AuthenticationResults {
4110 serv_id: BString::from("mx.example.com"),
4111 version: Some(1),
4112 results: vec![],
4113 };
4114 let encoded2 = ar2.encode_value();
4115 k9::snapshot!(&encoded2, "mx.example.com 1; none");
4116 let parsed = Parser::parse_authentication_results_header(encoded2.as_bytes()).unwrap();
4118 k9::assert_equal!(parsed.serv_id, ar2.serv_id);
4119 k9::assert_equal!(parsed.version, Some(1));
4120 }
4121
4122 #[test]
4123 fn arc_authentication_results_serialize_as_strings() {
4124 let arc = ARCAuthenticationResults {
4125 instance: 1,
4126 serv_id: BString::from("mx.example.com"),
4127 version: None,
4128 results: vec![],
4129 };
4130 let json = serde_json::to_string(&arc).unwrap();
4131 k9::assert_equal!(
4132 json,
4133 r#"{"instance":1,"serv_id":"mx.example.com","version":null,"results":[]}"#
4134 );
4135 }
4136}