1use crate::header::{HeaderParseResult, MessageConformance};
2use crate::headermap::HeaderMap;
3use crate::strings::IntoSharedString;
4use crate::{
5 has_lone_cr_or_lf, Header, MailParsingError, MessageID, MimeParameterEncoding, MimeParameters,
6 Result, SharedString,
7};
8use charset_normalizer_rs::entity::NormalizerSettings;
9use charset_normalizer_rs::Encoding;
10use chrono::Utc;
11use serde::{Deserialize, Serialize};
12use std::borrow::Cow;
13use std::str::FromStr;
14use std::sync::Arc;
15
16const BASE64_RFC2045: data_encoding::Encoding = data_encoding_macro::new_encoding! {
19 symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
20 padding: '=',
21 ignore: " \r\n\t",
22 wrap_width: 76,
23 wrap_separator: "\r\n",
24};
25
26#[derive(Debug, Clone, PartialEq)]
27pub struct MimePart<'a> {
28 bytes: SharedString<'a>,
30 headers: HeaderMap<'a>,
32 body_offset: usize,
34 body_len: usize,
35 conformance: MessageConformance,
36 parts: Vec<Self>,
37 intro: SharedString<'a>,
39 outro: SharedString<'a>,
41}
42
43#[derive(PartialEq, Debug)]
44pub struct Rfc2045Info {
45 pub encoding: ContentTransferEncoding,
46 pub charset: Result<&'static Encoding>,
47 pub content_type: Option<MimeParameters>,
48 pub is_text: bool,
49 pub is_multipart: bool,
50 pub attachment_options: Option<AttachmentOptions>,
51 pub invalid_mime_headers: bool,
52}
53
54impl Rfc2045Info {
55 fn new(headers: &HeaderMap) -> Self {
58 let mut invalid_mime_headers = false;
59 let encoding = match headers.content_transfer_encoding() {
60 Ok(Some(cte)) => match ContentTransferEncoding::from_str(&cte.value) {
61 Ok(encoding) => encoding,
62 Err(_) => {
63 invalid_mime_headers = true;
64 ContentTransferEncoding::SevenBit
65 }
66 },
67 Ok(None) => ContentTransferEncoding::SevenBit,
68 Err(_) => {
69 invalid_mime_headers = true;
70 ContentTransferEncoding::SevenBit
71 }
72 };
73
74 let content_type = match headers.content_type() {
75 Ok(ct) => ct,
76 Err(_) => {
77 invalid_mime_headers = true;
78 None
79 }
80 };
81
82 let mut ct_name = None;
83 let charset = if let Some(ct) = &content_type {
84 ct_name = ct.get("name");
85 ct.get("charset")
86 } else {
87 None
88 };
89 let charset = charset.unwrap_or_else(|| "us-ascii".to_string());
90
91 let charset = Encoding::by_name(&*charset)
92 .ok_or_else(|| MailParsingError::BodyParse(format!("unsupported charset {charset}")));
93
94 let (is_text, is_multipart) = if let Some(ct) = &content_type {
95 (ct.is_text(), ct.is_multipart())
96 } else {
97 (true, false)
98 };
99
100 let mut inline = false;
101 let mut cd_file_name = None;
102
103 match headers.content_disposition() {
104 Ok(Some(cd)) => {
105 inline = cd.value == "inline";
106 cd_file_name = cd.get("filename");
107 }
108 Ok(None) => {}
109 Err(_) => {
110 invalid_mime_headers = true;
111 }
112 };
113
114 let content_id = match headers.content_id() {
115 Ok(cid) => cid.map(|cid| cid.0),
116 Err(_) => {
117 invalid_mime_headers = true;
118 None
119 }
120 };
121
122 let file_name = match (cd_file_name, ct_name) {
123 (Some(name), _) | (None, Some(name)) => Some(name),
124 (None, None) => None,
125 };
126
127 let attachment_options = if inline || file_name.is_some() || content_id.is_some() {
128 Some(AttachmentOptions {
129 file_name,
130 inline,
131 content_id,
132 })
133 } else {
134 None
135 };
136
137 Self {
138 encoding,
139 charset,
140 content_type,
141 is_text,
142 is_multipart,
143 attachment_options,
144 invalid_mime_headers,
145 }
146 }
147
148 pub fn content_type(&self) -> Option<&str> {
149 self.content_type
150 .as_ref()
151 .map(|params| params.value.as_str())
152 }
153}
154
155impl<'a> MimePart<'a> {
156 pub fn parse<S>(bytes: S) -> Result<Self>
158 where
159 S: IntoSharedString<'a>,
160 {
161 let (bytes, base_conformance) = bytes.into_shared_string();
162 Self::parse_impl(bytes, base_conformance, true)
163 }
164
165 pub fn to_owned(&self) -> MimePart<'static> {
167 MimePart {
168 bytes: self.bytes.to_owned(),
169 headers: self.headers.to_owned(),
170 body_offset: self.body_offset,
171 body_len: self.body_len,
172 conformance: self.conformance,
173 parts: self.parts.iter().map(|p| p.to_owned()).collect(),
174 intro: self.intro.to_owned(),
175 outro: self.outro.to_owned(),
176 }
177 }
178
179 fn parse_impl(
180 bytes: SharedString<'a>,
181 base_conformance: MessageConformance,
182 is_top_level: bool,
183 ) -> Result<Self> {
184 let HeaderParseResult {
185 headers,
186 body_offset,
187 overall_conformance: mut conformance,
188 } = Header::parse_headers(bytes.clone())?;
189
190 conformance |= base_conformance;
191
192 let body_len = bytes.len();
193
194 if !bytes.is_ascii() {
195 conformance.set(MessageConformance::NEEDS_TRANSFER_ENCODING, true);
196 }
197 {
198 let mut prev = 0;
199 for idx in memchr::memchr_iter(b'\n', bytes.as_bytes()) {
200 if idx - prev > 78 {
201 conformance.set(MessageConformance::LINE_TOO_LONG, true);
202 break;
203 }
204 prev = idx;
205 }
206 }
207 conformance.set(
208 MessageConformance::NON_CANONICAL_LINE_ENDINGS,
209 has_lone_cr_or_lf(bytes.as_bytes()),
210 );
211
212 if is_top_level {
213 conformance.set(
214 MessageConformance::MISSING_DATE_HEADER,
215 !matches!(headers.date(), Ok(Some(_))),
216 );
217 conformance.set(
218 MessageConformance::MISSING_MESSAGE_ID_HEADER,
219 !matches!(headers.message_id(), Ok(Some(_))),
220 );
221 conformance.set(
222 MessageConformance::MISSING_MIME_VERSION,
223 match headers.mime_version() {
224 Ok(Some(v)) => v.as_str() != "1.0",
225 _ => true,
226 },
227 );
228 }
229
230 let mut part = Self {
231 bytes,
232 headers,
233 body_offset,
234 body_len,
235 conformance,
236 parts: vec![],
237 intro: SharedString::Borrowed(""),
238 outro: SharedString::Borrowed(""),
239 };
240
241 part.recursive_parse()?;
242
243 Ok(part)
244 }
245
246 fn recursive_parse(&mut self) -> Result<()> {
247 let info = Rfc2045Info::new(&self.headers);
248 if info.invalid_mime_headers {
249 self.conformance |= MessageConformance::INVALID_MIME_HEADERS;
250 }
251 if let Some((boundary, true)) = info
252 .content_type
253 .as_ref()
254 .and_then(|ct| ct.get("boundary").map(|b| (b, info.is_multipart)))
255 {
256 let boundary = format!("\n--{boundary}");
257 let raw_body = self
258 .bytes
259 .slice(self.body_offset.saturating_sub(1)..self.bytes.len());
260
261 let mut iter = memchr::memmem::find_iter(raw_body.as_bytes(), &boundary);
262 if let Some(first_boundary_pos) = iter.next() {
263 self.intro = raw_body.slice(0..first_boundary_pos);
264
265 self.body_len = 0;
268
269 let mut boundary_end = first_boundary_pos + boundary.len();
270
271 while let Some(part_start) =
272 memchr::memchr(b'\n', &raw_body.as_bytes()[boundary_end..])
273 .map(|p| p + boundary_end + 1)
274 {
275 let part_end = iter
276 .next()
277 .map(|p| {
278 p + 1
281 })
282 .unwrap_or(raw_body.len());
283
284 let child = Self::parse_impl(
285 raw_body.slice(part_start..part_end),
286 MessageConformance::default(),
287 false,
288 )?;
289 self.conformance |= child.conformance;
290 self.parts.push(child);
291
292 boundary_end = part_end -
293 1 + boundary.len();
295
296 if boundary_end + 2 > raw_body.len() {
297 break;
298 }
299 if &raw_body.as_bytes()[boundary_end..boundary_end + 2] == b"--" {
300 if let Some(after_boundary) =
301 memchr::memchr(b'\n', &raw_body.as_bytes()[boundary_end..])
302 .map(|p| p + boundary_end + 1)
303 {
304 self.outro = raw_body.slice(after_boundary..raw_body.len());
305 }
306 break;
307 }
308 }
309 }
310 }
311
312 Ok(())
313 }
314
315 pub fn deep_conformance_check(&self) -> MessageConformance {
322 if self.parts.is_empty() {
323 match self.extract_body(None) {
324 Ok((_, conformance)) => conformance,
325 Err(_) => self.conformance | MessageConformance::NEEDS_TRANSFER_ENCODING,
326 }
327 } else {
328 let mut conformance = self.conformance;
329 for p in &self.parts {
330 conformance |= p.deep_conformance_check();
331 }
332 conformance
333 }
334 }
335
336 pub fn conformance(&self) -> MessageConformance {
338 self.conformance
339 }
340
341 pub fn child_parts(&self) -> &[Self] {
343 &self.parts
344 }
345
346 pub fn child_parts_mut(&mut self) -> &mut Vec<Self> {
348 &mut self.parts
349 }
350
351 pub fn headers(&'_ self) -> &'_ HeaderMap<'_> {
353 &self.headers
354 }
355
356 pub fn headers_mut<'b>(&'b mut self) -> &'b mut HeaderMap<'a> {
358 &mut self.headers
359 }
360
361 pub fn raw_body(&'_ self) -> SharedString<'_> {
363 self.bytes
364 .slice(self.body_offset..self.body_len.max(self.body_offset))
365 }
366
367 pub fn rfc2045_info(&self) -> Rfc2045Info {
368 Rfc2045Info::new(&self.headers)
369 }
370
371 pub fn body(&'_ self) -> Result<DecodedBody<'_>> {
373 let (body, _conformance) = self.extract_body(None)?;
374 Ok(body)
375 }
376
377 fn extract_body(
378 &'_ self,
379 options: Option<&CheckFixSettings>,
380 ) -> Result<(DecodedBody<'_>, MessageConformance)> {
381 let info = Rfc2045Info::new(&self.headers);
382
383 let bytes = match info.encoding {
384 ContentTransferEncoding::Base64 => {
385 let data = self.raw_body();
386 let bytes = data.as_bytes();
387 BASE64_RFC2045.decode(bytes).map_err(|err| {
388 let b = bytes[err.position] as char;
389 let region =
390 &bytes[err.position.saturating_sub(8)..(err.position + 8).min(bytes.len())];
391 let region = String::from_utf8_lossy(region);
392 MailParsingError::BodyParse(format!(
393 "base64 decode: {err:#} b={b:?} in {region}"
394 ))
395 })?
396 }
397 ContentTransferEncoding::QuotedPrintable => quoted_printable::decode(
398 self.raw_body().as_bytes(),
399 quoted_printable::ParseMode::Robust,
400 )
401 .map_err(|err| {
402 MailParsingError::BodyParse(format!("quoted printable decode: {err:#}"))
403 })?,
404 ContentTransferEncoding::SevenBit
405 | ContentTransferEncoding::EightBit
406 | ContentTransferEncoding::Binary => self.raw_body().as_bytes().to_vec(),
407 };
408
409 if info.is_text {
410 let charset = info.charset?;
411
412 match charset.decode_simple(&bytes) {
413 Ok(decoded) => Ok((
414 DecodedBody::Text(decoded.to_string().into()),
415 self.conformance,
416 )),
417 Err(_err) => {
418 if let Some(settings) = options {
419 if settings.detect_encoding {
420 let norm_settings = NormalizerSettings {
421 include_encodings: settings.include_encodings.clone(),
422 exclude_encodings: settings.exclude_encodings.clone(),
423 ..Default::default()
424 };
425
426 if let Ok(guess) =
427 charset_normalizer_rs::from_bytes(&*bytes, Some(norm_settings))
428 {
429 if let Some(decoded) =
430 guess.get_best().and_then(|best| best.decoded_payload())
431 {
432 return Ok((
433 DecodedBody::Text(decoded.to_string().into()),
434 MessageConformance::NEEDS_TRANSFER_ENCODING
435 | self.conformance,
436 ));
437 }
438 }
439
440 return Ok((
448 DecodedBody::Binary(bytes),
449 MessageConformance::NEEDS_TRANSFER_ENCODING | self.conformance,
450 ));
451 }
452 }
453
454 if let Ok(decoded) = std::str::from_utf8(&bytes) {
459 return Ok((
460 DecodedBody::Text(decoded.to_string().into()),
461 MessageConformance::NEEDS_TRANSFER_ENCODING | self.conformance,
462 ));
463 }
464
465 Ok((
468 DecodedBody::Binary(bytes),
469 MessageConformance::NEEDS_TRANSFER_ENCODING | self.conformance,
470 ))
471 }
472 }
473 } else {
474 Ok((DecodedBody::Binary(bytes), self.conformance))
475 }
476 }
477
478 pub fn rebuild(&self, settings: Option<&CheckFixSettings>) -> Result<Self> {
485 let info = Rfc2045Info::new(&self.headers);
486
487 let mut children = vec![];
488 for part in &self.parts {
489 children.push(part.rebuild(settings)?);
490 }
491
492 let mut rebuilt = if children.is_empty() {
493 let (body, _conformance) = self.extract_body(settings)?;
494 match body {
495 DecodedBody::Text(text) => {
496 let ct = info
497 .content_type
498 .as_ref()
499 .map(|ct| ct.value.as_str())
500 .unwrap_or("text/plain");
501 Self::new_text(ct, text.as_str())?
502 }
503 DecodedBody::Binary(data) => {
504 let ct = info
505 .content_type
506 .as_ref()
507 .map(|ct| ct.value.as_str())
508 .unwrap_or("application/octet-stream");
509 Self::new_binary(ct, &data, info.attachment_options.as_ref())?
510 }
511 }
512 } else {
513 let ct = info.content_type.ok_or_else(|| {
514 MailParsingError::BodyParse(
515 "multipart message has no content-type information!?".to_string(),
516 )
517 })?;
518 Self::new_multipart(&ct.value, children, ct.get("boundary").as_deref())?
519 };
520
521 for hdr in self.headers.iter() {
522 let name = hdr.get_name();
523 if name.eq_ignore_ascii_case("Content-ID") {
524 continue;
525 }
526
527 if name.eq_ignore_ascii_case("Content-Type") {
530 if let Ok(params) = hdr.as_content_type() {
531 let Some(mut dest) = rebuilt.headers_mut().content_type()? else {
532 continue;
533 };
534
535 for (k, v) in params.parameter_map() {
536 if dest.get(&k).is_none() {
537 dest.set(&k, &v);
538 }
539 }
540
541 rebuilt.headers_mut().set_content_type(dest)?;
542 }
543 continue;
544 }
545 if name.eq_ignore_ascii_case("Content-Transfer-Encoding") {
546 if let Ok(params) = hdr.as_content_transfer_encoding() {
547 let Some(mut dest) = rebuilt.headers_mut().content_transfer_encoding()? else {
548 continue;
549 };
550
551 for (k, v) in params.parameter_map() {
552 if dest.get(&k).is_none() {
553 dest.set(&k, &v);
554 }
555 }
556
557 rebuilt.headers_mut().set_content_transfer_encoding(dest)?;
558 }
559 continue;
560 }
561 if name.eq_ignore_ascii_case("Content-Disposition") {
562 if let Ok(params) = hdr.as_content_disposition() {
563 let Some(mut dest) = rebuilt.headers_mut().content_disposition()? else {
564 continue;
565 };
566
567 for (k, v) in params.parameter_map() {
568 if dest.get(&k).is_none() {
569 dest.set(&k, &v);
570 }
571 }
572
573 rebuilt.headers_mut().set_content_disposition(dest)?;
574 }
575 continue;
576 }
577
578 if let Ok(hdr) = hdr.rebuild() {
579 rebuilt.headers_mut().push(hdr);
580 }
581 }
582
583 Ok(rebuilt)
584 }
585
586 pub fn write_message<W: std::io::Write>(&self, out: &mut W) -> Result<()> {
588 let line_ending = if self
589 .conformance
590 .contains(MessageConformance::NON_CANONICAL_LINE_ENDINGS)
591 {
592 "\n"
593 } else {
594 "\r\n"
595 };
596
597 for hdr in self.headers.iter() {
598 hdr.write_header(out)
599 .map_err(|_| MailParsingError::WriteMessageIOError)?;
600 }
601 out.write_all(line_ending.as_bytes())
602 .map_err(|_| MailParsingError::WriteMessageIOError)?;
603
604 if self.parts.is_empty() {
605 out.write_all(self.raw_body().as_bytes())
606 .map_err(|_| MailParsingError::WriteMessageIOError)?;
607 } else {
608 let info = Rfc2045Info::new(&self.headers);
609 let ct = info.content_type.ok_or({
610 MailParsingError::WriteMessageWtf(
611 "expected to have Content-Type when there are child parts",
612 )
613 })?;
614 let boundary = ct.get("boundary").ok_or({
615 MailParsingError::WriteMessageWtf("expected Content-Type to have a boundary")
616 })?;
617 out.write_all(self.intro.as_bytes())
618 .map_err(|_| MailParsingError::WriteMessageIOError)?;
619 for p in &self.parts {
620 write!(out, "--{boundary}{line_ending}")
621 .map_err(|_| MailParsingError::WriteMessageIOError)?;
622 p.write_message(out)?;
623 }
624 write!(out, "--{boundary}--{line_ending}")
625 .map_err(|_| MailParsingError::WriteMessageIOError)?;
626 out.write_all(self.outro.as_bytes())
627 .map_err(|_| MailParsingError::WriteMessageIOError)?;
628 }
629 Ok(())
630 }
631
632 pub fn to_message_string(&self) -> String {
635 let mut out = vec![];
636 self.write_message(&mut out).unwrap();
637 String::from_utf8_lossy(&out).to_string()
638 }
639
640 pub fn replace_text_body(&mut self, content_type: &str, content: &str) -> Result<()> {
641 let mut new_part = Self::new_text(content_type, content)?;
642 self.bytes = new_part.bytes;
643 self.body_offset = new_part.body_offset;
644 self.body_len = new_part.body_len;
645 self.headers.remove_all_named("Content-Type");
649 self.headers.remove_all_named("Content-Transfer-Encoding");
650 self.headers.append(&mut new_part.headers.headers);
652 Ok(())
653 }
654
655 pub fn replace_binary_body(&mut self, content_type: &str, content: &[u8]) -> Result<()> {
656 let mut new_part = Self::new_binary(content_type, content, None)?;
657 self.bytes = new_part.bytes;
658 self.body_offset = new_part.body_offset;
659 self.body_len = new_part.body_len;
660 self.headers.remove_all_named("Content-Type");
664 self.headers.remove_all_named("Content-Transfer-Encoding");
665 self.headers.append(&mut new_part.headers.headers);
667 Ok(())
668 }
669
670 pub fn new_no_transfer_encoding(content_type: &str, bytes: &[u8]) -> Result<Self> {
671 if bytes.iter().any(|b| !b.is_ascii()) {
672 return Err(MailParsingError::EightBit);
673 }
674
675 let mut headers = HeaderMap::default();
676
677 let ct = MimeParameters::new(content_type);
678 headers.set_content_type(ct)?;
679
680 let bytes = String::from_utf8_lossy(bytes).to_string();
681 let body_len = bytes.len();
682
683 Ok(Self {
684 bytes: bytes.into(),
685 headers,
686 body_offset: 0,
687 body_len,
688 conformance: MessageConformance::default(),
689 parts: vec![],
690 intro: "".into(),
691 outro: "".into(),
692 })
693 }
694
695 pub fn new_text(content_type: &str, content: &str) -> Result<Self> {
699 let qp_encoded = quoted_printable::encode(content);
701
702 let (mut encoded, encoding) = if qp_encoded == content.as_bytes() {
703 (qp_encoded, None)
704 } else if qp_encoded.len() <= BASE64_RFC2045.encode_len(content.len()) {
705 (qp_encoded, Some("quoted-printable"))
706 } else {
707 (
710 BASE64_RFC2045.encode(content.as_bytes()).into_bytes(),
711 Some("base64"),
712 )
713 };
714
715 if !encoded.ends_with(b"\r\n") {
716 encoded.extend_from_slice(b"\r\n");
717 }
718 let mut headers = HeaderMap::default();
719
720 let mut ct = MimeParameters::new(content_type);
721 ct.set(
722 "charset",
723 if content.is_ascii() {
724 "us-ascii"
725 } else {
726 "utf-8"
727 },
728 );
729 headers.set_content_type(ct)?;
730
731 if let Some(encoding) = encoding {
732 headers.set_content_transfer_encoding(MimeParameters::new(encoding))?;
733 }
734
735 let body_len = encoded.len();
736 let bytes =
737 String::from_utf8(encoded).expect("transfer encoder to produce valid ASCII output");
738
739 Ok(Self {
740 bytes: bytes.into(),
741 headers,
742 body_offset: 0,
743 body_len,
744 conformance: MessageConformance::default(),
745 parts: vec![],
746 intro: "".into(),
747 outro: "".into(),
748 })
749 }
750
751 pub fn new_text_plain(content: &str) -> Result<Self> {
752 Self::new_text("text/plain", content)
753 }
754
755 pub fn new_html(content: &str) -> Result<Self> {
756 Self::new_text("text/html", content)
757 }
758
759 pub fn new_multipart(
760 content_type: &str,
761 parts: Vec<Self>,
762 boundary: Option<&str>,
763 ) -> Result<Self> {
764 let mut headers = HeaderMap::default();
765
766 let mut ct = MimeParameters::new(content_type);
767 match boundary {
768 Some(b) => {
769 ct.set("boundary", b);
770 }
771 None => {
772 let uuid = uuid::Uuid::new_v4();
774 let boundary = data_encoding::BASE64_NOPAD.encode(uuid.as_bytes());
775 ct.set("boundary", &boundary);
776 }
777 }
778 headers.set_content_type(ct)?;
779
780 Ok(Self {
781 bytes: "".into(),
782 headers,
783 body_offset: 0,
784 body_len: 0,
785 conformance: MessageConformance::default(),
786 parts,
787 intro: "".into(),
788 outro: "".into(),
789 })
790 }
791
792 pub fn new_binary(
793 content_type: &str,
794 content: &[u8],
795 options: Option<&AttachmentOptions>,
796 ) -> Result<Self> {
797 let mut encoded = BASE64_RFC2045.encode(content);
798 if !encoded.ends_with("\r\n") {
799 encoded.push_str("\r\n");
800 }
801 let mut headers = HeaderMap::default();
802
803 let mut ct = MimeParameters::new(content_type);
804
805 if let Some(opts) = options {
806 let mut cd = MimeParameters::new(if opts.inline { "inline" } else { "attachment" });
807 if let Some(name) = &opts.file_name {
808 cd.set("filename", name);
809 let encoding = if name.chars().any(|c| !c.is_ascii()) {
810 MimeParameterEncoding::QuotedRfc2047
811 } else {
812 MimeParameterEncoding::None
813 };
814 ct.set_with_encoding("name", name, encoding);
815 }
816 headers.set_content_disposition(cd)?;
817
818 if let Some(id) = &opts.content_id {
819 headers.set_content_id(MessageID(id.to_string()))?;
820 }
821 }
822
823 headers.set_content_type(ct)?;
824 headers.set_content_transfer_encoding(MimeParameters::new("base64"))?;
825
826 let body_len = encoded.len();
827
828 Ok(Self {
829 bytes: encoded.into(),
830 headers,
831 body_offset: 0,
832 body_len,
833 conformance: MessageConformance::default(),
834 parts: vec![],
835 intro: "".into(),
836 outro: "".into(),
837 })
838 }
839
840 pub fn simplified_structure(&'a self) -> Result<SimplifiedStructure<'a>> {
845 let parts = self.simplified_structure_pointers()?;
846
847 let mut text = None;
848 let mut html = None;
849 let mut amp_html = None;
850
851 let headers = &self
852 .resolve_ptr(parts.header_part)
853 .expect("header part to always be valid")
854 .headers;
855
856 if let Some(p) = parts.text_part.and_then(|p| self.resolve_ptr(p)) {
857 text = match p.body()? {
858 DecodedBody::Text(t) => Some(t),
859 DecodedBody::Binary(_) => {
860 return Err(MailParsingError::BodyParse(
861 "expected text/plain part to be text, but it is binary".to_string(),
862 ))
863 }
864 };
865 }
866 if let Some(p) = parts.html_part.and_then(|p| self.resolve_ptr(p)) {
867 html = match p.body()? {
868 DecodedBody::Text(t) => Some(t),
869 DecodedBody::Binary(_) => {
870 return Err(MailParsingError::BodyParse(
871 "expected text/html part to be text, but it is binary".to_string(),
872 ))
873 }
874 };
875 }
876 if let Some(p) = parts.amp_html_part.and_then(|p| self.resolve_ptr(p)) {
877 amp_html = match p.body()? {
878 DecodedBody::Text(t) => Some(t),
879 DecodedBody::Binary(_) => {
880 return Err(MailParsingError::BodyParse(
881 "expected text/x-amp-html part to be text, but it is binary".to_string(),
882 ))
883 }
884 };
885 }
886
887 let mut attachments = vec![];
888 for ptr in parts.attachments {
889 attachments.push(self.resolve_ptr(ptr).expect("pointer to be valid").clone());
890 }
891
892 Ok(SimplifiedStructure {
893 text,
894 html,
895 amp_html,
896 headers,
897 attachments,
898 })
899 }
900
901 pub fn resolve_ptr(&self, ptr: PartPointer) -> Option<&Self> {
903 let mut current = self;
904 let mut cursor = ptr.0.as_slice();
905
906 loop {
907 match cursor.first() {
908 Some(&idx) => {
909 current = current.parts.get(idx as usize)?;
910 cursor = &cursor[1..];
911 }
912 None => {
913 return Some(current);
915 }
916 }
917 }
918 }
919
920 pub fn resolve_ptr_mut(&mut self, ptr: PartPointer) -> Option<&mut Self> {
922 let mut current = self;
923 let mut cursor = ptr.0.as_slice();
924
925 loop {
926 match cursor.first() {
927 Some(&idx) => {
928 current = current.parts.get_mut(idx as usize)?;
929 cursor = &cursor[1..];
930 }
931 None => {
932 return Some(current);
934 }
935 }
936 }
937 }
938
939 pub fn simplified_structure_pointers(&self) -> Result<SimplifiedStructurePointers> {
945 self.simplified_structure_pointers_impl(None)
946 }
947
948 fn simplified_structure_pointers_impl(
949 &self,
950 my_idx: Option<u8>,
951 ) -> Result<SimplifiedStructurePointers> {
952 let info = Rfc2045Info::new(&self.headers);
953 let is_inline = info
954 .attachment_options
955 .as_ref()
956 .map(|ao| ao.inline)
957 .unwrap_or(true);
958
959 if let Some(ct) = &info.content_type {
960 if is_inline {
961 if ct.value == "text/plain" {
962 return Ok(SimplifiedStructurePointers {
963 amp_html_part: None,
964 text_part: Some(PartPointer::root_or_nth(my_idx)),
965 html_part: None,
966 header_part: PartPointer::root_or_nth(my_idx),
967 attachments: vec![],
968 });
969 }
970 if ct.value == "text/html" {
971 return Ok(SimplifiedStructurePointers {
972 amp_html_part: None,
973 html_part: Some(PartPointer::root_or_nth(my_idx)),
974 text_part: None,
975 header_part: PartPointer::root_or_nth(my_idx),
976 attachments: vec![],
977 });
978 }
979 if ct.value == "text/x-amp-html" {
980 return Ok(SimplifiedStructurePointers {
981 amp_html_part: Some(PartPointer::root_or_nth(my_idx)),
982 html_part: None,
983 text_part: None,
984 header_part: PartPointer::root_or_nth(my_idx),
985 attachments: vec![],
986 });
987 }
988 }
989
990 if ct.value.starts_with("multipart/") {
991 let mut text_part = None;
992 let mut html_part = None;
993 let mut amp_html_part = None;
994 let mut attachments = vec![];
995
996 for (i, p) in self.parts.iter().enumerate() {
997 let part_idx = i.try_into().map_err(|_| MailParsingError::TooManyParts)?;
998 if let Ok(mut s) = p.simplified_structure_pointers_impl(Some(part_idx)) {
999 if let Some(p) = s.text_part {
1000 if text_part.is_none() {
1001 text_part.replace(PartPointer::root_or_nth(my_idx).append(p));
1002 } else {
1003 attachments.push(p);
1004 }
1005 }
1006 if let Some(p) = s.html_part {
1007 if html_part.is_none() {
1008 html_part.replace(PartPointer::root_or_nth(my_idx).append(p));
1009 } else {
1010 attachments.push(p);
1011 }
1012 }
1013 if let Some(p) = s.amp_html_part {
1014 if amp_html_part.is_none() {
1015 amp_html_part.replace(PartPointer::root_or_nth(my_idx).append(p));
1016 } else {
1017 attachments.push(p);
1018 }
1019 }
1020 attachments.append(&mut s.attachments);
1021 }
1022 }
1023
1024 return Ok(SimplifiedStructurePointers {
1025 amp_html_part,
1026 html_part,
1027 text_part,
1028 header_part: PartPointer::root_or_nth(my_idx),
1029 attachments,
1030 });
1031 }
1032
1033 return Ok(SimplifiedStructurePointers {
1034 html_part: None,
1035 text_part: None,
1036 amp_html_part: None,
1037 header_part: PartPointer::root_or_nth(my_idx),
1038 attachments: vec![PartPointer::root_or_nth(my_idx)],
1039 });
1040 }
1041
1042 Ok(SimplifiedStructurePointers {
1044 text_part: Some(PartPointer::root_or_nth(my_idx)),
1045 html_part: None,
1046 amp_html_part: None,
1047 header_part: PartPointer::root_or_nth(my_idx),
1048 attachments: vec![],
1049 })
1050 }
1051
1052 pub fn check_fix_conformance(
1053 &self,
1054 check: MessageConformance,
1055 fix: MessageConformance,
1056 settings: CheckFixSettings,
1057 ) -> Result<Option<Self>> {
1058 let mut msg = self.clone();
1059 let conformance = msg.deep_conformance_check();
1060
1061 let check = check - fix;
1063
1064 if check.intersects(conformance) {
1065 let problems = check.intersection(conformance);
1066 return Err(MailParsingError::ConformanceIssues(problems));
1067 }
1068
1069 if !fix.intersects(conformance) {
1070 return Ok(None);
1071 }
1072
1073 let to_fix = fix.intersection(conformance);
1074
1075 let missing_headers_only = to_fix
1076 .difference(
1077 MessageConformance::MISSING_DATE_HEADER
1078 | MessageConformance::MISSING_MIME_VERSION
1079 | MessageConformance::MISSING_MESSAGE_ID_HEADER,
1080 )
1081 .is_empty();
1082
1083 if !missing_headers_only {
1084 if to_fix.contains(MessageConformance::NEEDS_TRANSFER_ENCODING) {
1085 if settings.detect_encoding {
1094 if let Some(data_bytes) = &settings.data_bytes {
1095 let norm_settings = NormalizerSettings {
1096 include_encodings: settings.include_encodings.clone(),
1097 exclude_encodings: settings.exclude_encodings.clone(),
1098 ..Default::default()
1099 };
1100
1101 let guess =
1102 charset_normalizer_rs::from_bytes(&*data_bytes, Some(norm_settings))
1103 .map_err(|err| MailParsingError::CharsetDetectionFailed(err))?;
1104 if let Some(best) = guess.get_best() {
1105 if let Some(decoded) = best.decoded_payload() {
1106 msg = MimePart::parse(decoded.to_string())?;
1107 }
1108 }
1109 }
1110 }
1111 }
1112
1113 msg = msg.rebuild(Some(&settings))?;
1114 }
1115
1116 if to_fix.contains(MessageConformance::MISSING_DATE_HEADER) {
1117 msg.headers_mut().set_date(Utc::now())?;
1118 }
1119
1120 if to_fix.contains(MessageConformance::MISSING_MIME_VERSION) {
1121 msg.headers_mut().set_mime_version("1.0")?;
1122 }
1123
1124 if to_fix.contains(MessageConformance::MISSING_MESSAGE_ID_HEADER) {
1125 if let Some(message_id) = &settings.message_id {
1126 msg.headers_mut()
1127 .set_message_id(MessageID(message_id.clone()))?;
1128 }
1129 }
1130
1131 Ok(Some(msg))
1132 }
1133}
1134
1135#[derive(Default, Debug, Clone, Deserialize)]
1136pub struct CheckFixSettings {
1137 #[serde(default)]
1138 pub detect_encoding: bool,
1139 #[serde(default)]
1140 pub include_encodings: Vec<String>,
1141 #[serde(default)]
1142 pub exclude_encodings: Vec<String>,
1143 #[serde(default)]
1144 pub message_id: Option<String>,
1145 #[serde(skip)]
1146 pub data_bytes: Option<Arc<Box<[u8]>>>,
1147}
1148
1149#[derive(Debug, Clone, PartialEq, Eq)]
1156pub struct PartPointer(Vec<u8>);
1157
1158impl PartPointer {
1159 pub fn root() -> Self {
1161 Self(vec![])
1162 }
1163
1164 pub fn root_or_nth(n: Option<u8>) -> Self {
1167 match n {
1168 Some(n) => Self::nth(n),
1169 None => Self::root(),
1170 }
1171 }
1172
1173 pub fn nth(n: u8) -> Self {
1175 Self(vec![n])
1176 }
1177
1178 pub fn append(mut self, mut other: Self) -> Self {
1181 self.0.append(&mut other.0);
1182 Self(self.0)
1183 }
1184
1185 pub fn id_string(&self) -> String {
1186 let mut id = String::new();
1187 for p in &self.0 {
1188 if !id.is_empty() {
1189 id.push('.');
1190 }
1191 id.push_str(&p.to_string());
1192 }
1193 id
1194 }
1195}
1196
1197#[derive(Debug, Clone)]
1198pub struct SimplifiedStructurePointers {
1199 pub text_part: Option<PartPointer>,
1201 pub html_part: Option<PartPointer>,
1203 pub amp_html_part: Option<PartPointer>,
1205 pub header_part: PartPointer,
1207 pub attachments: Vec<PartPointer>,
1209}
1210
1211#[derive(Debug, Clone)]
1212pub struct SimplifiedStructure<'a> {
1213 pub text: Option<SharedString<'a>>,
1214 pub html: Option<SharedString<'a>>,
1215 pub amp_html: Option<SharedString<'a>>,
1216 pub headers: &'a HeaderMap<'a>,
1217 pub attachments: Vec<MimePart<'a>>,
1218}
1219
1220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1221#[serde(deny_unknown_fields)]
1222pub struct AttachmentOptions {
1223 #[serde(default)]
1224 pub file_name: Option<String>,
1225 #[serde(default)]
1226 pub inline: bool,
1227 #[serde(default)]
1228 pub content_id: Option<String>,
1229}
1230
1231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1232pub enum ContentTransferEncoding {
1233 SevenBit,
1234 EightBit,
1235 Binary,
1236 QuotedPrintable,
1237 Base64,
1238}
1239
1240impl FromStr for ContentTransferEncoding {
1241 type Err = MailParsingError;
1242
1243 fn from_str(s: &str) -> Result<Self> {
1244 if s.eq_ignore_ascii_case("7bit") {
1245 Ok(Self::SevenBit)
1246 } else if s.eq_ignore_ascii_case("8bit") {
1247 Ok(Self::EightBit)
1248 } else if s.eq_ignore_ascii_case("binary") {
1249 Ok(Self::Binary)
1250 } else if s.eq_ignore_ascii_case("quoted-printable") {
1251 Ok(Self::QuotedPrintable)
1252 } else if s.eq_ignore_ascii_case("base64") {
1253 Ok(Self::Base64)
1254 } else {
1255 Err(MailParsingError::InvalidContentTransferEncoding(
1256 s.to_string(),
1257 ))
1258 }
1259 }
1260}
1261
1262#[derive(Debug, PartialEq)]
1263pub enum DecodedBody<'a> {
1264 Text(SharedString<'a>),
1265 Binary(Vec<u8>),
1266}
1267
1268impl<'a> DecodedBody<'a> {
1269 pub fn to_string_lossy(&'a self) -> Cow<'a, str> {
1270 match self {
1271 Self::Text(s) => Cow::Borrowed(s),
1272 Self::Binary(b) => String::from_utf8_lossy(b),
1273 }
1274 }
1275}
1276
1277#[cfg(test)]
1278mod test {
1279 use super::*;
1280
1281 #[test]
1282 fn msg_parsing() {
1283 let message = concat!(
1284 "Subject: hello there\n",
1285 "From: Someone <someone@example.com>\n",
1286 "\n",
1287 "I am the body"
1288 );
1289
1290 let part = MimePart::parse(message).unwrap();
1291 k9::assert_equal!(message, part.to_message_string());
1292 assert_eq!(part.raw_body(), "I am the body");
1293 k9::snapshot!(
1294 part.body(),
1295 r#"
1296Ok(
1297 Text(
1298 "I am the body",
1299 ),
1300)
1301"#
1302 );
1303
1304 k9::snapshot!(
1305 part.rebuild(None).unwrap().to_message_string(),
1306 r#"
1307Content-Type: text/plain;\r
1308\tcharset="us-ascii"\r
1309Subject: hello there\r
1310From: Someone <someone@example.com>\r
1311\r
1312I am the body\r
1313
1314"#
1315 );
1316 }
1317
1318 #[test]
1319 fn mime_bogus_body() {
1320 let message = concat!(
1321 "Subject: hello there\n",
1322 "From: Someone <someone@example.com>\n",
1323 "Mime-Version: 1.0\n",
1324 "Content-Type: text/plain\n",
1325 "Content-Transfer-Encoding: base64\n",
1326 "\n",
1327 "hello\n"
1328 );
1329
1330 let part = MimePart::parse(message).unwrap();
1331 assert_eq!(
1332 part.body().unwrap_err(),
1333 MailParsingError::BodyParse(
1334 "base64 decode: invalid length at 4 b='o' in hello\n".to_string()
1335 )
1336 );
1337 }
1338
1339 #[test]
1340 fn mime_encoded_body() {
1341 let message = concat!(
1342 "Subject: hello there\n",
1343 "From: Someone <someone@example.com>\n",
1344 "Mime-Version: 1.0\n",
1345 "Content-Type: text/plain\n",
1346 "Content-Transfer-Encoding: base64\n",
1347 "\n",
1348 "aGVsbG8K\n"
1349 );
1350
1351 let part = MimePart::parse(message).unwrap();
1352 k9::assert_equal!(message, part.to_message_string());
1353 assert_eq!(part.raw_body(), "aGVsbG8K\n");
1354 k9::snapshot!(
1355 part.body(),
1356 r#"
1357Ok(
1358 Text(
1359 "hello
1360",
1361 ),
1362)
1363"#
1364 );
1365
1366 k9::snapshot!(
1367 part.rebuild(None).unwrap().to_message_string(),
1368 r#"
1369Content-Type: text/plain;\r
1370\tcharset="us-ascii"\r
1371Content-Transfer-Encoding: quoted-printable\r
1372Subject: hello there\r
1373From: Someone <someone@example.com>\r
1374Mime-Version: 1.0\r
1375\r
1376hello=0A\r
1377
1378"#
1379 );
1380 }
1381
1382 #[test]
1383 fn mime_multipart_1() {
1384 let message = concat!(
1385 "Subject: This is a test email\n",
1386 "Content-Type: multipart/alternative; boundary=foobar\n",
1387 "Mime-Version: 1.0\n",
1388 "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\n",
1389 "\n",
1390 "--foobar\n",
1391 "Content-Type: text/plain; charset=utf-8\n",
1392 "Content-Transfer-Encoding: quoted-printable\n",
1393 "\n",
1394 "This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\n",
1395 "--foobar\n",
1396 "Content-Type: text/html\n",
1397 "Content-Transfer-Encoding: base64\n",
1398 "\n",
1399 "PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \n",
1400 "dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \n",
1401 "--foobar--\n",
1402 "After the final boundary stuff gets ignored.\n"
1403 );
1404
1405 let part = MimePart::parse(message).unwrap();
1406
1407 k9::assert_equal!(message, part.to_message_string());
1408
1409 let children = part.child_parts();
1410 k9::assert_equal!(children.len(), 2);
1411
1412 k9::snapshot!(
1413 children[0].body(),
1414 r#"
1415Ok(
1416 Text(
1417 "This is the plaintext version, in utf-8. Proof by Euro: €\r
1418",
1419 ),
1420)
1421"#
1422 );
1423 k9::snapshot!(
1424 children[1].body(),
1425 r#"
1426Ok(
1427 Text(
1428 "<html><body>This is the <b>HTML</b> version, in us-ascii. Proof by Euro: €</body></html>
1429",
1430 ),
1431)
1432"#
1433 );
1434 }
1435
1436 #[test]
1437 fn mutate_1() {
1438 let message = concat!(
1439 "Subject: This is a test email\r\n",
1440 "Content-Type: multipart/alternative; boundary=foobar\r\n",
1441 "Mime-Version: 1.0\r\n",
1442 "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\r\n",
1443 "\r\n",
1444 "--foobar\r\n",
1445 "Content-Type: text/plain; charset=utf-8\r\n",
1446 "Content-Transfer-Encoding: quoted-printable\r\n",
1447 "\r\n",
1448 "This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\r\n",
1449 "--foobar\r\n",
1450 "Content-Type: text/html\r\n",
1451 "Content-Transfer-Encoding: base64\r\n",
1452 "\r\n",
1453 "PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \r\n",
1454 "dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \r\n",
1455 "--foobar--\r\n",
1456 "After the final boundary stuff gets ignored.\r\n"
1457 );
1458
1459 let mut part = MimePart::parse(message).unwrap();
1460 k9::assert_equal!(message, part.to_message_string());
1461 fn munge(part: &mut MimePart) {
1462 let headers = part.headers_mut();
1463 headers.push(Header::with_name_value("X-Woot", "Hello"));
1464 headers.insert(0, Header::with_name_value("X-First", "at the top"));
1465 headers.retain(|hdr| !hdr.get_name().eq_ignore_ascii_case("date"));
1466 }
1467 munge(&mut part);
1468
1469 let re_encoded = part.to_message_string();
1470 k9::snapshot!(
1471 re_encoded,
1472 r#"
1473X-First: at the top\r
1474Subject: This is a test email\r
1475Content-Type: multipart/alternative; boundary=foobar\r
1476Mime-Version: 1.0\r
1477X-Woot: Hello\r
1478\r
1479--foobar\r
1480Content-Type: text/plain; charset=utf-8\r
1481Content-Transfer-Encoding: quoted-printable\r
1482\r
1483This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\r
1484--foobar\r
1485Content-Type: text/html\r
1486Content-Transfer-Encoding: base64\r
1487\r
1488PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \r
1489dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \r
1490--foobar--\r
1491After the final boundary stuff gets ignored.\r
1492
1493"#
1494 );
1495
1496 eprintln!("part before mutate:\n{part:#?}");
1497
1498 part.child_parts_mut().retain(|part| {
1499 let ct = part.headers().content_type().unwrap().unwrap();
1500 ct.value == "text/html"
1501 });
1502
1503 eprintln!("part with html removed is:\n{part:#?}");
1504
1505 let re_encoded = part.to_message_string();
1506 k9::snapshot!(
1507 re_encoded,
1508 r#"
1509X-First: at the top\r
1510Subject: This is a test email\r
1511Content-Type: multipart/alternative; boundary=foobar\r
1512Mime-Version: 1.0\r
1513X-Woot: Hello\r
1514\r
1515--foobar\r
1516Content-Type: text/html\r
1517Content-Transfer-Encoding: base64\r
1518\r
1519PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \r
1520dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \r
1521--foobar--\r
1522After the final boundary stuff gets ignored.\r
1523
1524"#
1525 );
1526 }
1527
1528 #[test]
1529 fn replace_text_body() {
1530 let mut part = MimePart::new_text_plain("Hello 👻\r\n").unwrap();
1531 let encoded = part.to_message_string();
1532 k9::snapshot!(
1533 &encoded,
1534 r#"
1535Content-Type: text/plain;\r
1536\tcharset="utf-8"\r
1537Content-Transfer-Encoding: base64\r
1538\r
1539SGVsbG8g8J+Ruw0K\r
1540
1541"#
1542 );
1543
1544 part.replace_text_body("text/plain", "Hello 🚀\r\n")
1545 .unwrap();
1546 let encoded = part.to_message_string();
1547 k9::snapshot!(
1548 &encoded,
1549 r#"
1550Content-Type: text/plain;\r
1551\tcharset="utf-8"\r
1552Content-Transfer-Encoding: base64\r
1553\r
1554SGVsbG8g8J+agA0K\r
1555
1556"#
1557 );
1558 }
1559
1560 #[test]
1561 fn construct_1() {
1562 let input_text = "Well, hello there! This is the plaintext version, in utf-8. Here's a Euro: €, and here are some emoji 👻 🍉 💩 and this long should be long enough that we wrap it in the returned part, let's see how that turns out!\r\n";
1563
1564 let part = MimePart::new_text_plain(input_text).unwrap();
1565
1566 let encoded = part.to_message_string();
1567 k9::snapshot!(
1568 &encoded,
1569 r#"
1570Content-Type: text/plain;\r
1571\tcharset="utf-8"\r
1572Content-Transfer-Encoding: quoted-printable\r
1573\r
1574Well, hello there! This is the plaintext version, in utf-8. Here's a Euro: =\r
1575=E2=82=AC, and here are some emoji =F0=9F=91=BB =F0=9F=8D=89 =F0=9F=92=A9 a=\r
1576nd this long should be long enough that we wrap it in the returned part, le=\r
1577t's see how that turns out!\r
1578
1579"#
1580 );
1581
1582 let parsed_part = MimePart::parse(encoded.clone()).unwrap();
1583 k9::assert_equal!(encoded.as_str(), parsed_part.to_message_string().as_str());
1584 k9::assert_equal!(part.body().unwrap(), DecodedBody::Text(input_text.into()));
1585 k9::snapshot!(
1586 parsed_part.simplified_structure_pointers(),
1587 "
1588Ok(
1589 SimplifiedStructurePointers {
1590 text_part: Some(
1591 PartPointer(
1592 [],
1593 ),
1594 ),
1595 html_part: None,
1596 amp_html_part: None,
1597 header_part: PartPointer(
1598 [],
1599 ),
1600 attachments: [],
1601 },
1602)
1603"
1604 );
1605 }
1606
1607 #[test]
1608 fn construct_2() {
1609 let msg = MimePart::new_multipart(
1610 "multipart/mixed",
1611 vec![
1612 MimePart::new_text_plain("plain text").unwrap(),
1613 MimePart::new_html("<b>rich</b> text").unwrap(),
1614 MimePart::new_binary(
1615 "application/octet-stream",
1616 &[0, 1, 2, 3],
1617 Some(&AttachmentOptions {
1618 file_name: Some("woot.bin".to_string()),
1619 inline: false,
1620 content_id: Some("woot.id".to_string()),
1621 }),
1622 )
1623 .unwrap(),
1624 ],
1625 Some("my-boundary"),
1626 )
1627 .unwrap();
1628 k9::snapshot!(
1629 msg.to_message_string(),
1630 r#"
1631Content-Type: multipart/mixed;\r
1632\tboundary="my-boundary"\r
1633\r
1634--my-boundary\r
1635Content-Type: text/plain;\r
1636\tcharset="us-ascii"\r
1637\r
1638plain text\r
1639--my-boundary\r
1640Content-Type: text/html;\r
1641\tcharset="us-ascii"\r
1642\r
1643<b>rich</b> text\r
1644--my-boundary\r
1645Content-Disposition: attachment;\r
1646\tfilename="woot.bin"\r
1647Content-ID: <woot.id>\r
1648Content-Type: application/octet-stream;\r
1649\tname="woot.bin"\r
1650Content-Transfer-Encoding: base64\r
1651\r
1652AAECAw==\r
1653--my-boundary--\r
1654
1655"#
1656 );
1657
1658 k9::snapshot!(
1659 msg.simplified_structure_pointers(),
1660 "
1661Ok(
1662 SimplifiedStructurePointers {
1663 text_part: Some(
1664 PartPointer(
1665 [
1666 0,
1667 ],
1668 ),
1669 ),
1670 html_part: Some(
1671 PartPointer(
1672 [
1673 1,
1674 ],
1675 ),
1676 ),
1677 amp_html_part: None,
1678 header_part: PartPointer(
1679 [],
1680 ),
1681 attachments: [
1682 PartPointer(
1683 [
1684 2,
1685 ],
1686 ),
1687 ],
1688 },
1689)
1690"
1691 );
1692 }
1693
1694 #[test]
1695 fn attachment_name_order_prefers_content_disposition() {
1696 let message = concat!(
1697 "Content-Type: multipart/mixed;\r\n",
1698 " boundary=\"woot\"\r\n",
1699 "\r\n",
1700 "--woot\r\n",
1701 "Content-Type: text/plain;\r\n",
1702 " charset=\"us-ascii\"\r\n",
1703 "\r\n",
1704 "Hello, I am the main message content\r\n",
1705 "--woot\r\n",
1706 "Content-Disposition: attachment;\r\n",
1707 " filename=cdname\r\n",
1708 "Content-Type: application/octet-stream;\r\n",
1709 " name=ctname\r\n",
1710 "Content-Transfer-Encoding: base64\r\n",
1711 "\r\n",
1712 "u6o=\r\n",
1713 "--woot--\r\n"
1714 );
1715 let part = MimePart::parse(message).unwrap();
1716 let structure = part.simplified_structure().unwrap();
1717
1718 k9::assert_equal!(
1719 structure.attachments[0].rfc2045_info().attachment_options,
1720 Some(AttachmentOptions {
1721 content_id: None,
1722 inline: false,
1723 file_name: Some("cdname".to_string()),
1724 })
1725 );
1726 }
1727
1728 #[test]
1729 fn attachment_name_accepts_content_type_name() {
1730 let message = concat!(
1731 "Content-Type: multipart/mixed;\r\n",
1732 " boundary=\"woot\"\r\n",
1733 "\r\n",
1734 "--woot\r\n",
1735 "Content-Type: text/plain;\r\n",
1736 " charset=\"us-ascii\"\r\n",
1737 "\r\n",
1738 "Hello, I am the main message content\r\n",
1739 "--woot\r\n",
1740 "Content-Disposition: attachment\r\n",
1741 "Content-Type: application/octet-stream;\r\n",
1742 " name=ctname\r\n",
1743 "Content-Transfer-Encoding: base64\r\n",
1744 "\r\n",
1745 "u6o=\r\n",
1746 "--woot--\r\n"
1747 );
1748 let part = MimePart::parse(message).unwrap();
1749 let structure = part.simplified_structure().unwrap();
1750
1751 k9::assert_equal!(
1752 structure.attachments[0].rfc2045_info().attachment_options,
1753 Some(AttachmentOptions {
1754 content_id: None,
1755 inline: false,
1756 file_name: Some("ctname".to_string()),
1757 })
1758 );
1759 }
1760
1761 #[test]
1762 fn funky_headers() {
1763 let message = concat!(
1764 "Subject\r\n",
1765 "Other:\r\n",
1766 "Content-Type: multipart/alternative; boundary=foobar\r\n",
1767 "Mime-Version: 1.0\r\n",
1768 "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\r\n",
1769 "\r\n",
1770 "The body.\r\n"
1771 );
1772
1773 let part = MimePart::parse(message).unwrap();
1774 assert!(part
1775 .conformance()
1776 .contains(MessageConformance::MISSING_COLON_VALUE));
1777 }
1778
1779 #[test]
1784 fn rebuild_binary() {
1785 let expect = &[0, 1, 2, 3, 0xbe, 4, 5];
1786 let part = MimePart::new_binary("applicat/octet-stream", expect, None).unwrap();
1787
1788 let rebuilt = part.rebuild(None).unwrap();
1789 let body = rebuilt.body().unwrap();
1790
1791 assert_eq!(body, DecodedBody::Binary(expect.to_vec()));
1792 }
1793
1794 #[test]
1797 fn rebuild_invitation() {
1798 let message = concat!(
1799 "Subject: Test for events 2\r\n",
1800 "Content-Type: multipart/mixed;\r\n",
1801 " boundary=8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3\r\n",
1802 "\r\n",
1803 "--8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3\r\n",
1804 "Content-Type: multipart/alternative;\r\n",
1805 " boundary=a4e0aff9e05c7d94e2e13bd5590302f7802daac1e952c065207790d15a9f\r\n",
1806 "\r\n",
1807 "--a4e0aff9e05c7d94e2e13bd5590302f7802daac1e952c065207790d15a9f\r\n",
1808 "Content-Transfer-Encoding: quoted-printable\r\n",
1809 "Content-Type: text/plain; charset=UTF-8\r\n",
1810 "\r\n",
1811 "This is a test for calendar event invitation\r\n",
1812 "--a4e0aff9e05c7d94e2e13bd5590302f7802daac1e952c065207790d15a9f\r\n",
1813 "Content-Transfer-Encoding: quoted-printable\r\n",
1814 "Content-Type: text/html; charset=UTF-8\r\n",
1815 "\r\n",
1816 "<p>This is a test for calendar event invitation</p>\r\n",
1817 "--a4e0aff9e05c7d94e2e13bd5590302f7802daac1e952c065207790d15a9f--\r\n",
1818 "\r\n",
1819 "--8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3\r\n",
1820 "Content-Disposition: inline; name=\"Invitation.ics\"\r\n",
1821 "Content-Type: text/calendar; method=REQUEST; name=\"Invitation.ics\"\r\n",
1822 "\r\n",
1823 "Invitation\r\n",
1824 "--8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3\r\n",
1825 "Content-Disposition: attachment; filename=\"event.ics\"\r\n",
1826 "Content-Type: application/ics\r\n",
1827 "\r\n",
1828 "Event\r\n",
1829 "--8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3--\r\n",
1830 "\r\n"
1831 );
1832
1833 let part = MimePart::parse(message).unwrap();
1834 let rebuilt = part.rebuild(None).unwrap();
1835
1836 k9::snapshot!(
1837 rebuilt.to_message_string(),
1838 r#"
1839Content-Type: multipart/mixed;\r
1840\tboundary="8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3"\r
1841Subject: Test for events 2\r
1842\r
1843--8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3\r
1844Content-Type: multipart/alternative;\r
1845\tboundary="a4e0aff9e05c7d94e2e13bd5590302f7802daac1e952c065207790d15a9f"\r
1846\r
1847--a4e0aff9e05c7d94e2e13bd5590302f7802daac1e952c065207790d15a9f\r
1848Content-Type: text/plain;\r
1849\tcharset="us-ascii"\r
1850\r
1851This is a test for calendar event invitation\r
1852--a4e0aff9e05c7d94e2e13bd5590302f7802daac1e952c065207790d15a9f\r
1853Content-Type: text/html;\r
1854\tcharset="us-ascii"\r
1855\r
1856<p>This is a test for calendar event invitation</p>\r
1857--a4e0aff9e05c7d94e2e13bd5590302f7802daac1e952c065207790d15a9f--\r
1858--8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3\r
1859Content-Type: text/calendar;\r
1860\tcharset="us-ascii";\r
1861\tmethod="REQUEST";\r
1862\tname="Invitation.ics"\r
1863\r
1864Invitation\r
1865--8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3\r
1866Content-Disposition: attachment;\r
1867\tfilename="event.ics"\r
1868Content-Type: application/ics;\r
1869\tname="event.ics"\r
1870Content-Transfer-Encoding: base64\r
1871\r
1872RXZlbnQNCg==\r
1873--8a54d64d7ad7c04a084478052b36cbe1609b33bf3a41203aaee8dd642cd3--\r
1874
1875"#
1876 );
1877 }
1878
1879 #[test]
1880 fn check_conformance_angle_msg_id() {
1881 const DOUBLE_ANGLE_ONLY: &str = "Subject: hello\r
1882Message-ID: <<1234@example.com>>\r
1883\r
1884Hello";
1885 let msg = MimePart::parse(DOUBLE_ANGLE_ONLY).unwrap();
1886 k9::snapshot!(
1887 msg.check_fix_conformance(
1888 MessageConformance::MISSING_MESSAGE_ID_HEADER,
1889 MessageConformance::empty(),
1890 CheckFixSettings::default(),
1891 )
1892 .unwrap_err()
1893 .to_string(),
1894 "Message has conformance issues: MISSING_MESSAGE_ID_HEADER"
1895 );
1896
1897 let rebuilt = msg
1898 .check_fix_conformance(
1899 MessageConformance::MISSING_MESSAGE_ID_HEADER,
1900 MessageConformance::MISSING_MESSAGE_ID_HEADER,
1901 CheckFixSettings {
1902 message_id: Some("id@example.com".to_string()),
1903 ..Default::default()
1904 },
1905 )
1906 .unwrap()
1907 .unwrap()
1908 .to_message_string();
1909
1910 k9::snapshot!(
1911 rebuilt,
1912 r#"
1913Subject: hello\r
1914Message-ID: <id@example.com>\r
1915\r
1916Hello
1917"#
1918 );
1919
1920 const DOUBLE_ANGLE_AND_LONG_LINE: &str = "Subject: hello\r
1921Message-ID: <<1234@example.com>>\r
1922\r
1923Hello this is a really long line Hello this is a really long line \
1924Hello this is a really long line Hello this is a really long line \
1925Hello this is a really long line Hello this is a really long line \
1926Hello this is a really long line Hello this is a really long line \
1927Hello this is a really long line Hello this is a really long line \
1928Hello this is a really long line Hello this is a really long line \
1929Hello this is a really long line Hello this is a really long line
1930";
1931 let msg = MimePart::parse(DOUBLE_ANGLE_AND_LONG_LINE).unwrap();
1932 let rebuilt = msg
1933 .check_fix_conformance(
1934 MessageConformance::MISSING_COLON_VALUE,
1935 MessageConformance::MISSING_MESSAGE_ID_HEADER | MessageConformance::LINE_TOO_LONG,
1936 CheckFixSettings {
1937 message_id: Some("id@example.com".to_string()),
1938 ..Default::default()
1939 },
1940 )
1941 .unwrap()
1942 .unwrap()
1943 .to_message_string();
1944
1945 k9::snapshot!(
1946 rebuilt,
1947 r#"
1948Content-Type: text/plain;\r
1949\tcharset="us-ascii"\r
1950Content-Transfer-Encoding: quoted-printable\r
1951Subject: hello\r
1952Message-ID: <id@example.com>\r
1953\r
1954Hello this is a really long line Hello this is a really long line Hello thi=\r
1955s is a really long line Hello this is a really long line Hello this is a re=\r
1956ally long line Hello this is a really long line Hello this is a really long=\r
1957 line Hello this is a really long line Hello this is a really long line Hel=\r
1958lo this is a really long line Hello this is a really long line Hello this i=\r
1959s a really long line Hello this is a really long line Hello this is a reall=\r
1960y long line=0A\r
1961
1962"#
1963 );
1964 }
1965
1966 #[test]
1967 fn check_conformance() {
1968 const MULTI_HEADER_CONTENT: &str =
1969 "X-Hello: there\r\nX-Header: value\r\nSubject: Hello\r\nX-Header: another value\r\nFrom :Someone@somewhere\r\n\r\nBody";
1970
1971 let msg = MimePart::parse(MULTI_HEADER_CONTENT).unwrap();
1972 let rebuilt = msg
1973 .check_fix_conformance(
1974 MessageConformance::default(),
1975 MessageConformance::MISSING_MIME_VERSION,
1976 CheckFixSettings::default(),
1977 )
1978 .unwrap()
1979 .unwrap()
1980 .to_message_string();
1981 k9::snapshot!(
1982 rebuilt,
1983 r#"
1984X-Hello: there\r
1985X-Header: value\r
1986Subject: Hello\r
1987X-Header: another value\r
1988From :Someone@somewhere\r
1989Mime-Version: 1.0\r
1990\r
1991Body
1992"#
1993 );
1994
1995 let msg = MimePart::parse(MULTI_HEADER_CONTENT).unwrap();
1996 let rebuilt = msg
1997 .check_fix_conformance(
1998 MessageConformance::default(),
1999 MessageConformance::MISSING_MIME_VERSION | MessageConformance::NAME_ENDS_WITH_SPACE,
2000 CheckFixSettings::default(),
2001 )
2002 .unwrap()
2003 .unwrap()
2004 .to_message_string();
2005 k9::snapshot!(
2006 rebuilt,
2007 r#"
2008Content-Type: text/plain;\r
2009\tcharset="us-ascii"\r
2010X-Hello: there\r
2011X-Header: value\r
2012Subject: Hello\r
2013X-Header: another value\r
2014From: <Someone@somewhere>\r
2015Mime-Version: 1.0\r
2016\r
2017Body\r
2018
2019"#
2020 );
2021 }
2022
2023 #[test]
2024 fn check_fix_latin_input() {
2025 const POUNDS: &[u8] = b"Subject: \xa3\r\n\r\nGBP\r\n";
2026 let msg = MimePart::parse(POUNDS).unwrap();
2027 assert_eq!(
2028 msg.conformance(),
2029 MessageConformance::NEEDS_TRANSFER_ENCODING
2030 | MessageConformance::MISSING_DATE_HEADER
2031 | MessageConformance::MISSING_MESSAGE_ID_HEADER
2032 | MessageConformance::MISSING_MIME_VERSION
2033 );
2034 let rebuilt = msg
2035 .check_fix_conformance(
2036 MessageConformance::default(),
2037 MessageConformance::NEEDS_TRANSFER_ENCODING,
2038 CheckFixSettings {
2039 detect_encoding: true,
2040 include_encodings: vec!["iso-8859-1".to_string()],
2041 data_bytes: Some(Arc::new(POUNDS.into())),
2042 ..Default::default()
2043 },
2044 )
2045 .unwrap()
2046 .unwrap();
2047
2048 let subject = rebuilt.headers.subject().unwrap().unwrap();
2049 assert_eq!(subject, "£");
2050 }
2051
2052 #[test]
2059 fn check_fix_utf8_inside_transfer_encoding() {
2060 const CONTENT: &str = "Subject: hello\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n2KrYs9iqDQoNCg==\r\n";
2061
2062 let msg = MimePart::parse(CONTENT).unwrap();
2063
2064 assert_eq!(
2067 msg.conformance(),
2068 MessageConformance::MISSING_DATE_HEADER
2069 | MessageConformance::MISSING_MESSAGE_ID_HEADER
2070 | MessageConformance::MISSING_MIME_VERSION
2071 );
2072
2073 assert_eq!(
2075 msg.deep_conformance_check(),
2076 MessageConformance::NEEDS_TRANSFER_ENCODING
2077 | MessageConformance::MISSING_DATE_HEADER
2078 | MessageConformance::MISSING_MESSAGE_ID_HEADER
2079 | MessageConformance::MISSING_MIME_VERSION
2080 );
2081 let rebuilt = msg
2082 .check_fix_conformance(
2083 MessageConformance::default(),
2084 MessageConformance::NEEDS_TRANSFER_ENCODING,
2085 CheckFixSettings::default(),
2086 )
2087 .unwrap()
2088 .unwrap();
2089
2090 eprintln!("{rebuilt:?}");
2091 assert_eq!(rebuilt.body().unwrap().to_string_lossy().trim(), "تست");
2092 }
2093
2094 #[test]
2095 fn check_fix_latin1_inside_transfer_encoding() {
2096 const CONTENT: &str = "Subject: hello\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\nVGhlIGNvc3QgaXMgozQyLjAwCg==\r\n";
2097
2098 let msg = MimePart::parse(CONTENT).unwrap();
2099
2100 assert_eq!(
2103 msg.conformance(),
2104 MessageConformance::MISSING_DATE_HEADER
2105 | MessageConformance::MISSING_MESSAGE_ID_HEADER
2106 | MessageConformance::MISSING_MIME_VERSION
2107 );
2108
2109 assert_eq!(
2111 msg.deep_conformance_check(),
2112 MessageConformance::NEEDS_TRANSFER_ENCODING
2113 | MessageConformance::MISSING_DATE_HEADER
2114 | MessageConformance::MISSING_MESSAGE_ID_HEADER
2115 | MessageConformance::MISSING_MIME_VERSION
2116 );
2117 let rebuilt = msg
2118 .check_fix_conformance(
2119 MessageConformance::default(),
2120 MessageConformance::NEEDS_TRANSFER_ENCODING,
2121 CheckFixSettings {
2122 detect_encoding: true,
2123 include_encodings: vec!["iso-8859-1".to_string()],
2124 ..Default::default()
2125 },
2126 )
2127 .unwrap()
2128 .unwrap();
2129
2130 eprintln!("{rebuilt:?}");
2131 assert_eq!(
2132 rebuilt.body().unwrap().to_string_lossy().trim(),
2133 "The cost is £42.00"
2134 );
2135 }
2136
2137 #[test]
2138 fn check_fix_unknown_inside_transfer_encoding() {
2139 const CONTENT: &str = "Subject: hello\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\nowo=\r\n";
2144
2145 let msg = MimePart::parse(CONTENT).unwrap();
2146
2147 assert_eq!(
2150 msg.conformance(),
2151 MessageConformance::MISSING_DATE_HEADER
2152 | MessageConformance::MISSING_MESSAGE_ID_HEADER
2153 | MessageConformance::MISSING_MIME_VERSION
2154 );
2155
2156 assert_eq!(
2158 msg.deep_conformance_check(),
2159 MessageConformance::NEEDS_TRANSFER_ENCODING
2160 | MessageConformance::MISSING_DATE_HEADER
2161 | MessageConformance::MISSING_MESSAGE_ID_HEADER
2162 | MessageConformance::MISSING_MIME_VERSION
2163 );
2164 let rebuilt = msg
2165 .check_fix_conformance(
2166 MessageConformance::default(),
2167 MessageConformance::NEEDS_TRANSFER_ENCODING,
2168 CheckFixSettings {
2169 detect_encoding: true,
2170 include_encodings: vec!["iso-8859-1".to_string()],
2171 ..Default::default()
2172 },
2173 )
2174 .unwrap()
2175 .unwrap();
2176
2177 eprintln!("{rebuilt:?}");
2178 assert_eq!(rebuilt.body().unwrap().to_string_lossy().trim(), "�");
2179 }
2180}