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