1use crate::headermap::{EncodeHeaderValue, HeaderMap};
2use crate::rfc5322_parser::Parser;
3use crate::strings::IntoSharedString;
4use crate::{
5 AddressList, AuthenticationResults, MailParsingError, Mailbox, MailboxList, MessageID,
6 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 crate::textwrap::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_date(&self) -> Result<DateTime<FixedOffset>> {
227 DateTime::parse_from_rfc2822(self.get_raw_value()).map_err(MailParsingError::ChronoError)
228 }
229
230 pub fn parse_headers<S>(header_block: S) -> Result<HeaderParseResult<'a>>
231 where
232 S: IntoSharedString<'a>,
233 {
234 let (header_block, mut overall_conformance) = header_block.into_shared_string();
235 let mut headers = vec![];
236 let mut idx = 0;
237
238 while idx < header_block.len() {
239 let b = header_block[idx];
240 if b == b'\n' {
241 idx += 1;
243 overall_conformance.set(MessageConformance::NON_CANONICAL_LINE_ENDINGS, true);
244 break;
245 }
246 if b == b'\r' {
247 if idx + 1 < header_block.len() && header_block[idx + 1] == b'\n' {
248 idx += 2;
250 break;
251 }
252 return Err(MailParsingError::HeaderParse(
253 "lone CR in header".to_string(),
254 ));
255 }
256 if headers.is_empty() && b.is_ascii_whitespace() {
257 return Err(MailParsingError::HeaderParse(
258 "header block must not start with spaces".to_string(),
259 ));
260 }
261 let (header, next) = Self::parse(header_block.slice(idx..header_block.len()))?;
262 overall_conformance |= header.conformance;
263 headers.push(header);
264 debug_assert!(
265 idx != next + idx,
266 "idx={idx}, next={next}, headers: {headers:#?}"
267 );
268 idx += next;
269 }
270 Ok(HeaderParseResult {
271 headers: HeaderMap::new(headers),
272 body_offset: idx,
273 overall_conformance,
274 })
275 }
276
277 pub fn parse<S: Into<SharedString<'a>>>(header_block: S) -> Result<(Self, usize)> {
278 let header_block = header_block.into();
279
280 enum State {
281 Initial,
282 Name,
283 Separator,
284 Value,
285 NewLine,
286 }
287
288 let mut state = State::Initial;
289
290 let mut iter = header_block.as_bytes().iter();
291 let mut c = *iter
292 .next()
293 .ok_or_else(|| MailParsingError::HeaderParse("empty header string".to_string()))?;
294
295 let mut name_end = None;
296 let mut value_start = 0;
297 let mut value_end = 0;
298
299 let mut idx = 0usize;
300 let mut conformance = MessageConformance::default();
301 let mut saw_cr = false;
302 let mut line_start = 0;
303 let mut max_line_len = 0;
304
305 loop {
306 match state {
307 State::Initial => {
308 if c.is_ascii_whitespace() {
309 return Err(MailParsingError::HeaderParse(
310 "header cannot start with space".to_string(),
311 ));
312 }
313 state = State::Name;
314 continue;
315 }
316 State::Name => {
317 if c == b':' {
318 if name_end.is_none() {
319 name_end.replace(idx);
320 }
321 state = State::Separator;
322 } else if c == b' ' || c == b'\t' {
323 if name_end.is_none() {
324 name_end.replace(idx);
325 }
326 conformance.set(MessageConformance::NAME_ENDS_WITH_SPACE, true);
327 } else if c == b'\n' {
328 conformance.set(MessageConformance::MISSING_COLON_VALUE, true);
330 name_end.replace(idx);
331 max_line_len = max_line_len.max(idx.saturating_sub(line_start));
332 value_start = idx;
333 value_end = idx;
334 idx += 1;
335 break;
336 } else if c != b'\r' && !(33..=126).contains(&c) {
337 return Err(MailParsingError::HeaderParse(format!(
338 "header name must be comprised of printable US-ASCII characters. Found {c:?}"
339 )));
340 }
341 }
342 State::Separator => {
343 if c != b' ' {
344 value_start = idx;
345 value_end = idx;
346 state = State::Value;
347 continue;
348 }
349 }
350 State::Value => {
351 if c == b'\n' {
352 if !saw_cr {
353 conformance.set(MessageConformance::NON_CANONICAL_LINE_ENDINGS, true);
354 }
355 state = State::NewLine;
356 saw_cr = false;
357 max_line_len = max_line_len.max(idx.saturating_sub(line_start));
358 line_start = idx + 1;
359 } else if c != b'\r' {
360 value_end = idx + 1;
361 saw_cr = false;
362 } else {
363 saw_cr = true;
364 }
365 }
366 State::NewLine => {
367 if c == b' ' || c == b'\t' {
368 state = State::Value;
369 continue;
370 }
371 break;
372 }
373 }
374 idx += 1;
375 c = match iter.next() {
376 None => break,
377 Some(v) => *v,
378 };
379 }
380
381 max_line_len = max_line_len.max(idx.saturating_sub(line_start));
382 if max_line_len > 78 {
383 conformance.set(MessageConformance::LINE_TOO_LONG, true);
384 }
385
386 let name_end = name_end.unwrap_or_else(|| {
387 conformance.set(MessageConformance::MISSING_COLON_VALUE, true);
388 idx
389 });
390
391 let name = header_block.slice(0..name_end);
392 let value = header_block.slice(value_start..value_end.max(value_start));
393 let separator = header_block.slice(name_end..value_start.max(name_end));
394
395 let header = Self {
396 name,
397 value,
398 separator,
399 conformance,
400 };
401
402 Ok((header, idx))
403 }
404
405 pub fn rebuild(&self) -> Result<Self> {
413 let name = self.get_name();
414
415 macro_rules! hdr {
416 ($header_name:literal, $func_name:ident, encode) => {
417 if name.eq_ignore_ascii_case($header_name) {
418 let value = self.$func_name().map_err(|err| {
419 MailParsingError::HeaderParse(format!(
420 "rebuilding '{name}' header: {err:#}"
421 ))
422 })?;
423 return Ok(Self::with_name_value($header_name, value.encode_value()));
424 }
425 };
426 ($header_name:literal, unstructured) => {
427 if name.eq_ignore_ascii_case($header_name) {
428 let value = self.as_unstructured().map_err(|err| {
429 MailParsingError::HeaderParse(format!(
430 "rebuilding '{name}' header: {err:#}"
431 ))
432 })?;
433 return Ok(Self::new_unstructured($header_name, value));
434 }
435 };
436 }
437
438 hdr!("From", as_mailbox_list, encode);
439 hdr!("Resent-From", as_mailbox_list, encode);
440 hdr!("Reply-To", as_address_list, encode);
441 hdr!("To", as_address_list, encode);
442 hdr!("Cc", as_address_list, encode);
443 hdr!("Bcc", as_address_list, encode);
444 hdr!("Resent-To", as_address_list, encode);
445 hdr!("Resent-Cc", as_address_list, encode);
446 hdr!("Resent-Bcc", as_address_list, encode);
447 hdr!("Date", as_date, encode);
448 hdr!("Sender", as_mailbox, encode);
449 hdr!("Resent-Sender", as_mailbox, encode);
450 hdr!("Message-ID", as_message_id, encode);
451 hdr!("Content-ID", as_content_id, encode);
452 hdr!("Content-Type", as_content_type, encode);
453 hdr!(
454 "Content-Transfer-Encoding",
455 as_content_transfer_encoding,
456 encode
457 );
458 hdr!("Content-Disposition", as_content_disposition, encode);
459 hdr!("References", as_message_id_list, encode);
460 hdr!("Subject", unstructured);
461 hdr!("Comments", unstructured);
462 hdr!("Mime-Version", unstructured);
463
464 let value = self.as_unstructured().map_err(|err| {
466 MailParsingError::HeaderParse(format!("rebuilding '{name}' header: {err:#}"))
467 })?;
468 Ok(Self::new_unstructured(name.to_string(), value))
469 }
470}
471
472#[cfg(test)]
473mod test {
474 use super::*;
475 use crate::AddrSpec;
476
477 fn assert_static_lifetime(_header: Header<'static>) {
478 assert!(true, "I wouldn't compile if this wasn't true");
479 }
480
481 #[test]
482 fn header_construction() {
483 let header = Header::with_name_value("To", "someone@example.com");
484 assert_eq!(header.get_name(), "To");
485 assert_eq!(header.get_raw_value(), "someone@example.com");
486 assert_eq!(header.to_header_string(), "To: someone@example.com\r\n");
487 assert_static_lifetime(header);
488 }
489
490 #[test]
491 fn header_parsing() {
492 let message = concat!(
493 "Subject: hello there\n",
494 "From: Someone <someone@example.com>\n",
495 "\n",
496 "I am the body"
497 );
498
499 let HeaderParseResult {
500 headers,
501 body_offset,
502 overall_conformance,
503 } = Header::parse_headers(message).unwrap();
504 assert_eq!(&message[body_offset..], "I am the body");
505 k9::snapshot!(
506 overall_conformance,
507 "
508MessageConformance(
509 NON_CANONICAL_LINE_ENDINGS,
510)
511"
512 );
513 k9::snapshot!(
514 headers,
515 r#"
516HeaderMap {
517 headers: [
518 Header {
519 name: "Subject",
520 value: "hello there",
521 separator: ": ",
522 conformance: MessageConformance(
523 NON_CANONICAL_LINE_ENDINGS,
524 ),
525 },
526 Header {
527 name: "From",
528 value: "Someone <someone@example.com>",
529 separator: ": ",
530 conformance: MessageConformance(
531 NON_CANONICAL_LINE_ENDINGS,
532 ),
533 },
534 ],
535}
536"#
537 );
538 }
539
540 #[test]
541 fn as_mailbox() {
542 let sender = Header::with_name_value("Sender", "John Smith <jsmith@example.com>");
543 k9::snapshot!(
544 sender.as_mailbox(),
545 r#"
546Ok(
547 Mailbox {
548 name: Some(
549 "John Smith",
550 ),
551 address: AddrSpec {
552 local_part: "jsmith",
553 domain: "example.com",
554 },
555 },
556)
557"#
558 );
559 }
560
561 #[test]
562 fn assign_mailbox() {
563 let mut sender = Header::with_name_value("Sender", "");
564 sender.assign(Mailbox {
565 name: Some("John Smith".to_string()),
566 address: AddrSpec::new("john.smith", "example.com"),
567 });
568 assert_eq!(
569 sender.to_header_string(),
570 "Sender: John Smith <john.smith@example.com>\r\n"
571 );
572
573 sender.assign(Mailbox {
574 name: Some("John \"the smith\" Smith".to_string()),
575 address: AddrSpec::new("john.smith", "example.com"),
576 });
577 assert_eq!(
578 sender.to_header_string(),
579 "Sender: \"John \\\"the smith\\\" Smith\" <john.smith@example.com>\r\n"
580 );
581 }
582
583 #[test]
584 fn new_mailbox() {
585 let sender = Header::new(
586 "Sender",
587 Mailbox {
588 name: Some("John".to_string()),
589 address: AddrSpec::new("john.smith", "example.com"),
590 },
591 );
592 assert_eq!(
593 sender.to_header_string(),
594 "Sender: John <john.smith@example.com>\r\n"
595 );
596
597 let sender = Header::new(
598 "Sender",
599 Mailbox {
600 name: Some("John".to_string()),
601 address: AddrSpec::new("john smith", "example.com"),
602 },
603 );
604 assert_eq!(
605 sender.to_header_string(),
606 "Sender: John <\"john smith\"@example.com>\r\n"
607 );
608 }
609
610 #[test]
611 fn new_mailbox_2047() {
612 let sender = Header::new(
613 "Sender",
614 Mailbox {
615 name: Some("André Pirard".to_string()),
616 address: AddrSpec::new("andre", "example.com"),
617 },
618 );
619 assert_eq!(
620 sender.to_header_string(),
621 "Sender: =?UTF-8?q?Andr=C3=A9_Pirard?= <andre@example.com>\r\n"
622 );
623 }
624
625 #[test]
626 fn test_spacing_roundtrip() {
627 let (header, _size) = Header::parse(
628 "Subject: =?UTF-8?q?=D8=AA=D8=B3=D8=AA_=DB=8C=DA=A9_=D8=AF=D9=88_=D8=B3=D9=87?=",
629 )
630 .unwrap();
631 k9::snapshot!(
632 header.as_unstructured(),
633 r#"
634Ok(
635 "تست یک دو سه",
636)
637"#
638 );
639
640 let rebuilt = header.rebuild().unwrap();
641 k9::snapshot!(
642 rebuilt.as_unstructured(),
643 r#"
644Ok(
645 "تست یک دو سه",
646)
647"#
648 );
649 }
650
651 #[test]
652 fn test_unstructured_encode() {
653 let header = Header::new_unstructured("Subject", "hello there");
654 k9::snapshot!(header.value, "hello there");
655
656 let header = Header::new_unstructured("Subject", "hello \"there\"");
657 k9::snapshot!(header.value, "hello \"there\"");
658
659 let header = Header::new_unstructured("Subject", "hello André Pirard");
660 k9::snapshot!(header.value, "=?UTF-8?q?hello_Andr=C3=A9_Pirard?=");
661
662 let header = Header::new_unstructured(
663 "Subject",
664 "hello there, this is a \
665 longer header than the standard width and so it should \
666 get wrapped in the produced value",
667 );
668 k9::snapshot!(
669 header.to_header_string(),
670 r#"
671Subject: hello there, this is a longer header than the standard width and so it\r
672\tshould get wrapped in the produced value\r
673
674"#
675 );
676
677 let input_text = "hello there André, this is a longer header \
678 than the standard width and so it should get \
679 wrapped in the produced value. Do you hear me \
680 André? this should get really long!";
681 let header = Header::new_unstructured("Subject", input_text);
682 k9::snapshot!(
683 header.to_header_string(),
684 r#"
685Subject: =?UTF-8?q?hello_there_Andr=C3=A9,_this_is_a_longer_header_than_the_sta?=\r
686\t=?UTF-8?q?ndard_width_and_so_it_should_get_wrapped_in_the_produced_val?=\r
687\t=?UTF-8?q?ue._Do_you_hear_me_Andr=C3=A9=3F_this_should_get_really_long?=\r
688\t=?UTF-8?q?!?=\r
689
690"#
691 );
692
693 k9::assert_equal!(header.as_unstructured().unwrap(), input_text);
694 }
695
696 #[test]
697 fn test_unstructured_encode_farsi() {
698 let farsi_input = "بوتكمپ قدرت نوشتن رهنماکالج";
699 let header = Header::new_unstructured("Subject", farsi_input);
700 eprintln!("{}", header.value);
701 k9::assert_equal!(header.as_unstructured().unwrap(), farsi_input);
702 }
703
704 #[test]
705 fn test_wrapping_in_from_header() {
706 let header = Header::new_unstructured(
707 "From",
708 "=?UTF-8?q?=D8=B1=D9=87=D9=86=D9=85=D8=A7_=DA=A9=D8=A7=D9=84=D8=AC?= \
709 <from-dash-wrap-me@example.com>",
710 );
711
712 eprintln!("made: {}", header.to_header_string());
713
714 let _ = header.as_mailbox_list().unwrap();
718 }
719
720 #[test]
721 fn test_multi_line_filename() {
722 let header = Header::with_name_value(
723 "Content-Disposition",
724 "attachment;\r\n\
725 \tfilename*0*=UTF-8''%D0%A7%D0%B0%D1%81%D1%82%D0%B8%D0%BD%D0%B0%20%D0%B2;\r\n\
726 \tfilename*1*=%D0%BA%D0%BB%D0%B0%D0%B4%D0%B5%D0%BD%D0%BE%D0%B3%D0%BE%20;\r\n\
727 \tfilename*2*=%D0%BF%D0%BE%D0%B2%D1%96%D0%B4%D0%BE%D0%BC%D0%BB%D0%B5%D0%BD;\r\n\
728 \tfilename*3*=%D0%BD%D1%8F",
729 );
730
731 match header.as_content_disposition() {
732 Ok(cd) => {
733 k9::snapshot!(
734 cd.get("filename"),
735 r#"
736Some(
737 "Частина вкладеного повідомлення",
738)
739"#
740 );
741 }
742 Err(err) => {
743 eprintln!("{err:#}");
744 panic!("expected to parse");
745 }
746 }
747 }
748
749 #[test]
750 fn test_date() {
751 let header = Header::with_name_value("Date", "Tue, 1 Jul 2003 10:52:37 +0200");
752 let date = header.as_date().unwrap();
753 k9::snapshot!(date, "2003-07-01T10:52:37+02:00");
754 }
755
756 #[test]
757 fn conformance_string() {
758 k9::assert_equal!(
759 MessageConformance::LINE_TOO_LONG.to_string(),
760 "LINE_TOO_LONG"
761 );
762 k9::assert_equal!(
763 (MessageConformance::LINE_TOO_LONG | MessageConformance::NEEDS_TRANSFER_ENCODING)
764 .to_string(),
765 "LINE_TOO_LONG|NEEDS_TRANSFER_ENCODING"
766 );
767 k9::assert_equal!(
768 MessageConformance::from_str("").unwrap(),
769 MessageConformance::default()
770 );
771
772 k9::assert_equal!(
773 MessageConformance::from_str("LINE_TOO_LONG").unwrap(),
774 MessageConformance::LINE_TOO_LONG
775 );
776 k9::assert_equal!(
777 MessageConformance::from_str("LINE_TOO_LONG|MISSING_COLON_VALUE").unwrap(),
778 MessageConformance::LINE_TOO_LONG | MessageConformance::MISSING_COLON_VALUE
779 );
780 k9::assert_equal!(
781 MessageConformance::from_str("LINE_TOO_LONG|spoon").unwrap_err(),
782 "invalid MessageConformance flag 'spoon', possible values are \
783 'INVALID_MIME_HEADERS', \
784 'LINE_TOO_LONG', 'MISSING_COLON_VALUE', 'MISSING_DATE_HEADER', \
785 'MISSING_MESSAGE_ID_HEADER', 'MISSING_MIME_VERSION', 'NAME_ENDS_WITH_SPACE', \
786 'NEEDS_TRANSFER_ENCODING', 'NON_CANONICAL_LINE_ENDINGS'"
787 );
788 }
789
790 #[test]
791 fn split_qp_display_name() {
792 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>");
793
794 let mbox = header.as_mailbox_list().unwrap();
795
796 k9::snapshot!(
797 mbox,
798 r#"
799MailboxList(
800 [
801 Mailbox {
802 name: Some(
803 "انتشارات جم ال",
804 ),
805 address: AddrSpec {
806 local_part: "noreply",
807 domain: "email.ahasend.com",
808 },
809 },
810 ],
811)
812"#
813 );
814 }
815}