1use crate::headermap::{EncodeHeaderValue, HeaderMap};
2use crate::rfc5322_parser::Parser;
3use crate::strings::IntoSharedString;
4use crate::{
5 canonical_header_name, ARCAuthenticationResults, AddressList, AuthenticationResults,
6 MailParsingError, Mailbox, MailboxList, MessageID, MimeParameters, ParsedHeader, Result,
7 SharedString,
8};
9use bstr::{BStr, BString};
10use chrono::{DateTime, FixedOffset};
11use std::borrow::Cow;
12use std::str::FromStr;
13
14const MAX_HEADER_COUNT: usize = 1000;
22
23bitflags::bitflags! {
24 #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
25 pub struct MessageConformance: u16 {
26 const MISSING_COLON_VALUE = 0b0000_0001;
27 const NON_CANONICAL_LINE_ENDINGS = 0b0000_0010;
28 const NAME_ENDS_WITH_SPACE = 0b0000_0100;
29 const LINE_TOO_LONG = 0b0000_1000;
30 const NEEDS_TRANSFER_ENCODING = 0b0001_0000;
31 const MISSING_DATE_HEADER = 0b0010_0000;
32 const MISSING_MESSAGE_ID_HEADER = 0b0100_0000;
33 const MISSING_MIME_VERSION = 0b1000_0000;
34 const INVALID_MIME_HEADERS = 0b0001_0000_0000;
35 const MIME_NESTING_LIMIT_EXCEEDED = 0b0010_0000_0000;
36 const MIME_INVALID_BOUNDARY = 0b0100_0000_0000;
37 }
38}
39
40impl FromStr for MessageConformance {
41 type Err = String;
42
43 fn from_str(s: &str) -> std::result::Result<Self, String> {
44 let mut result = Self::default();
45 for ele in s.split('|') {
46 if ele.is_empty() {
47 continue;
48 }
49 match Self::from_name(ele) {
50 Some(v) => {
51 result = result.union(v);
52 }
53 None => {
54 let mut possible: Vec<String> = Self::all()
55 .iter_names()
56 .map(|(name, _)| format!("'{name}'"))
57 .collect();
58 possible.sort();
59 let possible = possible.join(", ");
60 return Err(format!(
61 "invalid MessageConformance flag '{ele}', possible values are {possible}"
62 ));
63 }
64 }
65 }
66 Ok(result)
67 }
68}
69
70impl ToString for MessageConformance {
71 fn to_string(&self) -> String {
72 let mut names: Vec<&str> = self.iter_names().map(|(name, _)| name).collect();
73 names.sort();
74 names.join("|")
75 }
76}
77
78#[derive(Clone, Debug, PartialEq)]
79pub struct Header<'a> {
80 name: SharedString<'a>,
82 value: SharedString<'a>,
84 separator: SharedString<'a>,
86 conformance: MessageConformance,
87}
88
89#[derive(Debug)]
91pub struct HeaderParseResult<'a> {
92 pub headers: HeaderMap<'a>,
93 pub body_offset: usize,
94 pub overall_conformance: MessageConformance,
95}
96
97impl<'a> Header<'a> {
98 pub fn with_name_value<N: Into<SharedString<'a>>, V: Into<SharedString<'a>>>(
99 name: N,
100 value: V,
101 ) -> Self {
102 let name = name.into();
103 let value = value.into();
104 Self {
105 name,
106 value,
107 separator: ": ".into(),
108 conformance: MessageConformance::default(),
109 }
110 }
111
112 pub fn to_owned(&'a self) -> Header<'static> {
113 Header {
114 name: self.name.to_owned(),
115 value: self.value.to_owned(),
116 separator: self.separator.to_owned(),
117 conformance: self.conformance,
118 }
119 }
120
121 pub fn new<N: Into<SharedString<'a>>>(name: N, value: impl EncodeHeaderValue) -> Self {
122 let name = name.into();
123 let value = value.encode_value();
124 Self {
125 name,
126 value,
127 separator: ": ".into(),
128 conformance: MessageConformance::default(),
129 }
130 }
131
132 pub fn new_unstructured<N: Into<SharedString<'a>>, V: Into<SharedString<'a>>>(
133 name: N,
134 value: V,
135 ) -> Self {
136 let name = name.into();
137 let value = value.into();
138 let value = crate::parsed_header::encode_unstructured_value(value.as_bytes());
139
140 Self {
141 name,
142 value,
143 separator: ": ".into(),
144 conformance: MessageConformance::default(),
145 }
146 }
147
148 pub fn assign(&mut self, v: impl EncodeHeaderValue) {
149 self.value = v.encode_value();
150 }
151
152 pub fn write_header<W: std::io::Write>(&self, out: &mut W) -> std::io::Result<()> {
155 let line_ending = if self
156 .conformance
157 .contains(MessageConformance::NON_CANONICAL_LINE_ENDINGS)
158 {
159 "\n"
160 } else {
161 "\r\n"
162 };
163 out.write_all(self.name.as_bytes())?;
164 out.write_all(self.separator.as_bytes())?;
165 out.write_all(self.value.as_bytes())?;
166 out.write_all(line_ending.as_bytes())
167 }
168
169 pub fn to_header_string(&self) -> String {
172 let mut out = vec![];
173 self.write_header(&mut out).unwrap();
174 String::from_utf8_lossy(&out).to_string()
175 }
176
177 pub fn get_name(&self) -> &BStr {
178 BStr::new(self.name.as_bytes())
179 }
180
181 pub fn get_name_lossy(&self) -> Cow<'_, str> {
182 String::from_utf8_lossy(self.name.as_bytes())
183 }
184
185 pub fn get_raw_value(&self) -> &BStr {
186 BStr::new(self.value.as_bytes())
187 }
188
189 pub fn get_raw_value_string(&self) -> Result<&str> {
190 self.value.to_str().map_err(|_| MailParsingError::EightBit)
191 }
192
193 pub fn as_content_transfer_encoding(&self) -> Result<MimeParameters> {
194 Parser::parse_content_transfer_encoding_header(self.get_raw_value())
195 }
196
197 pub fn as_content_disposition(&self) -> Result<MimeParameters> {
198 Parser::parse_content_transfer_encoding_header(self.get_raw_value())
199 }
200
201 pub fn as_content_type(&self) -> Result<MimeParameters> {
202 Parser::parse_content_type_header(self.get_raw_value())
203 }
204
205 pub fn as_mailbox_list(&self) -> Result<MailboxList> {
209 Parser::parse_mailbox_list_header(self.get_raw_value())
210 }
211
212 pub fn as_mailbox(&self) -> Result<Mailbox> {
216 Parser::parse_mailbox_header(self.get_raw_value())
217 }
218
219 pub fn as_address_list(&self) -> Result<AddressList> {
220 Parser::parse_address_list_header(self.get_raw_value())
221 }
222
223 pub fn as_message_id(&self) -> Result<MessageID> {
224 Parser::parse_msg_id_header(self.get_raw_value())
225 }
226
227 pub fn as_content_id(&self) -> Result<MessageID> {
228 Parser::parse_content_id_header(self.get_raw_value())
229 }
230
231 pub fn as_message_id_list(&self) -> Result<Vec<MessageID>> {
232 Parser::parse_msg_id_header_list(self.get_raw_value())
233 }
234
235 pub fn as_unstructured(&self) -> Result<BString> {
236 Parser::parse_unstructured_header(self.get_raw_value())
237 }
238
239 pub fn as_authentication_results(&self) -> Result<AuthenticationResults> {
240 Parser::parse_authentication_results_header(self.get_raw_value())
241 }
242
243 pub fn as_arc_authentication_results(&self) -> Result<ARCAuthenticationResults> {
244 Parser::parse_arc_authentication_results_header(self.get_raw_value())
245 }
246
247 pub fn as_date(&self) -> Result<DateTime<FixedOffset>> {
248 crate::parse_rfc2822_date(self.get_raw_value_string()?)
249 .map_err(MailParsingError::ChronoError)
250 }
251
252 pub fn structured(&self) -> Result<ParsedHeader> {
254 ParsedHeader::structured(self.name.as_bytes(), self.value.as_bytes())
255 }
256
257 pub fn parse_headers<S>(header_block: S) -> Result<HeaderParseResult<'a>>
258 where
259 S: IntoSharedString<'a>,
260 {
261 let (header_block, mut overall_conformance) = header_block.into_shared_string();
262 let mut headers = vec![];
263 let mut idx = 0;
264
265 while idx < header_block.len() {
266 let b = header_block[idx];
267 if b == b'\n' {
268 idx += 1;
270 overall_conformance.set(MessageConformance::NON_CANONICAL_LINE_ENDINGS, true);
271 break;
272 }
273 if b == b'\r' {
274 if idx + 1 < header_block.len() && header_block[idx + 1] == b'\n' {
275 idx += 2;
277 break;
278 }
279 return Err(MailParsingError::HeaderParse(
280 "lone CR in header".to_string(),
281 ));
282 }
283 if headers.is_empty() && b.is_ascii_whitespace() {
284 return Err(MailParsingError::HeaderParse(
285 "header block must not start with spaces".to_string(),
286 ));
287 }
288 if headers.len() >= MAX_HEADER_COUNT {
289 return Err(MailParsingError::HeaderParse(format!(
290 "header block has more than {MAX_HEADER_COUNT} headers"
291 )));
292 }
293 let (header, next) = Self::parse(header_block.slice(idx..header_block.len()))?;
294 overall_conformance |= header.conformance;
295 headers.push(header);
296 debug_assert!(
297 idx != next + idx,
298 "idx={idx}, next={next}, headers: {headers:#?}"
299 );
300 idx += next;
301 }
302 Ok(HeaderParseResult {
303 headers: HeaderMap::new(headers),
304 body_offset: idx,
305 overall_conformance,
306 })
307 }
308
309 pub fn parse<S: Into<SharedString<'a>>>(header_block: S) -> Result<(Self, usize)> {
310 let header_block = header_block.into();
311
312 enum State {
313 Initial,
314 Name,
315 Separator,
316 Value,
317 NewLine,
318 }
319
320 let mut state = State::Initial;
321
322 let mut iter = header_block.as_bytes().iter();
323 let mut c = *iter
324 .next()
325 .ok_or_else(|| MailParsingError::HeaderParse("empty header string".to_string()))?;
326
327 let mut name_end = None;
328 let mut value_start = 0;
329 let mut value_end = 0;
330
331 let mut idx = 0usize;
332 let mut conformance = MessageConformance::default();
333 let mut saw_cr = false;
334 let mut line_start = 0;
335 let mut max_line_len = 0;
336
337 loop {
338 match state {
339 State::Initial => {
340 if c.is_ascii_whitespace() {
341 return Err(MailParsingError::HeaderParse(
342 "header cannot start with space".to_string(),
343 ));
344 }
345 state = State::Name;
346 continue;
347 }
348 State::Name => {
349 if c == b':' {
350 if name_end.is_none() {
351 name_end.replace(idx);
352 }
353 state = State::Separator;
354 } else if c == b' ' || c == b'\t' {
355 if name_end.is_none() {
356 name_end.replace(idx);
357 }
358 conformance.set(MessageConformance::NAME_ENDS_WITH_SPACE, true);
359 } else if c == b'\n' {
360 conformance.set(MessageConformance::MISSING_COLON_VALUE, true);
362 name_end.replace(idx);
363 max_line_len = max_line_len.max(idx.saturating_sub(line_start));
364 value_start = idx;
365 value_end = idx;
366 idx += 1;
367 break;
368 } else if c != b'\r' && !(33..=126).contains(&c) {
369 return Err(MailParsingError::HeaderParse(format!(
370 "header name must be comprised of printable US-ASCII characters. Found {c:?}"
371 )));
372 }
373 }
374 State::Separator => {
375 if c != b' ' {
376 value_start = idx;
377 value_end = idx;
378 state = State::Value;
379 continue;
380 }
381 }
382 State::Value => {
383 if c == b'\n' {
384 if !saw_cr {
385 conformance.set(MessageConformance::NON_CANONICAL_LINE_ENDINGS, true);
386 }
387 state = State::NewLine;
388 saw_cr = false;
389 max_line_len = max_line_len.max(idx.saturating_sub(line_start));
390 line_start = idx + 1;
391 } else if c != b'\r' {
392 value_end = idx + 1;
393 saw_cr = false;
394 } else {
395 saw_cr = true;
396 }
397 }
398 State::NewLine => {
399 if c == b' ' || c == b'\t' {
400 state = State::Value;
401 continue;
402 }
403 break;
404 }
405 }
406 idx += 1;
407 c = match iter.next() {
408 None => break,
409 Some(v) => *v,
410 };
411 }
412
413 max_line_len = max_line_len.max(idx.saturating_sub(line_start));
414 if max_line_len > 78 {
415 conformance.set(MessageConformance::LINE_TOO_LONG, true);
416 }
417
418 let name_end = name_end.unwrap_or_else(|| {
419 conformance.set(MessageConformance::MISSING_COLON_VALUE, true);
420 idx
421 });
422
423 let name = header_block.slice(0..name_end);
424 let value = header_block.slice(value_start..value_end.max(value_start));
425 let separator = header_block.slice(name_end..value_start.max(name_end));
426
427 let header = Self {
428 name,
429 value,
430 separator,
431 conformance,
432 };
433
434 Ok((header, idx))
435 }
436
437 pub fn rebuild(&self) -> Result<Self> {
445 let name = self.get_name();
446 let value = self.structured().map_err(|err| {
447 MailParsingError::HeaderParse(format!("rebuilding '{name}' header: {err:#}"))
448 })?;
449 match canonical_header_name(name) {
450 Some(canonical) => Ok(Self::with_name_value(canonical, value.encode_value())),
451 None => Ok(Self::with_name_value(
452 name.to_string(),
453 value.encode_value(),
454 )),
455 }
456 }
457}
458
459#[cfg(test)]
460mod test {
461 use super::*;
462 use crate::AddrSpec;
463
464 fn assert_static_lifetime(_header: Header<'static>) {
465 assert!(true, "I wouldn't compile if this wasn't true");
466 }
467
468 #[test]
469 fn header_count_cap() {
470 let at_cap = "X: y\r\n".repeat(MAX_HEADER_COUNT) + "\r\n";
472 let parsed = Header::parse_headers(at_cap.as_str()).unwrap();
473 k9::assert_equal!(parsed.headers.iter().count(), MAX_HEADER_COUNT);
474
475 let over_cap = "X: y\r\n".repeat(MAX_HEADER_COUNT + 1) + "\r\n";
476 let err = Header::parse_headers(over_cap.as_str()).unwrap_err();
477 k9::assert_equal!(
478 err.to_string(),
479 "invalid header: header block has more than 1000 headers"
480 );
481 }
482
483 #[test]
484 fn header_construction() {
485 let header = Header::with_name_value("To", "someone@example.com");
486 assert_eq!(header.get_name(), "To");
487 assert_eq!(header.get_raw_value(), "someone@example.com");
488 assert_eq!(header.to_header_string(), "To: someone@example.com\r\n");
489 assert_static_lifetime(header);
490 }
491
492 #[test]
493 fn header_parsing() {
494 let message = concat!(
495 "Subject: hello there\n",
496 "From: Someone <someone@example.com>\n",
497 "\n",
498 "I am the body"
499 );
500
501 let HeaderParseResult {
502 headers,
503 body_offset,
504 overall_conformance,
505 } = Header::parse_headers(message).unwrap();
506 assert_eq!(&message[body_offset..], "I am the body");
507 k9::snapshot!(
508 overall_conformance,
509 "
510MessageConformance(
511 NON_CANONICAL_LINE_ENDINGS,
512)
513"
514 );
515 k9::snapshot!(
516 headers,
517 r#"
518HeaderMap {
519 headers: [
520 Header {
521 name: "Subject",
522 value: "hello there",
523 separator: ": ",
524 conformance: MessageConformance(
525 NON_CANONICAL_LINE_ENDINGS,
526 ),
527 },
528 Header {
529 name: "From",
530 value: "Someone <someone@example.com>",
531 separator: ": ",
532 conformance: MessageConformance(
533 NON_CANONICAL_LINE_ENDINGS,
534 ),
535 },
536 ],
537}
538"#
539 );
540 }
541
542 #[test]
543 fn as_mailbox() {
544 let sender = Header::with_name_value("Sender", "John Smith <jsmith@example.com>");
545 k9::snapshot!(
546 sender.as_mailbox(),
547 r#"
548Ok(
549 Mailbox {
550 name: Some(
551 "John Smith",
552 ),
553 address: AddrSpec {
554 local_part: "jsmith",
555 domain: "example.com",
556 },
557 },
558)
559"#
560 );
561 }
562
563 #[test]
564 fn assign_mailbox() {
565 let mut sender = Header::with_name_value("Sender", "");
566 sender.assign(Mailbox {
567 name: Some("John Smith".into()),
568 address: AddrSpec::new("john.smith", "example.com"),
569 });
570 assert_eq!(
571 sender.to_header_string(),
572 "Sender: \"John Smith\" <john.smith@example.com>\r\n"
573 );
574
575 sender.assign(Mailbox {
576 name: Some("John \"the smith\" Smith".into()),
577 address: AddrSpec::new("john.smith", "example.com"),
578 });
579 assert_eq!(
580 sender.to_header_string(),
581 "Sender: \"John \\\"the smith\\\" Smith\" <john.smith@example.com>\r\n"
582 );
583 }
584
585 #[test]
586 fn new_mailbox() {
587 let sender = Header::new(
588 "Sender",
589 Mailbox {
590 name: Some("John".into()),
591 address: AddrSpec::new("john.smith", "example.com"),
592 },
593 );
594 assert_eq!(
595 sender.to_header_string(),
596 "Sender: John <john.smith@example.com>\r\n"
597 );
598
599 let sender = Header::new(
600 "Sender",
601 Mailbox {
602 name: Some("John".into()),
603 address: AddrSpec::new("john smith", "example.com"),
604 },
605 );
606 assert_eq!(
607 sender.to_header_string(),
608 "Sender: John <\"john smith\"@example.com>\r\n"
609 );
610 }
611
612 #[test]
613 fn new_mailbox_2047() {
614 let sender = Header::new(
615 "Sender",
616 Mailbox {
617 name: Some("André Pirard".into()),
618 address: AddrSpec::new("andre", "example.com"),
619 },
620 );
621 assert_eq!(
622 sender.to_header_string(),
623 "Sender: =?UTF-8?q?Andr=C3=A9_Pirard?= <andre@example.com>\r\n"
624 );
625 }
626
627 #[test]
628 fn test_spacing_roundtrip() {
629 let (header, _size) = Header::parse(
630 "Subject: =?UTF-8?q?=D8=AA=D8=B3=D8=AA_=DB=8C=DA=A9_=D8=AF=D9=88_=D8=B3=D9=87?=",
631 )
632 .unwrap();
633 k9::snapshot!(
634 header.as_unstructured(),
635 r#"
636Ok(
637 "تست یک دو سه",
638)
639"#
640 );
641
642 let rebuilt = header.rebuild().unwrap();
643 k9::snapshot!(
644 rebuilt.as_unstructured(),
645 r#"
646Ok(
647 "تست یک دو سه",
648)
649"#
650 );
651 }
652
653 #[test]
654 fn test_rebuild_authentication_results() {
655 let header = Header::with_name_value(
659 "authentication-results",
660 "example.com;\n\tdkim=pass (good signature) header.d=example.com",
661 );
662 let rebuilt = header.rebuild().unwrap();
663 k9::assert_equal!(rebuilt.get_name(), "Authentication-Results");
664 rebuilt.as_authentication_results().unwrap();
665 assert!(
666 !rebuilt.get_raw_value().contains(&b'('),
667 "comment should be dropped by structured re-encode: {:?}",
668 rebuilt.get_raw_value()
669 );
670 }
671
672 #[test]
673 fn test_unstructured_encode() {
674 let header = Header::new_unstructured("Subject", "hello there");
675 k9::snapshot!(header.value, "hello there");
676
677 let header = Header::new_unstructured("Subject", "hello \"there\"");
678 k9::snapshot!(header.value, "hello \"there\"");
679
680 let header = Header::new_unstructured("Subject", "hello André Pirard");
681 k9::snapshot!(header.value, "=?UTF-8?q?hello_Andr=C3=A9_Pirard?=");
682
683 let header = Header::new_unstructured(
684 "Subject",
685 "hello there, this is a \
686 longer header than the standard width and so it should \
687 get wrapped in the produced value",
688 );
689 k9::snapshot!(
690 header.to_header_string(),
691 r#"
692Subject: hello there, this is a longer header than the standard width and so it\r
693\tshould get wrapped in the produced value\r
694
695"#
696 );
697
698 let input_text = "hello there André, this is a longer header \
699 than the standard width and so it should get \
700 wrapped in the produced value. Do you hear me \
701 André? this should get really long!";
702 let header = Header::new_unstructured("Subject", input_text);
703 k9::snapshot!(
704 header.to_header_string(),
705 r#"
706Subject: =?UTF-8?q?hello_there_Andr=C3=A9=2C_this_is_a_longer_header_than_the_s?=\r
707\t=?UTF-8?q?tandard_width_and_so_it_should_get_wrapped_in_the_produced_v?=\r
708\t=?UTF-8?q?alue=2E_Do_you_hear_me_Andr=C3=A9=3F_this_should_get_really_?=\r
709\t=?UTF-8?q?long!?=\r
710
711"#
712 );
713
714 k9::assert_equal!(header.as_unstructured().unwrap(), input_text);
715 }
716
717 #[test]
718 fn test_unstructured_encode_farsi() {
719 let farsi_input = "بوتكمپ قدرت نوشتن رهنماکالج";
720 let header = Header::new_unstructured("Subject", farsi_input);
721 eprintln!("{}", header.value);
722 k9::assert_equal!(header.as_unstructured().unwrap(), farsi_input);
723 }
724
725 #[test]
726 fn test_wrapping_in_from_header() {
727 let header = Header::new_unstructured(
728 "From",
729 "=?UTF-8?q?=D8=B1=D9=87=D9=86=D9=85=D8=A7_=DA=A9=D8=A7=D9=84=D8=AC?= \
730 <from-dash-wrap-me@example.com>",
731 );
732
733 eprintln!("made: {}", header.to_header_string());
734
735 let _ = header.as_mailbox_list().unwrap();
739 }
740
741 #[test]
742 fn test_multi_line_filename() {
743 let header = Header::with_name_value(
744 "Content-Disposition",
745 "attachment;\r\n\
746 \tfilename*0*=UTF-8''%D0%A7%D0%B0%D1%81%D1%82%D0%B8%D0%BD%D0%B0%20%D0%B2;\r\n\
747 \tfilename*1*=%D0%BA%D0%BB%D0%B0%D0%B4%D0%B5%D0%BD%D0%BE%D0%B3%D0%BE%20;\r\n\
748 \tfilename*2*=%D0%BF%D0%BE%D0%B2%D1%96%D0%B4%D0%BE%D0%BC%D0%BB%D0%B5%D0%BD;\r\n\
749 \tfilename*3*=%D0%BD%D1%8F",
750 );
751
752 match header.as_content_disposition() {
753 Ok(cd) => {
754 k9::snapshot!(
755 cd.get("filename"),
756 r#"
757Some(
758 "Частина вкладеного повідомлення",
759)
760"#
761 );
762 }
763 Err(err) => {
764 eprintln!("{err:#}");
765 panic!("expected to parse");
766 }
767 }
768 }
769
770 #[test]
771 fn test_date() {
772 let header = Header::with_name_value("Date", "Tue, 1 Jul 2003 10:52:37 +0200");
773 let date = header.as_date().unwrap();
774 k9::snapshot!(date, "2003-07-01T10:52:37+02:00");
775 }
776
777 #[test]
778 fn conformance_string() {
779 k9::assert_equal!(
780 MessageConformance::LINE_TOO_LONG.to_string(),
781 "LINE_TOO_LONG"
782 );
783 k9::assert_equal!(
784 (MessageConformance::LINE_TOO_LONG | MessageConformance::NEEDS_TRANSFER_ENCODING)
785 .to_string(),
786 "LINE_TOO_LONG|NEEDS_TRANSFER_ENCODING"
787 );
788 k9::assert_equal!(
789 MessageConformance::from_str("").unwrap(),
790 MessageConformance::default()
791 );
792
793 k9::assert_equal!(
794 MessageConformance::from_str("LINE_TOO_LONG").unwrap(),
795 MessageConformance::LINE_TOO_LONG
796 );
797 k9::assert_equal!(
798 MessageConformance::from_str("LINE_TOO_LONG|MISSING_COLON_VALUE").unwrap(),
799 MessageConformance::LINE_TOO_LONG | MessageConformance::MISSING_COLON_VALUE
800 );
801 k9::assert_equal!(
802 MessageConformance::from_str("LINE_TOO_LONG|spoon").unwrap_err(),
803 "invalid MessageConformance flag 'spoon', possible values are \
804 'INVALID_MIME_HEADERS', 'LINE_TOO_LONG', 'MIME_INVALID_BOUNDARY', \
805 'MIME_NESTING_LIMIT_EXCEEDED', 'MISSING_COLON_VALUE', 'MISSING_DATE_HEADER', \
806 'MISSING_MESSAGE_ID_HEADER', \
807 'MISSING_MIME_VERSION', 'NAME_ENDS_WITH_SPACE', 'NEEDS_TRANSFER_ENCODING', \
808 'NON_CANONICAL_LINE_ENDINGS'"
809 );
810 }
811
812 #[test]
813 fn split_qp_display_name() {
814 let header = Header::with_name_value("From", "=?UTF-8?q?=D8=A7=D9=86=D8=AA=D8=B4=D8=A7=D8=B1=D8=A7=D8=AA_=D8=AC=D9=85?=\r\n\t=?UTF-8?q?=D8=A7=D9=84?= <noreply@email.ahasend.com>");
815
816 let mbox = header.as_mailbox_list().unwrap();
817
818 k9::snapshot!(
819 mbox,
820 r#"
821MailboxList(
822 [
823 Mailbox {
824 name: Some(
825 "انتشارات جم ال",
826 ),
827 address: AddrSpec {
828 local_part: "noreply",
829 domain: "email.ahasend.com",
830 },
831 },
832 ],
833)
834"#
835 );
836 }
837}