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