1#![forbid(unsafe_code)]
2
3extern crate charset;
4extern crate data_encoding;
5extern crate quoted_printable;
6
7use std::borrow::Cow;
8use std::collections::{BTreeMap, HashMap};
9use std::error;
10use std::fmt;
11
12use charset::{decode_latin1, Charset};
13
14mod addrparse;
15pub mod body;
16mod dateparse;
17mod header;
18pub mod headers;
19mod msgidparse;
20
21pub use crate::addrparse::{
22 addrparse, addrparse_header, GroupInfo, MailAddr, MailAddrList, SingleInfo,
23};
24use crate::body::Body;
25pub use crate::dateparse::dateparse;
26use crate::header::HeaderToken;
27use crate::headers::Headers;
28pub use crate::msgidparse::{msgidparse, MessageIdList};
29
30#[derive(Debug)]
33pub enum MailParseError {
34 QuotedPrintableDecodeError(quoted_printable::QuotedPrintableError),
37 Base64DecodeError(data_encoding::DecodeError),
40 EncodingError(std::borrow::Cow<'static, str>),
43 Generic(&'static str),
46}
47
48impl fmt::Display for MailParseError {
49 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50 match *self {
51 MailParseError::QuotedPrintableDecodeError(ref err) => {
52 write!(f, "QuotedPrintable decode error: {}", err)
53 }
54 MailParseError::Base64DecodeError(ref err) => write!(f, "Base64 decode error: {}", err),
55 MailParseError::EncodingError(ref err) => write!(f, "Encoding error: {}", err),
56 MailParseError::Generic(ref description) => write!(f, "{}", description),
57 }
58 }
59}
60
61impl error::Error for MailParseError {
62 fn cause(&self) -> Option<&dyn error::Error> {
63 match *self {
64 MailParseError::QuotedPrintableDecodeError(ref err) => Some(err),
65 MailParseError::Base64DecodeError(ref err) => Some(err),
66 _ => None,
67 }
68 }
69
70 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
71 match *self {
72 MailParseError::QuotedPrintableDecodeError(ref err) => Some(err),
73 MailParseError::Base64DecodeError(ref err) => Some(err),
74 _ => None,
75 }
76 }
77}
78
79impl From<quoted_printable::QuotedPrintableError> for MailParseError {
80 fn from(err: quoted_printable::QuotedPrintableError) -> MailParseError {
81 MailParseError::QuotedPrintableDecodeError(err)
82 }
83}
84
85impl From<data_encoding::DecodeError> for MailParseError {
86 fn from(err: data_encoding::DecodeError) -> MailParseError {
87 MailParseError::Base64DecodeError(err)
88 }
89}
90
91impl From<std::borrow::Cow<'static, str>> for MailParseError {
92 fn from(err: std::borrow::Cow<'static, str>) -> MailParseError {
93 MailParseError::EncodingError(err)
94 }
95}
96
97pub struct MailHeader<'a> {
103 key: &'a [u8],
104 value: &'a [u8],
105}
106
107impl<'a> fmt::Debug for MailHeader<'a> {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 f.debug_struct("MailHeader")
111 .field("key", &String::from_utf8_lossy(self.key))
112 .field("value", &String::from_utf8_lossy(self.value))
113 .finish()
114 }
115}
116
117pub(crate) fn find_from(line: &str, ix_start: usize, key: &str) -> Option<usize> {
118 line[ix_start..].find(key).map(|v| ix_start + v)
119}
120
121fn find_from_u8(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
122 assert!(!key.is_empty());
123 assert!(ix_start <= line.len());
124 if line.len() < key.len() {
125 return None;
126 }
127 let ix_end = line.len() - key.len();
128 if ix_start <= ix_end {
129 for i in ix_start..=ix_end {
130 if line[i] == key[0] {
131 let mut success = true;
132 for j in 1..key.len() {
133 if line[i + j] != key[j] {
134 success = false;
135 break;
136 }
137 }
138 if success {
139 return Some(i);
140 }
141 }
142 }
143 }
144 None
145}
146
147#[test]
148fn test_find_from_u8() {
149 assert_eq!(find_from_u8(b"hello world", 0, b"hell"), Some(0));
150 assert_eq!(find_from_u8(b"hello world", 0, b"o"), Some(4));
151 assert_eq!(find_from_u8(b"hello world", 4, b"o"), Some(4));
152 assert_eq!(find_from_u8(b"hello world", 5, b"o"), Some(7));
153 assert_eq!(find_from_u8(b"hello world", 8, b"o"), None);
154 assert_eq!(find_from_u8(b"hello world", 10, b"d"), Some(10));
155 assert_eq!(find_from_u8(b"hello world", 0, b"world"), Some(6));
156}
157
158fn find_from_u8_line_prefix(line: &[u8], ix_start: usize, key: &[u8]) -> Option<usize> {
161 let mut start = ix_start;
162 while let Some(ix) = find_from_u8(line, start, key) {
163 if ix == ix_start || line[ix - 1] == b'\n' {
164 return Some(ix);
165 }
166 start = ix + 1;
167 }
168 None
169}
170
171#[test]
172fn test_find_from_u8_line_prefix() {
173 assert_eq!(find_from_u8_line_prefix(b"hello world", 0, b"he"), Some(0));
174 assert_eq!(find_from_u8_line_prefix(b"hello\nhello", 0, b"he"), Some(0));
175 assert_eq!(find_from_u8_line_prefix(b"hello\nhello", 1, b"he"), Some(6));
176 assert_eq!(find_from_u8_line_prefix(b"hello world", 0, b"wo"), None);
177 assert_eq!(find_from_u8_line_prefix(b"hello\nworld", 0, b"wo"), Some(6));
178 assert_eq!(find_from_u8_line_prefix(b"hello\nworld", 6, b"wo"), Some(6));
179 assert_eq!(find_from_u8_line_prefix(b"hello\nworld", 7, b"wo"), None);
180 assert_eq!(
181 find_from_u8_line_prefix(b"hello\nworld", 0, b"world"),
182 Some(6)
183 );
184}
185
186impl<'a> MailHeader<'a> {
187 pub fn get_key(&self) -> String {
190 decode_latin1(self.key).into_owned()
191 }
192
193 pub fn get_key_ref(&self) -> Cow<str> {
196 decode_latin1(self.key)
197 }
198
199 pub(crate) fn decode_utf8_or_latin1(&'a self) -> Cow<'a, str> {
200 match std::str::from_utf8(self.value) {
204 Ok(s) => Cow::Borrowed(s),
205 Err(_) => decode_latin1(self.value),
206 }
207 }
208
209 pub fn get_value(&self) -> String {
229 let chars = self.decode_utf8_or_latin1();
230 self.normalize_header(chars)
231 }
232
233 fn normalize_header(&'a self, chars: Cow<'a, str>) -> String {
234 let mut result = String::new();
235
236 for tok in header::normalized_tokens(&chars) {
237 match tok {
238 HeaderToken::Text(t) => {
239 result.push_str(t);
240 }
241 HeaderToken::Whitespace(ws) => {
242 result.push_str(ws);
243 }
244 HeaderToken::Newline(Some(ws)) => {
245 result.push_str(&ws);
246 }
247 HeaderToken::Newline(None) => {}
248 HeaderToken::DecodedWord(dw) => {
249 result.push_str(&dw);
250 }
251 }
252 }
253
254 result
255 }
256
257 pub fn get_value_utf8(&self) -> Result<String, MailParseError> {
274 let chars = std::str::from_utf8(self.value).map_err(|_| {
275 MailParseError::EncodingError(Cow::Borrowed("Invalid UTF-8 in header value"))
276 })?;
277 Ok(self.normalize_header(Cow::Borrowed(chars)))
278 }
279
280 pub fn get_key_raw(&self) -> &[u8] {
289 self.key
290 }
291
292 pub fn get_value_raw(&self) -> &[u8] {
302 self.value
303 }
304}
305
306#[derive(Debug)]
307enum HeaderParseState {
308 Initial,
309 Key,
310 PreValue,
311 Value,
312 ValueNewline,
313}
314
315pub fn parse_header(raw_data: &[u8]) -> Result<(MailHeader, usize), MailParseError> {
336 let mut it = raw_data.iter();
337 let mut ix = 0;
338 let mut c = match it.next() {
339 None => return Err(MailParseError::Generic("Empty string provided")),
340 Some(v) => *v,
341 };
342
343 let mut ix_key_end = None;
344 let mut ix_value_start = 0;
345 let mut ix_value_end = 0;
346
347 let mut state = HeaderParseState::Initial;
348 loop {
349 match state {
350 HeaderParseState::Initial => {
351 if c == b' ' {
352 return Err(MailParseError::Generic(
353 "Header cannot start with a space; it is \
354 likely an overhanging line from a \
355 previous header",
356 ));
357 };
358 state = HeaderParseState::Key;
359 continue;
360 }
361 HeaderParseState::Key => {
362 if c == b':' {
363 ix_key_end = Some(ix);
364 state = HeaderParseState::PreValue;
365 } else if c == b'\n' {
366 ix_key_end = Some(ix);
372 ix_value_start = ix;
373 ix_value_end = ix;
374 ix += 1;
375 break;
376 }
377 }
378 HeaderParseState::PreValue => {
379 if c != b' ' {
380 ix_value_start = ix;
381 ix_value_end = ix;
382 state = HeaderParseState::Value;
383 continue;
384 }
385 }
386 HeaderParseState::Value => {
387 if c == b'\n' {
388 state = HeaderParseState::ValueNewline;
389 } else if c != b'\r' {
390 ix_value_end = ix + 1;
391 }
392 }
393 HeaderParseState::ValueNewline => {
394 if c == b' ' || c == b'\t' {
395 state = HeaderParseState::Value;
396 continue;
397 } else {
398 break;
399 }
400 }
401 }
402 ix += 1;
403 c = match it.next() {
404 None => break,
405 Some(v) => *v,
406 };
407 }
408 match ix_key_end {
409 Some(v) => Ok((
410 MailHeader {
411 key: &raw_data[0..v],
412 value: &raw_data[ix_value_start..ix_value_end],
413 },
414 ix,
415 )),
416
417 None => Ok((
418 MailHeader {
423 key: &raw_data[0..ix],
424 value: &raw_data[ix..ix],
425 },
426 ix,
427 )),
428 }
429}
430
431pub trait MailHeaderMap {
436 fn get_first_value(&self, key: &str) -> Option<String>;
451
452 fn get_first_header(&self, key: &str) -> Option<&MailHeader>;
455
456 fn get_all_values(&self, key: &str) -> Vec<String>;
473
474 fn get_all_headers(&self, key: &str) -> Vec<&MailHeader>;
477}
478
479impl<'a> MailHeaderMap for [MailHeader<'a>] {
480 fn get_first_value(&self, key: &str) -> Option<String> {
481 for x in self {
482 if x.get_key_ref().eq_ignore_ascii_case(key) {
483 return Some(x.get_value());
484 }
485 }
486 None
487 }
488
489 fn get_first_header(&self, key: &str) -> Option<&MailHeader> {
490 self.iter()
491 .find(|&x| x.get_key_ref().eq_ignore_ascii_case(key))
492 }
493
494 fn get_all_values(&self, key: &str) -> Vec<String> {
495 let mut values: Vec<String> = Vec::new();
496 for x in self {
497 if x.get_key_ref().eq_ignore_ascii_case(key) {
498 values.push(x.get_value());
499 }
500 }
501 values
502 }
503
504 fn get_all_headers(&self, key: &str) -> Vec<&MailHeader> {
505 let mut headers: Vec<&MailHeader> = Vec::new();
506 for x in self {
507 if x.get_key_ref().eq_ignore_ascii_case(key) {
508 headers.push(x);
509 }
510 }
511 headers
512 }
513}
514
515pub fn parse_headers(raw_data: &[u8]) -> Result<(Vec<MailHeader>, usize), MailParseError> {
539 let mut headers: Vec<MailHeader> = Vec::new();
540 let mut ix = 0;
541 loop {
542 if ix >= raw_data.len() {
543 break;
544 } else if raw_data[ix] == b'\n' {
545 ix += 1;
546 break;
547 } else if raw_data[ix] == b'\r' {
548 if ix + 1 < raw_data.len() && raw_data[ix + 1] == b'\n' {
549 ix += 2;
550 break;
551 } else {
552 return Err(MailParseError::Generic(
553 "Headers were followed by an unexpected lone \
554 CR character!",
555 ));
556 }
557 }
558 let (header, ix_next) = parse_header(&raw_data[ix..])?;
559 headers.push(header);
560 ix += ix_next;
561 }
562 Ok((headers, ix))
563}
564
565#[derive(Debug)]
569pub struct ParsedContentType {
570 pub mimetype: String,
572 pub charset: String,
575 pub params: BTreeMap<String, String>,
579}
580
581impl Default for ParsedContentType {
582 fn default() -> Self {
583 ParsedContentType {
584 mimetype: "text/plain".to_string(),
585 charset: "us-ascii".to_string(),
586 params: BTreeMap::new(),
587 }
588 }
589}
590
591impl ParsedContentType {
592 fn default_conditional(in_multipart_digest: bool) -> Self {
593 let mut default = Self::default();
594 if in_multipart_digest {
595 default.mimetype = "message/rfc822".to_string();
596 }
597 default
598 }
599}
600
601pub fn parse_content_type(header: &str) -> ParsedContentType {
638 let params = parse_param_content(header);
639 let mimetype = params.value.to_lowercase();
640 let charset = params
641 .params
642 .get("charset")
643 .cloned()
644 .unwrap_or_else(|| "us-ascii".to_string());
645
646 ParsedContentType {
647 mimetype,
648 charset,
649 params: params.params,
650 }
651}
652
653#[derive(Debug, Clone, PartialEq)]
659pub enum DispositionType {
660 Inline,
663 Attachment,
666 FormData,
668 Extension(String),
670}
671
672impl Default for DispositionType {
673 fn default() -> Self {
674 DispositionType::Inline
675 }
676}
677
678fn parse_disposition_type(disposition: &str) -> DispositionType {
680 match &disposition.to_lowercase()[..] {
681 "inline" => DispositionType::Inline,
682 "attachment" => DispositionType::Attachment,
683 "form-data" => DispositionType::FormData,
684 extension => DispositionType::Extension(extension.to_string()),
685 }
686}
687
688#[derive(Debug, Default)]
692pub struct ParsedContentDisposition {
693 pub disposition: DispositionType,
696 pub params: BTreeMap<String, String>,
700}
701
702pub fn parse_content_disposition(header: &str) -> ParsedContentDisposition {
718 let params = parse_param_content(header);
719 let disposition = parse_disposition_type(¶ms.value);
720 ParsedContentDisposition {
721 disposition,
722 params: params.params,
723 }
724}
725
726#[derive(Debug)]
731pub struct ParsedMail<'a> {
732 pub raw_bytes: &'a [u8],
734 header_bytes: &'a [u8],
736 pub headers: Vec<MailHeader<'a>>,
738 pub ctype: ParsedContentType,
740 body_bytes: &'a [u8],
742 pub subparts: Vec<ParsedMail<'a>>,
745}
746
747impl<'a> ParsedMail<'a> {
748 pub fn get_body(&self) -> Result<String, MailParseError> {
770 match self.get_body_encoded() {
771 Body::Base64(body) | Body::QuotedPrintable(body) => body.get_decoded_as_string(),
772 Body::SevenBit(body) | Body::EightBit(body) => body.get_as_string(),
773 Body::Binary(body) => body.get_as_string(),
774 }
775 }
776
777 pub fn get_body_raw(&self) -> Result<Vec<u8>, MailParseError> {
792 match self.get_body_encoded() {
793 Body::Base64(body) | Body::QuotedPrintable(body) => body.get_decoded(),
794 Body::SevenBit(body) | Body::EightBit(body) => Ok(Vec::<u8>::from(body.get_raw())),
795 Body::Binary(body) => Ok(Vec::<u8>::from(body.get_raw())),
796 }
797 }
798
799 pub fn get_body_encoded(&'a self) -> Body<'a> {
841 let transfer_encoding = self
842 .headers
843 .get_first_value("Content-Transfer-Encoding")
844 .map(|s| s.to_lowercase());
845
846 Body::new(self.body_bytes, &self.ctype, &transfer_encoding)
847 }
848
849 pub fn get_headers(&'a self) -> Headers<'a> {
852 Headers::new(self.header_bytes, &self.headers)
853 }
854
855 pub fn get_content_disposition(&self) -> ParsedContentDisposition {
861 self.headers
862 .get_first_value("Content-Disposition")
863 .map(|s| parse_content_disposition(&s))
864 .unwrap_or_default()
865 }
866
867 pub fn parts(&'a self) -> PartsIterator<'a> {
871 PartsIterator {
872 parts: vec![self],
873 index: 0,
874 }
875 }
876}
877
878pub struct PartsIterator<'a> {
879 parts: Vec<&'a ParsedMail<'a>>,
880 index: usize,
881}
882
883impl<'a> Iterator for PartsIterator<'a> {
884 type Item = &'a ParsedMail<'a>;
885
886 fn next(&mut self) -> Option<Self::Item> {
887 if self.index >= self.parts.len() {
888 return None;
889 }
890
891 let cur = self.parts[self.index];
892 self.index += 1;
893 self.parts
894 .splice(self.index..self.index, cur.subparts.iter());
895 Some(cur)
896 }
897}
898
899pub fn parse_mail(raw_data: &[u8]) -> Result<ParsedMail, MailParseError> {
937 parse_mail_recursive(raw_data, false)
938}
939
940fn strip_trailing_crlf(raw_data: &[u8], ix_start: usize, mut ix: usize) -> usize {
945 if ix > ix_start && raw_data[ix - 1] == b'\n' {
946 ix -= 1;
947 if ix > ix_start && raw_data[ix - 1] == b'\r' {
948 ix -= 1;
949 }
950 }
951 ix
952}
953
954fn parse_mail_recursive(
955 raw_data: &[u8],
956 in_multipart_digest: bool,
957) -> Result<ParsedMail, MailParseError> {
958 let (headers, ix_body) = parse_headers(raw_data)?;
959 let ctype = headers
960 .get_first_value("Content-Type")
961 .map(|s| parse_content_type(&s))
962 .unwrap_or_else(|| ParsedContentType::default_conditional(in_multipart_digest));
963
964 let mut result = ParsedMail {
965 raw_bytes: raw_data,
966 header_bytes: &raw_data[0..ix_body],
967 headers,
968 ctype,
969 body_bytes: &raw_data[ix_body..],
970 subparts: Vec::<ParsedMail>::new(),
971 };
972 if result.ctype.mimetype.starts_with("multipart/")
973 && result.ctype.params.get("boundary").is_some()
974 && raw_data.len() > ix_body
975 {
976 let in_multipart_digest = result.ctype.mimetype == "multipart/digest";
977 let boundary = String::from("--") + &result.ctype.params["boundary"];
978 if let Some(ix_boundary_start) =
979 find_from_u8_line_prefix(raw_data, ix_body, boundary.as_bytes())
980 {
981 let ix_body_end = strip_trailing_crlf(raw_data, ix_body, ix_boundary_start);
982 result.body_bytes = &raw_data[ix_body..ix_body_end];
983 let mut ix_boundary_end = ix_boundary_start + boundary.len();
984 while let Some(ix_part_start) =
985 find_from_u8(raw_data, ix_boundary_end, b"\n").map(|v| v + 1)
986 {
987 let ix_part_boundary_start =
988 find_from_u8_line_prefix(raw_data, ix_part_start, boundary.as_bytes());
989 let ix_part_end = ix_part_boundary_start
990 .map(|x| strip_trailing_crlf(raw_data, ix_part_start, x))
991 .unwrap_or(raw_data.len());
993
994 result.subparts.push(parse_mail_recursive(
995 &raw_data[ix_part_start..ix_part_end],
996 in_multipart_digest,
997 )?);
998 ix_boundary_end = ix_part_boundary_start
999 .map(|x| x + boundary.len())
1000 .unwrap_or(raw_data.len());
1001 if ix_boundary_end + 2 > raw_data.len()
1002 || (raw_data[ix_boundary_end] == b'-' && raw_data[ix_boundary_end + 1] == b'-')
1003 {
1004 break;
1005 }
1006 }
1007 }
1008 }
1009 Ok(result)
1010}
1011
1012struct ParamContent {
1014 value: String,
1015 params: BTreeMap<String, String>,
1016}
1017
1018fn parse_param_content(content: &str) -> ParamContent {
1030 let mut tokens = content.split(';');
1031 let value = tokens.next().unwrap().trim();
1033 let mut map: BTreeMap<String, String> = tokens
1034 .filter_map(|kv| {
1035 kv.find('=').map(|idx| {
1036 let key = kv[0..idx].trim().to_lowercase();
1037 let mut value = kv[idx + 1..].trim();
1038 if value.starts_with('"') && value.ends_with('"') && value.len() > 1 {
1039 value = &value[1..value.len() - 1];
1040 }
1041 (key, value.to_string())
1042 })
1043 })
1044 .collect();
1045
1046 let decode_key_list: Vec<String> = map
1048 .keys()
1049 .filter_map(|k| k.strip_suffix('*'))
1050 .map(String::from)
1051 .filter(|k| !map.contains_key(k))
1053 .collect();
1054 let encodings = compute_parameter_encodings(&map, &decode_key_list);
1055 for (k, (e, strip)) in encodings {
1059 if let Some(charset) = Charset::for_label_no_replacement(e.as_bytes()) {
1060 let key = format!("{}*", k);
1061 let percent_encoded_value = map.remove(&key).unwrap();
1062 let encoded_value = if strip {
1063 percent_decode(percent_encoded_value.splitn(3, '\'').nth(2).unwrap_or(""))
1064 } else {
1065 percent_decode(&percent_encoded_value)
1066 };
1067 let decoded_value = charset.decode_without_bom_handling(&encoded_value).0;
1068 map.insert(k, decoded_value.to_string());
1069 }
1070 }
1071
1072 let unwrap_key_list: Vec<String> = map
1074 .keys()
1075 .filter_map(|k| k.strip_suffix("*0"))
1076 .map(String::from)
1077 .filter(|k| !map.contains_key(k))
1079 .collect();
1080 for unwrap_key in unwrap_key_list {
1081 let mut unwrapped_value = String::new();
1082 let mut index = 0;
1083 while let Some(wrapped_value_part) = map.remove(&format!("{}*{}", &unwrap_key, index)) {
1084 index += 1;
1085 unwrapped_value.push_str(&wrapped_value_part);
1086 }
1087 let old_value = map.insert(unwrap_key, unwrapped_value);
1088 assert!(old_value.is_none());
1089 }
1090
1091 ParamContent {
1092 value: value.into(),
1093 params: map,
1094 }
1095}
1096
1097fn compute_parameter_encodings(
1103 map: &BTreeMap<String, String>,
1104 decode_key_list: &Vec<String>,
1105) -> HashMap<String, (String, bool)> {
1106 let mut encodings: HashMap<String, (String, bool)> = HashMap::new();
1111 for decode_key in decode_key_list {
1112 if let Some(unwrap_key) = decode_key.strip_suffix("*0") {
1113 let encoding = map
1116 .get(&format!("{}*", decode_key))
1117 .unwrap()
1118 .split('\'')
1119 .next()
1120 .unwrap_or("");
1121 let continuation_prefix = format!("{}*", unwrap_key);
1122 for continuation_key in decode_key_list {
1123 if continuation_key.starts_with(&continuation_prefix) {
1124 encodings.insert(
1128 continuation_key.clone(),
1129 (encoding.to_string(), continuation_key == decode_key),
1130 );
1131 }
1132 }
1133 } else if !encodings.contains_key(decode_key) {
1134 let encoding = map
1135 .get(&format!("{}*", decode_key))
1136 .unwrap()
1137 .split('\'')
1138 .next()
1139 .unwrap_or("")
1140 .to_string();
1141 let old_value = encodings.insert(decode_key.clone(), (encoding, true));
1142 assert!(old_value.is_none());
1143 }
1144 }
1147 encodings
1148}
1149
1150fn percent_decode(encoded: &str) -> Vec<u8> {
1151 let mut decoded = Vec::with_capacity(encoded.len());
1152 let mut bytes = encoded.bytes();
1153 let mut next = bytes.next();
1154 while next.is_some() {
1155 let b = next.unwrap();
1156 if b != b'%' {
1157 decoded.push(b);
1158 next = bytes.next();
1159 continue;
1160 }
1161
1162 let top = match bytes.next() {
1163 Some(n) if n.is_ascii_hexdigit() => n,
1164 n => {
1165 decoded.push(b);
1166 next = n;
1167 continue;
1168 }
1169 };
1170 let bottom = match bytes.next() {
1171 Some(n) if n.is_ascii_hexdigit() => n,
1172 n => {
1173 decoded.push(b);
1174 decoded.push(top);
1175 next = n;
1176 continue;
1177 }
1178 };
1179 let decoded_byte = (hex_to_nybble(top) << 4) | hex_to_nybble(bottom);
1180 decoded.push(decoded_byte);
1181
1182 next = bytes.next();
1183 }
1184 decoded
1185}
1186
1187fn hex_to_nybble(byte: u8) -> u8 {
1188 match byte {
1189 b'0'..=b'9' => byte - b'0',
1190 b'a'..=b'f' => byte - b'a' + 10,
1191 b'A'..=b'F' => byte - b'A' + 10,
1192 _ => panic!("Not a hex character!"),
1193 }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199
1200 #[test]
1201 fn parse_basic_header() {
1202 let (parsed, _) = parse_header(b"Key: Value").unwrap();
1203 assert_eq!(parsed.key, b"Key");
1204 assert_eq!(parsed.get_key(), "Key");
1205 assert_eq!(parsed.get_key_ref(), "Key");
1206 assert_eq!(parsed.value, b"Value");
1207 assert_eq!(parsed.get_value(), "Value");
1208 assert_eq!(parsed.get_value_raw(), "Value".as_bytes());
1209
1210 let (parsed, _) = parse_header(b"Key : Value ").unwrap();
1211 assert_eq!(parsed.key, b"Key ");
1212 assert_eq!(parsed.value, b"Value ");
1213 assert_eq!(parsed.get_value(), "Value ");
1214 assert_eq!(parsed.get_value_raw(), "Value ".as_bytes());
1215
1216 let (parsed, _) = parse_header(b"Key:").unwrap();
1217 assert_eq!(parsed.key, b"Key");
1218 assert_eq!(parsed.value, b"");
1219
1220 let (parsed, _) = parse_header(b":\n").unwrap();
1221 assert_eq!(parsed.key, b"");
1222 assert_eq!(parsed.value, b"");
1223
1224 let (parsed, _) = parse_header(b"Key:Multi-line\n value").unwrap();
1225 assert_eq!(parsed.key, b"Key");
1226 assert_eq!(parsed.value, b"Multi-line\n value");
1227 assert_eq!(parsed.get_value(), "Multi-line value");
1228 assert_eq!(parsed.get_value_raw(), "Multi-line\n value".as_bytes());
1229
1230 let (parsed, _) = parse_header(b"Key: Multi\n line\n value\n").unwrap();
1231 assert_eq!(parsed.key, b"Key");
1232 assert_eq!(parsed.value, b"Multi\n line\n value");
1233 assert_eq!(parsed.get_value(), "Multi line value");
1234 assert_eq!(parsed.get_value_raw(), "Multi\n line\n value".as_bytes());
1235
1236 let (parsed, _) = parse_header(b"Key: One\nKey2: Two").unwrap();
1237 assert_eq!(parsed.key, b"Key");
1238 assert_eq!(parsed.value, b"One");
1239
1240 let (parsed, _) = parse_header(b"Key: One\n\tOverhang").unwrap();
1241 assert_eq!(parsed.key, b"Key");
1242 assert_eq!(parsed.value, b"One\n\tOverhang");
1243 assert_eq!(parsed.get_value(), "One Overhang");
1244 assert_eq!(parsed.get_value_raw(), "One\n\tOverhang".as_bytes());
1245
1246 let (parsed, _) = parse_header(b"SPAM: VIAGRA \xAE").unwrap();
1247 assert_eq!(parsed.key, b"SPAM");
1248 assert_eq!(parsed.value, b"VIAGRA \xAE");
1249 assert_eq!(parsed.get_value(), "VIAGRA \u{ae}");
1250 assert_eq!(parsed.get_value_raw(), b"VIAGRA \xAE");
1251
1252 parse_header(b" Leading: Space").unwrap_err();
1253
1254 let (parsed, _) = parse_header(b"Just a string").unwrap();
1255 assert_eq!(parsed.key, b"Just a string");
1256 assert_eq!(parsed.value, b"");
1257 assert_eq!(parsed.get_value(), "");
1258 assert_eq!(parsed.get_value_raw(), b"");
1259
1260 let (parsed, _) = parse_header(b"Key\nBroken: Value").unwrap();
1261 assert_eq!(parsed.key, b"Key");
1262 assert_eq!(parsed.value, b"");
1263 assert_eq!(parsed.get_value(), "");
1264 assert_eq!(parsed.get_value_raw(), b"");
1265
1266 let (parsed, _) = parse_header(b"Key: With CRLF\r\n").unwrap();
1267 assert_eq!(parsed.key, b"Key");
1268 assert_eq!(parsed.value, b"With CRLF");
1269 assert_eq!(parsed.get_value(), "With CRLF");
1270 assert_eq!(parsed.get_value_raw(), b"With CRLF");
1271
1272 let (parsed, _) = parse_header(b"Key: With spurious CRs\r\r\r\n").unwrap();
1273 assert_eq!(parsed.value, b"With spurious CRs");
1274 assert_eq!(parsed.get_value(), "With spurious CRs");
1275 assert_eq!(parsed.get_value_raw(), b"With spurious CRs");
1276
1277 let (parsed, _) = parse_header(b"Key: With \r mixed CR\r\n").unwrap();
1278 assert_eq!(parsed.value, b"With \r mixed CR");
1279 assert_eq!(parsed.get_value(), "With \r mixed CR");
1280 assert_eq!(parsed.get_value_raw(), b"With \r mixed CR");
1281
1282 let (parsed, _) = parse_header(b"Key:\r\n Value after linebreak").unwrap();
1283 assert_eq!(parsed.value, b"\r\n Value after linebreak");
1284 assert_eq!(parsed.get_value(), " Value after linebreak");
1285 assert_eq!(parsed.get_value_raw(), b"\r\n Value after linebreak");
1286 }
1287
1288 #[test]
1289 fn parse_encoded_headers() {
1290 let (parsed, _) = parse_header(b"Subject: =?iso-8859-1?Q?=A1Hola,_se=F1or!?=").unwrap();
1291 assert_eq!(parsed.get_key(), "Subject");
1292 assert_eq!(parsed.get_key_ref(), "Subject");
1293 assert_eq!(parsed.get_value(), "\u{a1}Hola, se\u{f1}or!");
1294 assert_eq!(
1295 parsed.get_value_raw(),
1296 "=?iso-8859-1?Q?=A1Hola,_se=F1or!?=".as_bytes()
1297 );
1298
1299 let (parsed, _) = parse_header(
1300 b"Subject: =?iso-8859-1?Q?=A1Hola,?=\n \
1301 =?iso-8859-1?Q?_se=F1or!?=",
1302 )
1303 .unwrap();
1304 assert_eq!(parsed.get_key(), "Subject");
1305 assert_eq!(parsed.get_key_ref(), "Subject");
1306 assert_eq!(parsed.get_value(), "\u{a1}Hola, se\u{f1}or!");
1307 assert_eq!(
1308 parsed.get_value_raw(),
1309 "=?iso-8859-1?Q?=A1Hola,?=\n \
1310 =?iso-8859-1?Q?_se=F1or!?="
1311 .as_bytes()
1312 );
1313
1314 let (parsed, _) = parse_header(b"Euro: =?utf-8?Q?=E2=82=AC?=").unwrap();
1315 assert_eq!(parsed.get_key(), "Euro");
1316 assert_eq!(parsed.get_key_ref(), "Euro");
1317 assert_eq!(parsed.get_value(), "\u{20ac}");
1318 assert_eq!(parsed.get_value_raw(), "=?utf-8?Q?=E2=82=AC?=".as_bytes());
1319
1320 let (parsed, _) = parse_header(b"HelloWorld: =?utf-8?B?aGVsbG8gd29ybGQ=?=").unwrap();
1321 assert_eq!(parsed.get_value(), "hello world");
1322 assert_eq!(
1323 parsed.get_value_raw(),
1324 "=?utf-8?B?aGVsbG8gd29ybGQ=?=".as_bytes()
1325 );
1326
1327 let (parsed, _) = parse_header(b"Empty: =?utf-8?Q??=").unwrap();
1328 assert_eq!(parsed.get_value(), "");
1329 assert_eq!(parsed.get_value_raw(), "=?utf-8?Q??=".as_bytes());
1330
1331 let (parsed, _) = parse_header(b"Incomplete: =?").unwrap();
1332 assert_eq!(parsed.get_value(), "=?");
1333 assert_eq!(parsed.get_value_raw(), "=?".as_bytes());
1334
1335 let (parsed, _) = parse_header(b"BadEncoding: =?garbage?Q??=").unwrap();
1336 assert_eq!(parsed.get_value(), "=?garbage?Q??=");
1337 assert_eq!(parsed.get_value_raw(), "=?garbage?Q??=".as_bytes());
1338
1339 let (parsed, _) = parse_header(b"Invalid: =?utf-8?Q?=E2=AC?=").unwrap();
1340 assert_eq!(parsed.get_value(), "\u{fffd}");
1341
1342 let (parsed, _) = parse_header(b"LineBreak: =?utf-8?Q?=E2=82\n =AC?=").unwrap();
1343 assert_eq!(parsed.get_value(), "=?utf-8?Q?=E2=82 =AC?=");
1344
1345 let (parsed, _) = parse_header(b"NotSeparateWord: hello=?utf-8?Q?world?=").unwrap();
1346 assert_eq!(parsed.get_value(), "hello=?utf-8?Q?world?=");
1347
1348 let (parsed, _) = parse_header(b"NotSeparateWord2: =?utf-8?Q?hello?=world").unwrap();
1349 assert_eq!(parsed.get_value(), "=?utf-8?Q?hello?=world");
1350
1351 let (parsed, _) = parse_header(b"Key: \"=?utf-8?Q?value?=\"").unwrap();
1352 assert_eq!(parsed.get_value(), "\"value\"");
1353
1354 let (parsed, _) = parse_header(b"Subject: =?utf-8?q?=5BOntario_Builder=5D_Understanding_home_shopping_=E2=80=93_a_q?=\n \
1355 =?utf-8?q?uick_survey?=")
1356 .unwrap();
1357 assert_eq!(parsed.get_key(), "Subject");
1358 assert_eq!(parsed.get_key_ref(), "Subject");
1359 assert_eq!(
1360 parsed.get_value(),
1361 "[Ontario Builder] Understanding home shopping \u{2013} a quick survey"
1362 );
1363
1364 let (parsed, _) = parse_header(
1365 b"Subject: =?utf-8?q?=5BOntario_Builder=5D?= non-qp words\n \
1366 and the subject continues",
1367 )
1368 .unwrap();
1369 assert_eq!(
1370 parsed.get_value(),
1371 "[Ontario Builder] non-qp words and the subject continues"
1372 );
1373
1374 let (parsed, _) = parse_header(
1375 b"Subject: =?utf-8?q?=5BOntario_Builder=5D?= \n \
1376 and the subject continues",
1377 )
1378 .unwrap();
1379 assert_eq!(
1380 parsed.get_value(),
1381 "[Ontario Builder] and the subject continues"
1382 );
1383 assert_eq!(
1384 parsed.get_value_raw(),
1385 "=?utf-8?q?=5BOntario_Builder=5D?= \n \
1386 and the subject continues"
1387 .as_bytes()
1388 );
1389
1390 let (parsed, _) = parse_header(b"Subject: =?ISO-2022-JP?B?GyRCRnwbKEI=?=\n\t=?ISO-2022-JP?B?GyRCS1wbKEI=?=\n\t=?ISO-2022-JP?B?GyRCOGwbKEI=?=")
1391 .unwrap();
1392 assert_eq!(parsed.get_key(), "Subject");
1393 assert_eq!(parsed.get_key_ref(), "Subject");
1394 assert_eq!(parsed.get_key_raw(), "Subject".as_bytes());
1395 assert_eq!(parsed.get_value(), "\u{65E5}\u{672C}\u{8A9E}");
1396 assert_eq!(parsed.get_value_raw(), "=?ISO-2022-JP?B?GyRCRnwbKEI=?=\n\t=?ISO-2022-JP?B?GyRCS1wbKEI=?=\n\t=?ISO-2022-JP?B?GyRCOGwbKEI=?=".as_bytes());
1397
1398 let (parsed, _) = parse_header(b"Subject: =?ISO-2022-JP?Q?=1B\x24\x42\x46\x7C=1B\x28\x42?=\n\t=?ISO-2022-JP?Q?=1B\x24\x42\x4B\x5C=1B\x28\x42?=\n\t=?ISO-2022-JP?Q?=1B\x24\x42\x38\x6C=1B\x28\x42?=")
1399 .unwrap();
1400 assert_eq!(parsed.get_key(), "Subject");
1401 assert_eq!(parsed.get_key_ref(), "Subject");
1402 assert_eq!(parsed.get_key_raw(), "Subject".as_bytes());
1403 assert_eq!(parsed.get_value(), "\u{65E5}\u{672C}\u{8A9E}");
1404 assert_eq!(parsed.get_value_raw(), "=?ISO-2022-JP?Q?=1B\x24\x42\x46\x7C=1B\x28\x42?=\n\t=?ISO-2022-JP?Q?=1B\x24\x42\x4B\x5C=1B\x28\x42?=\n\t=?ISO-2022-JP?Q?=1B\x24\x42\x38\x6C=1B\x28\x42?=".as_bytes());
1405
1406 let (parsed, _) = parse_header(b"Subject: =?UTF-7?Q?+JgM-?=").unwrap();
1407 assert_eq!(parsed.get_key(), "Subject");
1408 assert_eq!(parsed.get_key_ref(), "Subject");
1409 assert_eq!(parsed.get_key_raw(), "Subject".as_bytes());
1410 assert_eq!(parsed.get_value(), "\u{2603}");
1411 assert_eq!(parsed.get_value_raw(), b"=?UTF-7?Q?+JgM-?=");
1412
1413 let (parsed, _) =
1414 parse_header(b"Content-Type: image/jpeg; name=\"=?UTF-8?B?MDY2MTM5ODEuanBn?=\"")
1415 .unwrap();
1416 assert_eq!(parsed.get_key(), "Content-Type");
1417 assert_eq!(parsed.get_key_ref(), "Content-Type");
1418 assert_eq!(parsed.get_key_raw(), "Content-Type".as_bytes());
1419 assert_eq!(parsed.get_value(), "image/jpeg; name=\"06613981.jpg\"");
1420 assert_eq!(
1421 parsed.get_value_raw(),
1422 "image/jpeg; name=\"=?UTF-8?B?MDY2MTM5ODEuanBn?=\"".as_bytes()
1423 );
1424
1425 let (parsed, _) = parse_header(
1426 b"From: =?UTF-8?Q?\"Motorola_Owners=E2=80=99_Forums\"_?=<forums@motorola.com>",
1427 )
1428 .unwrap();
1429 assert_eq!(parsed.get_key(), "From");
1430 assert_eq!(parsed.get_key_ref(), "From");
1431 assert_eq!(parsed.get_key_raw(), "From".as_bytes());
1432 assert_eq!(
1433 parsed.get_value(),
1434 "\"Motorola Owners\u{2019} Forums\" <forums@motorola.com>"
1435 );
1436 }
1437
1438 #[test]
1439 fn encoded_words_and_spaces() {
1440 let (parsed, _) = parse_header(b"K: an =?utf-8?q?encoded?=\n word").unwrap();
1441 assert_eq!(parsed.get_value(), "an encoded word");
1442 assert_eq!(
1443 parsed.get_value_raw(),
1444 "an =?utf-8?q?encoded?=\n word".as_bytes()
1445 );
1446
1447 let (parsed, _) = parse_header(b"K: =?utf-8?q?glue?= =?utf-8?q?these?= \n words").unwrap();
1448 assert_eq!(parsed.get_value(), "gluethese words");
1449 assert_eq!(
1450 parsed.get_value_raw(),
1451 "=?utf-8?q?glue?= =?utf-8?q?these?= \n words".as_bytes()
1452 );
1453
1454 let (parsed, _) = parse_header(b"K: =?utf-8?q?glue?= \n =?utf-8?q?again?=").unwrap();
1455 assert_eq!(parsed.get_value(), "glueagain");
1456 assert_eq!(
1457 parsed.get_value_raw(),
1458 "=?utf-8?q?glue?= \n =?utf-8?q?again?=".as_bytes()
1459 );
1460 }
1461
1462 #[test]
1463 fn parse_multiple_headers() {
1464 let (parsed, _) = parse_headers(b"Key: Value\nTwo: Second").unwrap();
1465 assert_eq!(parsed.len(), 2);
1466 assert_eq!(parsed[0].key, b"Key");
1467 assert_eq!(parsed[0].value, b"Value");
1468 assert_eq!(parsed[1].key, b"Two");
1469 assert_eq!(parsed[1].value, b"Second");
1470
1471 let (parsed, _) =
1472 parse_headers(b"Key: Value\n Overhang\nTwo: Second\nThree: Third").unwrap();
1473 assert_eq!(parsed.len(), 3);
1474 assert_eq!(parsed[0].key, b"Key");
1475 assert_eq!(parsed[0].value, b"Value\n Overhang");
1476 assert_eq!(parsed[1].key, b"Two");
1477 assert_eq!(parsed[1].value, b"Second");
1478 assert_eq!(parsed[2].key, b"Three");
1479 assert_eq!(parsed[2].value, b"Third");
1480
1481 let (parsed, _) = parse_headers(b"Key: Value\nTwo: Second\n\nBody").unwrap();
1482 assert_eq!(parsed.len(), 2);
1483 assert_eq!(parsed[0].key, b"Key");
1484 assert_eq!(parsed[0].value, b"Value");
1485 assert_eq!(parsed[1].key, b"Two");
1486 assert_eq!(parsed[1].value, b"Second");
1487
1488 let (parsed, _) = parse_headers(
1489 concat!(
1490 "Return-Path: <kats@foobar.staktrace.com>\n",
1491 "X-Original-To: kats@baz.staktrace.com\n",
1492 "Delivered-To: kats@baz.staktrace.com\n",
1493 "Received: from foobar.staktrace.com (localhost [127.0.0.1])\n",
1494 " by foobar.staktrace.com (Postfix) with ESMTP id \
1495 139F711C1C34\n",
1496 " for <kats@baz.staktrace.com>; Fri, 27 May 2016 02:34:26 \
1497 -0400 (EDT)\n",
1498 "Date: Fri, 27 May 2016 02:34:25 -0400\n",
1499 "To: kats@baz.staktrace.com\n",
1500 "From: kats@foobar.staktrace.com\n",
1501 "Subject: test Fri, 27 May 2016 02:34:25 -0400\n",
1502 "X-Mailer: swaks v20130209.0 jetmore.org/john/code/swaks/\n",
1503 "Message-Id: \
1504 <20160527063426.139F711C1C34@foobar.staktrace.com>\n",
1505 "\n",
1506 "This is a test mailing\n"
1507 )
1508 .as_bytes(),
1509 )
1510 .unwrap();
1511 assert_eq!(parsed.len(), 10);
1512 assert_eq!(parsed[0].key, b"Return-Path");
1513 assert_eq!(parsed[9].key, b"Message-Id");
1514
1515 let (parsed, _) =
1516 parse_headers(b"Key: Value\nAnotherKey: AnotherValue\nKey: Value2\nKey: Value3\n")
1517 .unwrap();
1518 assert_eq!(parsed.len(), 4);
1519 assert_eq!(parsed.get_first_value("Key"), Some("Value".to_string()));
1520 assert_eq!(
1521 parsed.get_all_values("Key"),
1522 vec!["Value", "Value2", "Value3"]
1523 );
1524 assert_eq!(
1525 parsed.get_first_value("AnotherKey"),
1526 Some("AnotherValue".to_string())
1527 );
1528 assert_eq!(parsed.get_all_values("AnotherKey"), vec!["AnotherValue"]);
1529 assert_eq!(parsed.get_first_value("NoKey"), None);
1530 assert_eq!(parsed.get_all_values("NoKey"), Vec::<String>::new());
1531
1532 let (parsed, _) = parse_headers(b"Key: value\r\nWith: CRLF\r\n\r\nBody").unwrap();
1533 assert_eq!(parsed.len(), 2);
1534 assert_eq!(parsed.get_first_value("Key"), Some("value".to_string()));
1535 assert_eq!(parsed.get_first_value("With"), Some("CRLF".to_string()));
1536
1537 let (parsed, _) = parse_headers(b"Bad\nKey\n").unwrap();
1538 assert_eq!(parsed.len(), 2);
1539 assert_eq!(parsed.get_first_value("Bad"), Some("".to_string()));
1540 assert_eq!(parsed.get_first_value("Key"), Some("".to_string()));
1541
1542 let (parsed, _) = parse_headers(b"K:V\nBad\nKey").unwrap();
1543 assert_eq!(parsed.len(), 3);
1544 assert_eq!(parsed.get_first_value("K"), Some("V".to_string()));
1545 assert_eq!(parsed.get_first_value("Bad"), Some("".to_string()));
1546 assert_eq!(parsed.get_first_value("Key"), Some("".to_string()));
1547 }
1548
1549 #[test]
1550 fn test_parse_content_type() {
1551 let ctype = parse_content_type("text/html; charset=utf-8");
1552 assert_eq!(ctype.mimetype, "text/html");
1553 assert_eq!(ctype.charset, "utf-8");
1554 assert_eq!(ctype.params.get("boundary"), None);
1555
1556 let ctype = parse_content_type(" foo/bar; x=y; charset=\"fake\" ; x2=y2");
1557 assert_eq!(ctype.mimetype, "foo/bar");
1558 assert_eq!(ctype.charset, "fake");
1559 assert_eq!(ctype.params.get("boundary"), None);
1560
1561 let ctype = parse_content_type(" multipart/bar; boundary=foo ");
1562 assert_eq!(ctype.mimetype, "multipart/bar");
1563 assert_eq!(ctype.charset, "us-ascii");
1564 assert_eq!(ctype.params.get("boundary").unwrap(), "foo");
1565 }
1566
1567 #[test]
1568 fn test_parse_content_disposition() {
1569 let dis = parse_content_disposition("inline");
1570 assert_eq!(dis.disposition, DispositionType::Inline);
1571 assert_eq!(dis.params.get("name"), None);
1572 assert_eq!(dis.params.get("filename"), None);
1573
1574 let dis = parse_content_disposition(
1575 " attachment; x=y; charset=\"fake\" ; x2=y2; name=\"King Joffrey.death\"",
1576 );
1577 assert_eq!(dis.disposition, DispositionType::Attachment);
1578 assert_eq!(
1579 dis.params.get("name"),
1580 Some(&"King Joffrey.death".to_string())
1581 );
1582 assert_eq!(dis.params.get("filename"), None);
1583
1584 let dis = parse_content_disposition(" form-data");
1585 assert_eq!(dis.disposition, DispositionType::FormData);
1586 assert_eq!(dis.params.get("name"), None);
1587 assert_eq!(dis.params.get("filename"), None);
1588 }
1589
1590 #[test]
1591 fn test_parse_mail() {
1592 let mail = parse_mail(b"Key: value\r\n\r\nSome body stuffs").unwrap();
1593 assert_eq!(mail.header_bytes, b"Key: value\r\n\r\n");
1594 assert_eq!(mail.headers.len(), 1);
1595 assert_eq!(mail.headers[0].get_key(), "Key");
1596 assert_eq!(mail.headers[0].get_key_ref(), "Key");
1597 assert_eq!(mail.headers[0].get_value(), "value");
1598 assert_eq!(mail.ctype.mimetype, "text/plain");
1599 assert_eq!(mail.ctype.charset, "us-ascii");
1600 assert_eq!(mail.ctype.params.get("boundary"), None);
1601 assert_eq!(mail.body_bytes, b"Some body stuffs");
1602 assert_eq!(mail.get_body_raw().unwrap(), b"Some body stuffs");
1603 assert_eq!(mail.get_body().unwrap(), "Some body stuffs");
1604 assert_eq!(mail.subparts.len(), 0);
1605
1606 let mail = parse_mail(
1607 concat!(
1608 "Content-Type: MULTIpart/alternative; bounDAry=myboundary\r\n\r\n",
1609 "--myboundary\r\n",
1610 "Content-Type: text/plain\r\n\r\n",
1611 "This is the plaintext version.\r\n",
1612 "--myboundary\r\n",
1613 "Content-Type: text/html;chARset=utf-8\r\n\r\n",
1614 "This is the <b>HTML</b> version with fake --MYBOUNDARY.\r\n",
1615 "--myboundary--"
1616 )
1617 .as_bytes(),
1618 )
1619 .unwrap();
1620 assert_eq!(mail.headers.len(), 1);
1621 assert_eq!(mail.headers[0].get_key(), "Content-Type");
1622 assert_eq!(mail.headers[0].get_key_ref(), "Content-Type");
1623 assert_eq!(mail.ctype.mimetype, "multipart/alternative");
1624 assert_eq!(mail.ctype.charset, "us-ascii");
1625 assert_eq!(mail.ctype.params.get("boundary").unwrap(), "myboundary");
1626 assert_eq!(mail.subparts.len(), 2);
1627 assert_eq!(mail.subparts[0].headers.len(), 1);
1628 assert_eq!(mail.subparts[0].ctype.mimetype, "text/plain");
1629 assert_eq!(mail.subparts[0].ctype.charset, "us-ascii");
1630 assert_eq!(mail.subparts[0].ctype.params.get("boundary"), None);
1631 assert_eq!(mail.subparts[1].ctype.mimetype, "text/html");
1632 assert_eq!(mail.subparts[1].ctype.charset, "utf-8");
1633 assert_eq!(mail.subparts[1].ctype.params.get("boundary"), None);
1634
1635 let mail =
1636 parse_mail(b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=").unwrap();
1637 assert_eq!(mail.get_body_raw().unwrap(), b"hello world");
1638 assert_eq!(mail.get_body().unwrap(), "hello world");
1639
1640 let mail =
1641 parse_mail(b"Content-Type: text/plain; charset=x-unknown\r\n\r\nhello world").unwrap();
1642 assert_eq!(mail.get_body_raw().unwrap(), b"hello world");
1643 assert_eq!(mail.get_body().unwrap(), "hello world");
1644
1645 let mail = parse_mail(b"ConTENT-tyPE: text/html\r\n\r\nhello world").unwrap();
1646 assert_eq!(mail.ctype.mimetype, "text/html");
1647 assert_eq!(mail.get_body_raw().unwrap(), b"hello world");
1648 assert_eq!(mail.get_body().unwrap(), "hello world");
1649
1650 let mail = parse_mail(
1651 b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n+JgM-",
1652 ).unwrap();
1653 assert_eq!(mail.get_body_raw().unwrap(), b"+JgM-");
1654 assert_eq!(mail.get_body().unwrap(), "\u{2603}");
1655
1656 let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\n\r\n+JgM-").unwrap();
1657 assert_eq!(mail.get_body_raw().unwrap(), b"+JgM-");
1658 assert_eq!(mail.get_body().unwrap(), "\u{2603}");
1659 }
1660
1661 #[test]
1662 fn test_missing_terminating_boundary() {
1663 let mail = parse_mail(
1664 concat!(
1665 "Content-Type: multipart/alternative; boundary=myboundary\r\n\r\n",
1666 "--myboundary\r\n",
1667 "Content-Type: text/plain\r\n\r\n",
1668 "part0\r\n",
1669 "--myboundary\r\n",
1670 "Content-Type: text/html\r\n\r\n",
1671 "part1\r\n"
1672 )
1673 .as_bytes(),
1674 )
1675 .unwrap();
1676 assert_eq!(mail.subparts[0].get_body().unwrap(), "part0");
1677 assert_eq!(mail.subparts[1].get_body().unwrap(), "part1\r\n");
1678 }
1679
1680 #[test]
1681 fn test_missing_body() {
1682 let parsed =
1683 parse_mail("Content-Type: multipart/related; boundary=\"----=_\"\n".as_bytes())
1684 .unwrap();
1685 assert_eq!(parsed.headers[0].get_key(), "Content-Type");
1686 assert_eq!(parsed.get_body_raw().unwrap(), b"");
1687 assert_eq!(parsed.get_body().unwrap(), "");
1688 }
1689
1690 #[test]
1691 fn test_no_headers_in_subpart() {
1692 let mail = parse_mail(
1693 concat!(
1694 "Content-Type: multipart/report; report-type=delivery-status;\n",
1695 "\tboundary=\"1404630116.22555.postech.q0.x.x.x\"\n",
1696 "\n",
1697 "--1404630116.22555.postech.q0.x.x.x\n",
1698 "\n",
1699 "--1404630116.22555.postech.q0.x.x.x--\n"
1700 )
1701 .as_bytes(),
1702 )
1703 .unwrap();
1704 assert_eq!(mail.ctype.mimetype, "multipart/report");
1705 assert_eq!(mail.subparts[0].headers.len(), 0);
1706 assert_eq!(mail.subparts[0].ctype.mimetype, "text/plain");
1707 assert_eq!(mail.subparts[0].get_body_raw().unwrap(), b"");
1708 assert_eq!(mail.subparts[0].get_body().unwrap(), "");
1709 }
1710
1711 #[test]
1712 fn test_empty() {
1713 let mail = parse_mail("".as_bytes()).unwrap();
1714 assert_eq!(mail.get_body_raw().unwrap(), b"");
1715 assert_eq!(mail.get_body().unwrap(), "");
1716 }
1717
1718 #[test]
1719 fn test_dont_panic_for_value_with_new_lines() {
1720 let parsed = parse_param_content(r#"application/octet-stream; name=""#);
1721 assert_eq!(parsed.params["name"], "\"");
1722 }
1723
1724 #[test]
1725 fn test_parameter_value_continuations() {
1726 let parsed =
1727 parse_param_content("attachment;\n\tfilename*0=\"X\";\n\tfilename*1=\"Y.pdf\"");
1728 assert_eq!(parsed.value, "attachment");
1729 assert_eq!(parsed.params["filename"], "XY.pdf");
1730 assert_eq!(parsed.params.contains_key("filename*0"), false);
1731 assert_eq!(parsed.params.contains_key("filename*1"), false);
1732
1733 let parsed = parse_param_content(
1734 "attachment;\n\tfilename=XX.pdf;\n\tfilename*0=\"X\";\n\tfilename*1=\"Y.pdf\"",
1735 );
1736 assert_eq!(parsed.value, "attachment");
1737 assert_eq!(parsed.params["filename"], "XX.pdf");
1738 assert_eq!(parsed.params["filename*0"], "X");
1739 assert_eq!(parsed.params["filename*1"], "Y.pdf");
1740
1741 let parsed = parse_param_content("attachment; filename*1=\"Y.pdf\"");
1742 assert_eq!(parsed.params["filename*1"], "Y.pdf");
1743 assert_eq!(parsed.params.contains_key("filename"), false);
1744 }
1745
1746 #[test]
1747 fn test_parameter_encodings() {
1748 let parsed = parse_param_content("attachment;\n\tfilename*0*=us-ascii''%28X%29%20801%20-%20X;\n\tfilename*1*=%20%E2%80%93%20X%20;\n\tfilename*2*=X%20X%2Epdf");
1749 assert_eq!(
1752 parsed.params["filename"],
1753 "(X) 801 - X \u{00E2}\u{20AC}\u{201C} X X X.pdf"
1754 );
1755 assert_eq!(parsed.params.contains_key("filename*0*"), false);
1756 assert_eq!(parsed.params.contains_key("filename*0"), false);
1757 assert_eq!(parsed.params.contains_key("filename*1*"), false);
1758 assert_eq!(parsed.params.contains_key("filename*1"), false);
1759 assert_eq!(parsed.params.contains_key("filename*2*"), false);
1760 assert_eq!(parsed.params.contains_key("filename*2"), false);
1761
1762 let parsed = parse_param_content("attachment;\n\tfilename*0*=utf-8''%28X%29%20801%20-%20X;\n\tfilename*1*=%20%E2%80%93%20X%20;\n\tfilename*2*=X%20X%2Epdf");
1764 assert_eq!(parsed.params["filename"], "(X) 801 - X \u{2013} X X X.pdf");
1765 assert_eq!(parsed.params.contains_key("filename*0*"), false);
1766 assert_eq!(parsed.params.contains_key("filename*0"), false);
1767 assert_eq!(parsed.params.contains_key("filename*1*"), false);
1768 assert_eq!(parsed.params.contains_key("filename*1"), false);
1769 assert_eq!(parsed.params.contains_key("filename*2*"), false);
1770 assert_eq!(parsed.params.contains_key("filename*2"), false);
1771 let parsed = parse_param_content("attachment; filename*=utf-8'en'%e2%80%A1.bin");
1772 assert_eq!(parsed.params["filename"], "\u{2021}.bin");
1773 assert_eq!(parsed.params.contains_key("filename*"), false);
1774
1775 let parsed = parse_param_content("attachment; filename*='foo'%e2%80%A1.bin");
1776 assert_eq!(parsed.params["filename*"], "'foo'%e2%80%A1.bin");
1777 assert_eq!(parsed.params.contains_key("filename"), false);
1778
1779 let parsed = parse_param_content("attachment; filename*=nonexistent'foo'%e2%80%a1.bin");
1780 assert_eq!(parsed.params["filename*"], "nonexistent'foo'%e2%80%a1.bin");
1781 assert_eq!(parsed.params.contains_key("filename"), false);
1782
1783 let parsed = parse_param_content(
1784 "attachment; filename*0*=utf-8'en'%e2%80%a1; filename*1*=%e2%80%A1.bin",
1785 );
1786 assert_eq!(parsed.params["filename"], "\u{2021}\u{2021}.bin");
1787 assert_eq!(parsed.params.contains_key("filename*0*"), false);
1788 assert_eq!(parsed.params.contains_key("filename*0"), false);
1789 assert_eq!(parsed.params.contains_key("filename*1*"), false);
1790 assert_eq!(parsed.params.contains_key("filename*1"), false);
1791
1792 let parsed =
1793 parse_param_content("attachment; filename*0*=utf-8'en'%e2%80%a1; filename*1=%20.bin");
1794 assert_eq!(parsed.params["filename"], "\u{2021}%20.bin");
1795 assert_eq!(parsed.params.contains_key("filename*0*"), false);
1796 assert_eq!(parsed.params.contains_key("filename*0"), false);
1797 assert_eq!(parsed.params.contains_key("filename*1*"), false);
1798 assert_eq!(parsed.params.contains_key("filename*1"), false);
1799
1800 let parsed =
1801 parse_param_content("attachment; filename*0*=utf-8'en'%e2%80%a1; filename*2*=%20.bin");
1802 assert_eq!(parsed.params["filename"], "\u{2021}");
1803 assert_eq!(parsed.params["filename*2"], " .bin");
1804 assert_eq!(parsed.params.contains_key("filename*0*"), false);
1805 assert_eq!(parsed.params.contains_key("filename*0"), false);
1806 assert_eq!(parsed.params.contains_key("filename*2*"), false);
1807
1808 let parsed =
1809 parse_param_content("attachment; filename*0*=utf-8'en'%e2%80%a1; filename*0=foo.bin");
1810 assert_eq!(parsed.params["filename"], "foo.bin");
1811 assert_eq!(parsed.params["filename*0*"], "utf-8'en'%e2%80%a1");
1812 assert_eq!(parsed.params.contains_key("filename*0"), false);
1813 }
1814
1815 #[test]
1816 fn test_default_content_encoding() {
1817 let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\n\r\n+JgM-").unwrap();
1818 let body = mail.get_body_encoded();
1819 match body {
1820 Body::SevenBit(body) => {
1821 assert_eq!(body.get_raw(), b"+JgM-");
1822 assert_eq!(body.get_as_string().unwrap(), "\u{2603}");
1823 }
1824 _ => assert!(false),
1825 };
1826 }
1827
1828 #[test]
1829 fn test_7bit_content_encoding() {
1830 let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: 7bit\r\n\r\n+JgM-").unwrap();
1831 let body = mail.get_body_encoded();
1832 match body {
1833 Body::SevenBit(body) => {
1834 assert_eq!(body.get_raw(), b"+JgM-");
1835 assert_eq!(body.get_as_string().unwrap(), "\u{2603}");
1836 }
1837 _ => assert!(false),
1838 };
1839 }
1840
1841 #[test]
1842 fn test_8bit_content_encoding() {
1843 let mail = parse_mail(b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: 8bit\r\n\r\n+JgM-").unwrap();
1844 let body = mail.get_body_encoded();
1845 match body {
1846 Body::EightBit(body) => {
1847 assert_eq!(body.get_raw(), b"+JgM-");
1848 assert_eq!(body.get_as_string().unwrap(), "\u{2603}");
1849 }
1850 _ => assert!(false),
1851 };
1852 }
1853
1854 #[test]
1855 fn test_quoted_printable_content_encoding() {
1856 let mail = parse_mail(
1857 b"Content-Type: text/plain; charset=UTF-7\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n+JgM-",
1858 ).unwrap();
1859 match mail.get_body_encoded() {
1860 Body::QuotedPrintable(body) => {
1861 assert_eq!(body.get_raw(), b"+JgM-");
1862 assert_eq!(body.get_decoded().unwrap(), b"+JgM-");
1863 assert_eq!(body.get_decoded_as_string().unwrap(), "\u{2603}");
1864 }
1865 _ => assert!(false),
1866 };
1867 }
1868
1869 #[test]
1870 fn test_base64_content_encoding() {
1871 let mail =
1872 parse_mail(b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=").unwrap();
1873 match mail.get_body_encoded() {
1874 Body::Base64(body) => {
1875 assert_eq!(body.get_raw(), b"aGVsbG 8gd\r\n29ybGQ=");
1876 assert_eq!(body.get_decoded().unwrap(), b"hello world");
1877 assert_eq!(body.get_decoded_as_string().unwrap(), "hello world");
1878 }
1879 _ => assert!(false),
1880 };
1881 }
1882
1883 #[test]
1884 fn test_base64_content_encoding_multiple_strings() {
1885 let mail = parse_mail(
1886 b"Content-Transfer-Encoding: base64\r\n\r\naGVsbG 8gd\r\n29ybGQ=\r\nZm9vCg==",
1887 )
1888 .unwrap();
1889 match mail.get_body_encoded() {
1890 Body::Base64(body) => {
1891 assert_eq!(body.get_raw(), b"aGVsbG 8gd\r\n29ybGQ=\r\nZm9vCg==");
1892 assert_eq!(body.get_decoded().unwrap(), b"hello worldfoo\n");
1893 assert_eq!(body.get_decoded_as_string().unwrap(), "hello worldfoo\n");
1894 }
1895 _ => assert!(false),
1896 };
1897 }
1898
1899 #[test]
1900 fn test_binary_content_encoding() {
1901 let mail = parse_mail(b"Content-Transfer-Encoding: binary\r\n\r\n######").unwrap();
1902 let body = mail.get_body_encoded();
1903 match body {
1904 Body::Binary(body) => {
1905 assert_eq!(body.get_raw(), b"######");
1906 }
1907 _ => assert!(false),
1908 };
1909 }
1910
1911 #[test]
1912 fn test_body_content_encoding_with_multipart() {
1913 let mail_filepath = "./tests/files/test_email_01.txt";
1914 let mail = std::fs::read(mail_filepath)
1915 .expect(&format!("Unable to open the file [{}]", mail_filepath));
1916 let mail = parse_mail(&mail).unwrap();
1917
1918 let subpart_0 = mail.subparts.get(0).unwrap();
1919 match subpart_0.get_body_encoded() {
1920 Body::SevenBit(body) => {
1921 assert_eq!(
1922 body.get_as_string().unwrap().trim(),
1923 "<html>Test with attachments</html>"
1924 );
1925 }
1926 _ => assert!(false),
1927 };
1928
1929 let subpart_1 = mail.subparts.get(1).unwrap();
1930 match subpart_1.get_body_encoded() {
1931 Body::Base64(body) => {
1932 let pdf_filepath = "./tests/files/test_email_01_sample.pdf";
1933 let original_pdf = std::fs::read(pdf_filepath)
1934 .expect(&format!("Unable to open the file [{}]", pdf_filepath));
1935 assert_eq!(body.get_decoded().unwrap(), original_pdf);
1936 }
1937 _ => assert!(false),
1938 };
1939
1940 let subpart_2 = mail.subparts.get(2).unwrap();
1941 match subpart_2.get_body_encoded() {
1942 Body::Base64(body) => {
1943 assert_eq!(
1944 body.get_decoded_as_string().unwrap(),
1945 "txt file context for email collector\n1234567890987654321\n"
1946 );
1947 }
1948 _ => assert!(false),
1949 };
1950 }
1951
1952 #[test]
1953 fn test_fuzzer_testcase() {
1954 const INPUT: &str = "U3ViamVjdDplcy1UeXBlOiBtdW50ZW50LVV5cGU6IW11bAAAAAAAAAAAamVjdDplcy1UeXBlOiBtdW50ZW50LVV5cGU6IG11bAAAAAAAAAAAAAAAAABTTUFZdWJqZf86OiP/dCBTdWJqZWN0Ol8KRGF0ZTog/////////////////////wAAAAAAAAAAAHQgYnJmAHQgYnJmZXItRW5jeXBlOnY9NmU3OjA2OgAAAAAAAAAAAAAAADEAAAAAAP/8mAAAAAAAAAAA+f///wAAAAAAAP8AAAAAAAAAAAAAAAAAAAAAAAAAPT0/PzEAAAEAAA==";
1955
1956 if let Ok(parsed) = parse_mail(&data_encoding::BASE64.decode(INPUT.as_bytes()).unwrap()) {
1957 if let Some(date) = parsed.headers.get_first_value("Date") {
1958 let _ = dateparse(&date);
1959 }
1960 }
1961 }
1962
1963 #[test]
1964 fn test_fuzzer_testcase_2() {
1965 const INPUT: &str = "U3ViamVjdDogVGhpcyBpcyBhIHRlc3QgZW1haWwKQ29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7IGJvdW5kYXJ5PczMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMZm9vYmFyCkRhdGU6IFN1biwgMDIgT2MKCi1TdWJqZWMtZm9vYmFydDo=";
1966 if let Ok(parsed) = parse_mail(&data_encoding::BASE64.decode(INPUT.as_bytes()).unwrap()) {
1967 if let Some(date) = parsed.headers.get_first_value("Date") {
1968 let _ = dateparse(&date);
1969 }
1970 }
1971 }
1972
1973 #[test]
1974 fn test_header_split() {
1975 let mail = parse_mail(
1976 b"Content-Type: text/plain;\r\ncharset=\"utf-8\"\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
1977 ).unwrap();
1978 assert_eq!(mail.ctype.mimetype, "text/plain");
1979 assert_eq!(mail.ctype.charset, "us-ascii");
1980 }
1981
1982 #[test]
1983 fn test_percent_decoder() {
1984 assert_eq!(percent_decode("hi %0d%0A%%2A%zz%"), b"hi \r\n%*%zz%");
1985 }
1986
1987 #[test]
1988 fn test_default_content_type_in_multipart_digest() {
1989 let mail = parse_mail(
1991 concat!(
1992 "Content-Type: multipart/digest; boundary=myboundary\r\n\r\n",
1993 "--myboundary\r\n\r\n",
1994 "blah blah blah\r\n\r\n",
1995 "--myboundary--\r\n"
1996 )
1997 .as_bytes(),
1998 )
1999 .unwrap();
2000 assert_eq!(mail.headers.len(), 1);
2001 assert_eq!(mail.ctype.mimetype, "multipart/digest");
2002 assert_eq!(mail.subparts[0].headers.len(), 0);
2003 assert_eq!(mail.subparts[0].ctype.mimetype, "message/rfc822");
2004
2005 let mail = parse_mail(
2006 concat!(
2007 "Content-Type: multipart/whatever; boundary=myboundary\n",
2008 "\n",
2009 "--myboundary\n",
2010 "\n",
2011 "blah blah blah\n",
2012 "--myboundary\n",
2013 "Content-Type: multipart/digest; boundary=nestedboundary\n",
2014 "\n",
2015 "--nestedboundary\n",
2016 "\n",
2017 "nested default part\n",
2018 "--nestedboundary\n",
2019 "Content-Type: text/html\n",
2020 "\n",
2021 "nested html part\n",
2022 "--nestedboundary\n",
2023 "Content-Type: multipart/insidedigest; boundary=insideboundary\n",
2024 "\n",
2025 "--insideboundary\n",
2026 "\n",
2027 "inside part\n",
2028 "--insideboundary--\n",
2029 "--nestedboundary--\n",
2030 "--myboundary--\n"
2031 )
2032 .as_bytes(),
2033 )
2034 .unwrap();
2035 let mut parts = mail.parts();
2036 let mut part = parts.next().unwrap(); assert_eq!(part.headers.len(), 1);
2039 assert_eq!(part.ctype.mimetype, "multipart/whatever");
2040
2041 part = parts.next().unwrap(); assert_eq!(part.headers.len(), 0);
2043 assert_eq!(part.ctype.mimetype, "text/plain");
2044 assert_eq!(part.get_body_raw().unwrap(), b"blah blah blah");
2045
2046 part = parts.next().unwrap(); assert_eq!(part.ctype.mimetype, "multipart/digest");
2048
2049 part = parts.next().unwrap(); assert_eq!(part.headers.len(), 0);
2051 assert_eq!(part.ctype.mimetype, "message/rfc822");
2052 assert_eq!(part.get_body_raw().unwrap(), b"nested default part");
2053
2054 part = parts.next().unwrap(); assert_eq!(part.headers.len(), 1);
2056 assert_eq!(part.ctype.mimetype, "text/html");
2057 assert_eq!(part.get_body_raw().unwrap(), b"nested html part");
2058
2059 part = parts.next().unwrap(); assert_eq!(part.headers.len(), 1);
2061 assert_eq!(part.ctype.mimetype, "multipart/insidedigest");
2062
2063 part = parts.next().unwrap(); assert_eq!(part.headers.len(), 0);
2065 assert_eq!(part.ctype.mimetype, "text/plain");
2066 assert_eq!(part.get_body_raw().unwrap(), b"inside part");
2067
2068 assert!(parts.next().is_none());
2069 }
2070
2071 #[test]
2072 fn boundary_is_suffix_of_another_boundary() {
2073 let mail = parse_mail(
2075 concat!(
2076 "Content-Type: multipart/mixed; boundary=\"section_boundary\"\n",
2077 "\n",
2078 "--section_boundary\n",
2079 "Content-Type: multipart/alternative; boundary=\"--section_boundary\"\n",
2080 "\n",
2081 "----section_boundary\n",
2082 "Content-Type: text/html;\n",
2083 "\n",
2084 "<em>Good evening!</em>\n",
2085 "----section_boundary\n",
2086 "Content-Type: text/plain;\n",
2087 "\n",
2088 "Good evening!\n",
2089 "----section_boundary\n",
2090 "--section_boundary\n"
2091 )
2092 .as_bytes(),
2093 )
2094 .unwrap();
2095
2096 let mut parts = mail.parts();
2097 let mut part = parts.next().unwrap(); assert_eq!(part.headers.len(), 1);
2100 assert_eq!(part.ctype.mimetype, "multipart/mixed");
2101 assert_eq!(part.subparts.len(), 1);
2102
2103 part = parts.next().unwrap(); assert_eq!(part.headers.len(), 1);
2105 assert_eq!(part.ctype.mimetype, "multipart/alternative");
2106 assert_eq!(part.subparts.len(), 2);
2107
2108 part = parts.next().unwrap(); assert_eq!(part.headers.len(), 1);
2110 assert_eq!(part.ctype.mimetype, "text/html");
2111 assert_eq!(part.get_body_raw().unwrap(), b"<em>Good evening!</em>");
2112 assert_eq!(part.subparts.len(), 0);
2113
2114 part = parts.next().unwrap(); assert_eq!(part.headers.len(), 1);
2116 assert_eq!(part.ctype.mimetype, "text/plain");
2117 assert_eq!(part.get_body_raw().unwrap(), b"Good evening!");
2118 assert_eq!(part.subparts.len(), 0);
2119
2120 assert!(parts.next().is_none());
2121 }
2122
2123 #[test]
2124 fn test_parts_iterator() {
2125 let mail = parse_mail(
2126 concat!(
2127 "Content-Type: multipart/mixed; boundary=\"top_boundary\"\n",
2128 "\n",
2129 "--top_boundary\n",
2130 "Content-Type: multipart/alternative; boundary=\"internal_boundary\"\n",
2131 "\n",
2132 "--internal_boundary\n",
2133 "Content-Type: text/html;\n",
2134 "\n",
2135 "<em>Good evening!</em>\n",
2136 "--internal_boundary\n",
2137 "Content-Type: text/plain;\n",
2138 "\n",
2139 "Good evening!\n",
2140 "--internal_boundary\n",
2141 "--top_boundary\n",
2142 "Content-Type: text/unknown;\n",
2143 "\n",
2144 "You read this?\n",
2145 "--top_boundary\n"
2146 )
2147 .as_bytes(),
2148 )
2149 .unwrap();
2150
2151 let mut parts = mail.parts();
2152 assert_eq!(parts.next().unwrap().ctype.mimetype, "multipart/mixed");
2153 assert_eq!(
2154 parts.next().unwrap().ctype.mimetype,
2155 "multipart/alternative"
2156 );
2157 assert_eq!(parts.next().unwrap().ctype.mimetype, "text/html");
2158 assert_eq!(parts.next().unwrap().ctype.mimetype, "text/plain");
2159 assert_eq!(parts.next().unwrap().ctype.mimetype, "text/unknown");
2160 assert!(parts.next().is_none());
2161
2162 let mail = parse_mail(concat!("Content-Type: text/plain\n").as_bytes()).unwrap();
2163
2164 let mut parts = mail.parts();
2165 assert_eq!(parts.next().unwrap().ctype.mimetype, "text/plain");
2166 assert!(parts.next().is_none());
2167 }
2168
2169 #[test]
2170 fn test_no_parts() {
2171 let mail = parse_mail(
2172 concat!(
2173 "Content-Type: multipart/mixed; boundary=\"foobar\"\n",
2174 "\n",
2175 "--foobar--\n"
2176 )
2177 .as_bytes(),
2178 )
2179 .unwrap();
2180
2181 let mut parts = mail.parts();
2182 let part = parts.next().unwrap();
2183 assert_eq!(part.ctype.mimetype, "multipart/mixed");
2184
2185 let part = parts.next().unwrap();
2186 assert_eq!(part.ctype.mimetype, "text/plain");
2187 assert!(parts.next().is_none());
2188 }
2189}