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