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