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::Charset;
9use serde::{Deserialize, Serialize};
10use std::borrow::Cow;
11use std::str::FromStr;
12
13const BASE64_RFC2045: data_encoding::Encoding = data_encoding_macro::new_encoding! {
16 symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
17 padding: '=',
18 ignore: " \r\n\t",
19 wrap_width: 76,
20 wrap_separator: "\r\n",
21};
22
23#[derive(Debug, Clone, PartialEq)]
24pub struct MimePart<'a> {
25 bytes: SharedString<'a>,
27 headers: HeaderMap<'a>,
29 body_offset: usize,
31 body_len: usize,
32 conformance: MessageConformance,
33 parts: Vec<Self>,
34 intro: SharedString<'a>,
36 outro: SharedString<'a>,
38}
39
40#[derive(PartialEq, Debug)]
41pub struct Rfc2045Info {
42 pub encoding: ContentTransferEncoding,
43 pub charset: Result<Charset>,
44 pub content_type: Option<MimeParameters>,
45 pub is_text: bool,
46 pub is_multipart: bool,
47 pub attachment_options: Option<AttachmentOptions>,
48 pub invalid_mime_headers: bool,
49}
50
51impl Rfc2045Info {
52 fn new(headers: &HeaderMap) -> Self {
55 let mut invalid_mime_headers = false;
56 let encoding = match headers.content_transfer_encoding() {
57 Ok(Some(cte)) => match ContentTransferEncoding::from_str(&cte.value) {
58 Ok(encoding) => encoding,
59 Err(_) => {
60 invalid_mime_headers = true;
61 ContentTransferEncoding::SevenBit
62 }
63 },
64 Ok(None) => ContentTransferEncoding::SevenBit,
65 Err(_) => {
66 invalid_mime_headers = true;
67 ContentTransferEncoding::SevenBit
68 }
69 };
70
71 let content_type = match headers.content_type() {
72 Ok(ct) => ct,
73 Err(_) => {
74 invalid_mime_headers = true;
75 None
76 }
77 };
78
79 let mut ct_name = None;
80 let charset = if let Some(ct) = &content_type {
81 ct_name = ct.get("name");
82 ct.get("charset")
83 } else {
84 None
85 };
86 let charset = charset.unwrap_or_else(|| "us-ascii".to_string());
87
88 let charset = Charset::for_label_no_replacement(charset.as_bytes())
89 .ok_or_else(|| MailParsingError::BodyParse(format!("unsupported charset {charset}")));
90
91 let (is_text, is_multipart) = if let Some(ct) = &content_type {
92 (ct.is_text(), ct.is_multipart())
93 } else {
94 (true, false)
95 };
96
97 let mut inline = false;
98 let mut cd_file_name = None;
99
100 match headers.content_disposition() {
101 Ok(Some(cd)) => {
102 inline = cd.value == "inline";
103 cd_file_name = cd.get("filename");
104 }
105 Ok(None) => {}
106 Err(_) => {
107 invalid_mime_headers = true;
108 }
109 };
110
111 let content_id = match headers.content_id() {
112 Ok(cid) => cid.map(|cid| cid.0),
113 Err(_) => {
114 invalid_mime_headers = true;
115 None
116 }
117 };
118
119 let file_name = match (cd_file_name, ct_name) {
120 (Some(name), _) | (None, Some(name)) => Some(name),
121 (None, None) => None,
122 };
123
124 let attachment_options = if inline || file_name.is_some() || content_id.is_some() {
125 Some(AttachmentOptions {
126 file_name,
127 inline,
128 content_id,
129 })
130 } else {
131 None
132 };
133
134 Self {
135 encoding,
136 charset,
137 content_type,
138 is_text,
139 is_multipart,
140 attachment_options,
141 invalid_mime_headers,
142 }
143 }
144}
145
146impl<'a> MimePart<'a> {
147 pub fn parse<S>(bytes: S) -> Result<Self>
149 where
150 S: IntoSharedString<'a>,
151 {
152 let (bytes, base_conformance) = bytes.into_shared_string();
153 Self::parse_impl(bytes, base_conformance, true)
154 }
155
156 pub fn to_owned(&self) -> MimePart<'static> {
158 MimePart {
159 bytes: self.bytes.to_owned(),
160 headers: self.headers.to_owned(),
161 body_offset: self.body_offset,
162 body_len: self.body_len,
163 conformance: self.conformance,
164 parts: self.parts.iter().map(|p| p.to_owned()).collect(),
165 intro: self.intro.to_owned(),
166 outro: self.outro.to_owned(),
167 }
168 }
169
170 fn parse_impl(
171 bytes: SharedString<'a>,
172 base_conformance: MessageConformance,
173 is_top_level: bool,
174 ) -> Result<Self> {
175 let HeaderParseResult {
176 headers,
177 body_offset,
178 overall_conformance: mut conformance,
179 } = Header::parse_headers(bytes.clone())?;
180
181 conformance |= base_conformance;
182
183 let body_len = bytes.len();
184
185 if !bytes.is_ascii() {
186 conformance.set(MessageConformance::NEEDS_TRANSFER_ENCODING, true);
187 }
188 {
189 let mut prev = 0;
190 for idx in memchr::memchr_iter(b'\n', bytes.as_bytes()) {
191 if idx - prev > 78 {
192 conformance.set(MessageConformance::LINE_TOO_LONG, true);
193 break;
194 }
195 prev = idx;
196 }
197 }
198 conformance.set(
199 MessageConformance::NON_CANONICAL_LINE_ENDINGS,
200 has_lone_cr_or_lf(bytes.as_bytes()),
201 );
202
203 if is_top_level {
204 conformance.set(
205 MessageConformance::MISSING_DATE_HEADER,
206 !matches!(headers.date(), Ok(Some(_))),
207 );
208 conformance.set(
209 MessageConformance::MISSING_MESSAGE_ID_HEADER,
210 !matches!(headers.message_id(), Ok(Some(_))),
211 );
212 conformance.set(
213 MessageConformance::MISSING_MIME_VERSION,
214 match headers.mime_version() {
215 Ok(Some(v)) => v.as_str() != "1.0",
216 _ => true,
217 },
218 );
219 }
220
221 let mut part = Self {
222 bytes,
223 headers,
224 body_offset,
225 body_len,
226 conformance,
227 parts: vec![],
228 intro: SharedString::Borrowed(""),
229 outro: SharedString::Borrowed(""),
230 };
231
232 part.recursive_parse()?;
233
234 Ok(part)
235 }
236
237 fn recursive_parse(&mut self) -> Result<()> {
238 let info = Rfc2045Info::new(&self.headers);
239 if info.invalid_mime_headers {
240 self.conformance |= MessageConformance::INVALID_MIME_HEADERS;
241 }
242 if let Some((boundary, true)) = info
243 .content_type
244 .as_ref()
245 .and_then(|ct| ct.get("boundary").map(|b| (b, info.is_multipart)))
246 {
247 let boundary = format!("\n--{boundary}");
248 let raw_body = self
249 .bytes
250 .slice(self.body_offset.saturating_sub(1)..self.bytes.len());
251
252 let mut iter = memchr::memmem::find_iter(raw_body.as_bytes(), &boundary);
253 if let Some(first_boundary_pos) = iter.next() {
254 self.intro = raw_body.slice(0..first_boundary_pos);
255
256 self.body_len = 0;
259
260 let mut boundary_end = first_boundary_pos + boundary.len();
261
262 while let Some(part_start) =
263 memchr::memchr(b'\n', &raw_body.as_bytes()[boundary_end..])
264 .map(|p| p + boundary_end + 1)
265 {
266 let part_end = iter
267 .next()
268 .map(|p| {
269 p + 1
272 })
273 .unwrap_or(raw_body.len());
274
275 let child = Self::parse_impl(
276 raw_body.slice(part_start..part_end),
277 MessageConformance::default(),
278 false,
279 )?;
280 self.conformance |= child.conformance;
281 self.parts.push(child);
282
283 boundary_end = part_end -
284 1 + boundary.len();
286
287 if boundary_end + 2 > raw_body.len() {
288 break;
289 }
290 if &raw_body.as_bytes()[boundary_end..boundary_end + 2] == b"--" {
291 if let Some(after_boundary) =
292 memchr::memchr(b'\n', &raw_body.as_bytes()[boundary_end..])
293 .map(|p| p + boundary_end + 1)
294 {
295 self.outro = raw_body.slice(after_boundary..raw_body.len());
296 }
297 break;
298 }
299 }
300 }
301 }
302
303 Ok(())
304 }
305
306 pub fn conformance(&self) -> MessageConformance {
307 self.conformance
308 }
309
310 pub fn child_parts(&self) -> &[Self] {
312 &self.parts
313 }
314
315 pub fn child_parts_mut(&mut self) -> &mut Vec<Self> {
317 &mut self.parts
318 }
319
320 pub fn headers(&'_ self) -> &'_ HeaderMap<'_> {
322 &self.headers
323 }
324
325 pub fn headers_mut<'b>(&'b mut self) -> &'b mut HeaderMap<'a> {
327 &mut self.headers
328 }
329
330 pub fn raw_body(&'_ self) -> SharedString<'_> {
332 self.bytes
333 .slice(self.body_offset..self.body_len.max(self.body_offset))
334 }
335
336 pub fn rfc2045_info(&self) -> Rfc2045Info {
337 Rfc2045Info::new(&self.headers)
338 }
339
340 pub fn body(&'_ self) -> Result<DecodedBody<'_>> {
342 let info = Rfc2045Info::new(&self.headers);
343
344 let bytes = match info.encoding {
345 ContentTransferEncoding::Base64 => {
346 let data = self.raw_body();
347 let bytes = data.as_bytes();
348 BASE64_RFC2045.decode(bytes).map_err(|err| {
349 let b = bytes[err.position] as char;
350 let region =
351 &bytes[err.position.saturating_sub(8)..(err.position + 8).min(bytes.len())];
352 let region = String::from_utf8_lossy(region);
353 MailParsingError::BodyParse(format!(
354 "base64 decode: {err:#} b={b:?} in {region}"
355 ))
356 })?
357 }
358 ContentTransferEncoding::QuotedPrintable => quoted_printable::decode(
359 self.raw_body().as_bytes(),
360 quoted_printable::ParseMode::Robust,
361 )
362 .map_err(|err| {
363 MailParsingError::BodyParse(format!("quoted printable decode: {err:#}"))
364 })?,
365 ContentTransferEncoding::SevenBit
366 | ContentTransferEncoding::EightBit
367 | ContentTransferEncoding::Binary
368 if info.is_text =>
369 {
370 return Ok(DecodedBody::Text(self.raw_body()));
371 }
372 ContentTransferEncoding::SevenBit
373 | ContentTransferEncoding::EightBit
374 | ContentTransferEncoding::Binary => {
375 return Ok(DecodedBody::Binary(self.raw_body().as_bytes().to_vec()))
376 }
377 };
378
379 if info.is_text {
380 let (decoded, _malformed) = info.charset?.decode_without_bom_handling(&bytes);
381 Ok(DecodedBody::Text(decoded.to_string().into()))
382 } else {
383 Ok(DecodedBody::Binary(bytes))
384 }
385 }
386
387 pub fn rebuild(&self) -> Result<Self> {
394 let info = Rfc2045Info::new(&self.headers);
395
396 let mut children = vec![];
397 for part in &self.parts {
398 children.push(part.rebuild()?);
399 }
400
401 let mut rebuilt = if children.is_empty() {
402 let body = self.body()?;
403 match body {
404 DecodedBody::Text(text) => {
405 let ct = info
406 .content_type
407 .as_ref()
408 .map(|ct| ct.value.as_str())
409 .unwrap_or("text/plain");
410 Self::new_text(ct, text.as_str())?
411 }
412 DecodedBody::Binary(data) => {
413 let ct = info
414 .content_type
415 .as_ref()
416 .map(|ct| ct.value.as_str())
417 .unwrap_or("application/octet-stream");
418 Self::new_binary(ct, &data, info.attachment_options.as_ref())?
419 }
420 }
421 } else {
422 let ct = info.content_type.ok_or_else(|| {
423 MailParsingError::BodyParse(
424 "multipart message has no content-type information!?".to_string(),
425 )
426 })?;
427 Self::new_multipart(&ct.value, children, ct.get("boundary").as_deref())?
428 };
429
430 for hdr in self.headers.iter() {
431 let name = hdr.get_name();
434 if name.eq_ignore_ascii_case("Content-Type")
435 || name.eq_ignore_ascii_case("Content-Transfer-Encoding")
436 || name.eq_ignore_ascii_case("Content-Disposition")
437 || name.eq_ignore_ascii_case("Content-ID")
438 {
439 continue;
440 }
441
442 if let Ok(hdr) = hdr.rebuild() {
443 rebuilt.headers_mut().push(hdr);
444 }
445 }
446
447 Ok(rebuilt)
448 }
449
450 pub fn write_message<W: std::io::Write>(&self, out: &mut W) -> Result<()> {
452 let line_ending = if self
453 .conformance
454 .contains(MessageConformance::NON_CANONICAL_LINE_ENDINGS)
455 {
456 "\n"
457 } else {
458 "\r\n"
459 };
460
461 for hdr in self.headers.iter() {
462 hdr.write_header(out)
463 .map_err(|_| MailParsingError::WriteMessageIOError)?;
464 }
465 out.write_all(line_ending.as_bytes())
466 .map_err(|_| MailParsingError::WriteMessageIOError)?;
467
468 if self.parts.is_empty() {
469 out.write_all(self.raw_body().as_bytes())
470 .map_err(|_| MailParsingError::WriteMessageIOError)?;
471 } else {
472 let info = Rfc2045Info::new(&self.headers);
473 let ct = info.content_type.ok_or({
474 MailParsingError::WriteMessageWtf(
475 "expected to have Content-Type when there are child parts",
476 )
477 })?;
478 let boundary = ct.get("boundary").ok_or({
479 MailParsingError::WriteMessageWtf("expected Content-Type to have a boundary")
480 })?;
481 out.write_all(self.intro.as_bytes())
482 .map_err(|_| MailParsingError::WriteMessageIOError)?;
483 for p in &self.parts {
484 write!(out, "--{boundary}{line_ending}")
485 .map_err(|_| MailParsingError::WriteMessageIOError)?;
486 p.write_message(out)?;
487 }
488 write!(out, "--{boundary}--{line_ending}")
489 .map_err(|_| MailParsingError::WriteMessageIOError)?;
490 out.write_all(self.outro.as_bytes())
491 .map_err(|_| MailParsingError::WriteMessageIOError)?;
492 }
493 Ok(())
494 }
495
496 pub fn to_message_string(&self) -> String {
499 let mut out = vec![];
500 self.write_message(&mut out).unwrap();
501 String::from_utf8_lossy(&out).to_string()
502 }
503
504 pub fn replace_text_body(&mut self, content_type: &str, content: &str) -> Result<()> {
505 let mut new_part = Self::new_text(content_type, content)?;
506 self.bytes = new_part.bytes;
507 self.body_offset = new_part.body_offset;
508 self.body_len = new_part.body_len;
509 self.headers.remove_all_named("Content-Type");
513 self.headers.remove_all_named("Content-Transfer-Encoding");
514 self.headers.append(&mut new_part.headers.headers);
516 Ok(())
517 }
518
519 pub fn replace_binary_body(&mut self, content_type: &str, content: &[u8]) -> Result<()> {
520 let mut new_part = Self::new_binary(content_type, content, None)?;
521 self.bytes = new_part.bytes;
522 self.body_offset = new_part.body_offset;
523 self.body_len = new_part.body_len;
524 self.headers.remove_all_named("Content-Type");
528 self.headers.remove_all_named("Content-Transfer-Encoding");
529 self.headers.append(&mut new_part.headers.headers);
531 Ok(())
532 }
533
534 pub fn new_no_transfer_encoding(content_type: &str, bytes: &[u8]) -> Result<Self> {
535 if bytes.iter().any(|b| !b.is_ascii()) {
536 return Err(MailParsingError::EightBit);
537 }
538
539 let mut headers = HeaderMap::default();
540
541 let ct = MimeParameters::new(content_type);
542 headers.set_content_type(ct)?;
543
544 let bytes = String::from_utf8_lossy(bytes).to_string();
545 let body_len = bytes.len();
546
547 Ok(Self {
548 bytes: bytes.into(),
549 headers,
550 body_offset: 0,
551 body_len,
552 conformance: MessageConformance::default(),
553 parts: vec![],
554 intro: "".into(),
555 outro: "".into(),
556 })
557 }
558
559 pub fn new_text(content_type: &str, content: &str) -> Result<Self> {
563 let qp_encoded = quoted_printable::encode(content);
565
566 let (mut encoded, encoding) = if qp_encoded == content.as_bytes() {
567 (qp_encoded, None)
568 } else if qp_encoded.len() <= BASE64_RFC2045.encode_len(content.len()) {
569 (qp_encoded, Some("quoted-printable"))
570 } else {
571 (
574 BASE64_RFC2045.encode(content.as_bytes()).into_bytes(),
575 Some("base64"),
576 )
577 };
578
579 if !encoded.ends_with(b"\r\n") {
580 encoded.extend_from_slice(b"\r\n");
581 }
582 let mut headers = HeaderMap::default();
583
584 let mut ct = MimeParameters::new(content_type);
585 ct.set(
586 "charset",
587 if content.is_ascii() {
588 "us-ascii"
589 } else {
590 "utf-8"
591 },
592 );
593 headers.set_content_type(ct)?;
594
595 if let Some(encoding) = encoding {
596 headers.set_content_transfer_encoding(MimeParameters::new(encoding))?;
597 }
598
599 let body_len = encoded.len();
600 let bytes =
601 String::from_utf8(encoded).expect("transfer encoder to produce valid ASCII output");
602
603 Ok(Self {
604 bytes: bytes.into(),
605 headers,
606 body_offset: 0,
607 body_len,
608 conformance: MessageConformance::default(),
609 parts: vec![],
610 intro: "".into(),
611 outro: "".into(),
612 })
613 }
614
615 pub fn new_text_plain(content: &str) -> Result<Self> {
616 Self::new_text("text/plain", content)
617 }
618
619 pub fn new_html(content: &str) -> Result<Self> {
620 Self::new_text("text/html", content)
621 }
622
623 pub fn new_multipart(
624 content_type: &str,
625 parts: Vec<Self>,
626 boundary: Option<&str>,
627 ) -> Result<Self> {
628 let mut headers = HeaderMap::default();
629
630 let mut ct = MimeParameters::new(content_type);
631 match boundary {
632 Some(b) => {
633 ct.set("boundary", b);
634 }
635 None => {
636 let uuid = uuid::Uuid::new_v4();
638 let boundary = data_encoding::BASE64_NOPAD.encode(uuid.as_bytes());
639 ct.set("boundary", &boundary);
640 }
641 }
642 headers.set_content_type(ct)?;
643
644 Ok(Self {
645 bytes: "".into(),
646 headers,
647 body_offset: 0,
648 body_len: 0,
649 conformance: MessageConformance::default(),
650 parts,
651 intro: "".into(),
652 outro: "".into(),
653 })
654 }
655
656 pub fn new_binary(
657 content_type: &str,
658 content: &[u8],
659 options: Option<&AttachmentOptions>,
660 ) -> Result<Self> {
661 let mut encoded = BASE64_RFC2045.encode(content);
662 if !encoded.ends_with("\r\n") {
663 encoded.push_str("\r\n");
664 }
665 let mut headers = HeaderMap::default();
666
667 let mut ct = MimeParameters::new(content_type);
668
669 if let Some(opts) = options {
670 let mut cd = MimeParameters::new(if opts.inline { "inline" } else { "attachment" });
671 if let Some(name) = &opts.file_name {
672 cd.set("filename", name);
673 let encoding = if name.chars().any(|c| !c.is_ascii()) {
674 MimeParameterEncoding::QuotedRfc2047
675 } else {
676 MimeParameterEncoding::None
677 };
678 ct.set_with_encoding("name", name, encoding);
679 }
680 headers.set_content_disposition(cd)?;
681
682 if let Some(id) = &opts.content_id {
683 headers.set_content_id(MessageID(id.to_string()))?;
684 }
685 }
686
687 headers.set_content_type(ct)?;
688 headers.set_content_transfer_encoding(MimeParameters::new("base64"))?;
689
690 let body_len = encoded.len();
691
692 Ok(Self {
693 bytes: encoded.into(),
694 headers,
695 body_offset: 0,
696 body_len,
697 conformance: MessageConformance::default(),
698 parts: vec![],
699 intro: "".into(),
700 outro: "".into(),
701 })
702 }
703
704 pub fn simplified_structure(&'a self) -> Result<SimplifiedStructure<'a>> {
709 let parts = self.simplified_structure_pointers()?;
710
711 let mut text = None;
712 let mut html = None;
713
714 let headers = &self
715 .resolve_ptr(parts.header_part)
716 .expect("header part to always be valid")
717 .headers;
718
719 if let Some(p) = parts.text_part.and_then(|p| self.resolve_ptr(p)) {
720 text = match p.body()? {
721 DecodedBody::Text(t) => Some(t),
722 DecodedBody::Binary(_) => {
723 return Err(MailParsingError::BodyParse(
724 "expected text/plain part to be text, but it is binary".to_string(),
725 ))
726 }
727 };
728 }
729 if let Some(p) = parts.html_part.and_then(|p| self.resolve_ptr(p)) {
730 html = match p.body()? {
731 DecodedBody::Text(t) => Some(t),
732 DecodedBody::Binary(_) => {
733 return Err(MailParsingError::BodyParse(
734 "expected text/html part to be text, but it is binary".to_string(),
735 ))
736 }
737 };
738 }
739
740 let mut attachments = vec![];
741 for ptr in parts.attachments {
742 attachments.push(self.resolve_ptr(ptr).expect("pointer to be valid").clone());
743 }
744
745 Ok(SimplifiedStructure {
746 text,
747 html,
748 headers,
749 attachments,
750 })
751 }
752
753 pub fn resolve_ptr(&self, ptr: PartPointer) -> Option<&Self> {
755 let mut current = self;
756 let mut cursor = ptr.0.as_slice();
757
758 loop {
759 match cursor.first() {
760 Some(&idx) => {
761 current = current.parts.get(idx as usize)?;
762 cursor = &cursor[1..];
763 }
764 None => {
765 return Some(current);
767 }
768 }
769 }
770 }
771
772 pub fn resolve_ptr_mut(&mut self, ptr: PartPointer) -> Option<&mut Self> {
774 let mut current = self;
775 let mut cursor = ptr.0.as_slice();
776
777 loop {
778 match cursor.first() {
779 Some(&idx) => {
780 current = current.parts.get_mut(idx as usize)?;
781 cursor = &cursor[1..];
782 }
783 None => {
784 return Some(current);
786 }
787 }
788 }
789 }
790
791 pub fn simplified_structure_pointers(&self) -> Result<SimplifiedStructurePointers> {
797 self.simplified_structure_pointers_impl(None)
798 }
799
800 fn simplified_structure_pointers_impl(
801 &self,
802 my_idx: Option<u8>,
803 ) -> Result<SimplifiedStructurePointers> {
804 let info = Rfc2045Info::new(&self.headers);
805 let is_inline = info
806 .attachment_options
807 .as_ref()
808 .map(|ao| ao.inline)
809 .unwrap_or(true);
810
811 if let Some(ct) = &info.content_type {
812 if is_inline {
813 if ct.value == "text/plain" {
814 return Ok(SimplifiedStructurePointers {
815 text_part: Some(PartPointer::root_or_nth(my_idx)),
816 html_part: None,
817 header_part: PartPointer::root_or_nth(my_idx),
818 attachments: vec![],
819 });
820 }
821 if ct.value == "text/html" {
822 return Ok(SimplifiedStructurePointers {
823 html_part: Some(PartPointer::root_or_nth(my_idx)),
824 text_part: None,
825 header_part: PartPointer::root_or_nth(my_idx),
826 attachments: vec![],
827 });
828 }
829 }
830
831 if ct.value.starts_with("multipart/") {
832 let mut text_part = None;
833 let mut html_part = None;
834 let mut attachments = vec![];
835
836 for (i, p) in self.parts.iter().enumerate() {
837 let part_idx = i.try_into().map_err(|_| MailParsingError::TooManyParts)?;
838 if let Ok(mut s) = p.simplified_structure_pointers_impl(Some(part_idx)) {
839 if let Some(p) = s.text_part {
840 if text_part.is_none() {
841 text_part.replace(PartPointer::root_or_nth(my_idx).append(p));
842 } else {
843 attachments.push(p);
844 }
845 }
846 if let Some(p) = s.html_part {
847 if html_part.is_none() {
848 html_part.replace(PartPointer::root_or_nth(my_idx).append(p));
849 } else {
850 attachments.push(p);
851 }
852 }
853 attachments.append(&mut s.attachments);
854 }
855 }
856
857 return Ok(SimplifiedStructurePointers {
858 html_part,
859 text_part,
860 header_part: PartPointer::root_or_nth(my_idx),
861 attachments,
862 });
863 }
864
865 return Ok(SimplifiedStructurePointers {
866 html_part: None,
867 text_part: None,
868 header_part: PartPointer::root_or_nth(my_idx),
869 attachments: vec![PartPointer::root_or_nth(my_idx)],
870 });
871 }
872
873 Ok(SimplifiedStructurePointers {
875 text_part: Some(PartPointer::root_or_nth(my_idx)),
876 html_part: None,
877 header_part: PartPointer::root_or_nth(my_idx),
878 attachments: vec![],
879 })
880 }
881}
882
883#[derive(Debug, Clone, PartialEq, Eq)]
890pub struct PartPointer(Vec<u8>);
891
892impl PartPointer {
893 pub fn root() -> Self {
895 Self(vec![])
896 }
897
898 pub fn root_or_nth(n: Option<u8>) -> Self {
901 match n {
902 Some(n) => Self::nth(n),
903 None => Self::root(),
904 }
905 }
906
907 pub fn nth(n: u8) -> Self {
909 Self(vec![n])
910 }
911
912 pub fn append(mut self, mut other: Self) -> Self {
915 self.0.append(&mut other.0);
916 Self(self.0)
917 }
918
919 pub fn id_string(&self) -> String {
920 let mut id = String::new();
921 for p in &self.0 {
922 if !id.is_empty() {
923 id.push('.');
924 }
925 id.push_str(&p.to_string());
926 }
927 id
928 }
929}
930
931#[derive(Debug, Clone)]
932pub struct SimplifiedStructurePointers {
933 pub text_part: Option<PartPointer>,
935 pub html_part: Option<PartPointer>,
937 pub header_part: PartPointer,
939 pub attachments: Vec<PartPointer>,
941}
942
943#[derive(Debug, Clone)]
944pub struct SimplifiedStructure<'a> {
945 pub text: Option<SharedString<'a>>,
946 pub html: Option<SharedString<'a>>,
947 pub headers: &'a HeaderMap<'a>,
948 pub attachments: Vec<MimePart<'a>>,
949}
950
951#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
952#[serde(deny_unknown_fields)]
953pub struct AttachmentOptions {
954 #[serde(default)]
955 pub file_name: Option<String>,
956 #[serde(default)]
957 pub inline: bool,
958 #[serde(default)]
959 pub content_id: Option<String>,
960}
961
962#[derive(Debug, Clone, Copy, PartialEq, Eq)]
963pub enum ContentTransferEncoding {
964 SevenBit,
965 EightBit,
966 Binary,
967 QuotedPrintable,
968 Base64,
969}
970
971impl FromStr for ContentTransferEncoding {
972 type Err = MailParsingError;
973
974 fn from_str(s: &str) -> Result<Self> {
975 if s.eq_ignore_ascii_case("7bit") {
976 Ok(Self::SevenBit)
977 } else if s.eq_ignore_ascii_case("8bit") {
978 Ok(Self::EightBit)
979 } else if s.eq_ignore_ascii_case("binary") {
980 Ok(Self::Binary)
981 } else if s.eq_ignore_ascii_case("quoted-printable") {
982 Ok(Self::QuotedPrintable)
983 } else if s.eq_ignore_ascii_case("base64") {
984 Ok(Self::Base64)
985 } else {
986 Err(MailParsingError::InvalidContentTransferEncoding(
987 s.to_string(),
988 ))
989 }
990 }
991}
992
993#[derive(Debug, PartialEq)]
994pub enum DecodedBody<'a> {
995 Text(SharedString<'a>),
996 Binary(Vec<u8>),
997}
998
999impl<'a> DecodedBody<'a> {
1000 pub fn to_string_lossy(&'a self) -> Cow<'a, str> {
1001 match self {
1002 Self::Text(s) => Cow::Borrowed(s),
1003 Self::Binary(b) => String::from_utf8_lossy(b),
1004 }
1005 }
1006}
1007
1008#[cfg(test)]
1009mod test {
1010 use super::*;
1011
1012 #[test]
1013 fn msg_parsing() {
1014 let message = concat!(
1015 "Subject: hello there\n",
1016 "From: Someone <someone@example.com>\n",
1017 "\n",
1018 "I am the body"
1019 );
1020
1021 let part = MimePart::parse(message).unwrap();
1022 k9::assert_equal!(message, part.to_message_string());
1023 assert_eq!(part.raw_body(), "I am the body");
1024 k9::snapshot!(
1025 part.body(),
1026 r#"
1027Ok(
1028 Text(
1029 "I am the body",
1030 ),
1031)
1032"#
1033 );
1034
1035 k9::snapshot!(
1036 part.rebuild().unwrap().to_message_string(),
1037 r#"
1038Content-Type: text/plain;\r
1039\tcharset="us-ascii"\r
1040Subject: hello there\r
1041From: Someone <someone@example.com>\r
1042\r
1043I am the body\r
1044
1045"#
1046 );
1047 }
1048
1049 #[test]
1050 fn mime_bogus_body() {
1051 let message = concat!(
1052 "Subject: hello there\n",
1053 "From: Someone <someone@example.com>\n",
1054 "Mime-Version: 1.0\n",
1055 "Content-Type: text/plain\n",
1056 "Content-Transfer-Encoding: base64\n",
1057 "\n",
1058 "hello\n"
1059 );
1060
1061 let part = MimePart::parse(message).unwrap();
1062 assert_eq!(
1063 part.body().unwrap_err(),
1064 MailParsingError::BodyParse(
1065 "base64 decode: invalid length at 4 b='o' in hello\n".to_string()
1066 )
1067 );
1068 }
1069
1070 #[test]
1071 fn mime_encoded_body() {
1072 let message = concat!(
1073 "Subject: hello there\n",
1074 "From: Someone <someone@example.com>\n",
1075 "Mime-Version: 1.0\n",
1076 "Content-Type: text/plain\n",
1077 "Content-Transfer-Encoding: base64\n",
1078 "\n",
1079 "aGVsbG8K\n"
1080 );
1081
1082 let part = MimePart::parse(message).unwrap();
1083 k9::assert_equal!(message, part.to_message_string());
1084 assert_eq!(part.raw_body(), "aGVsbG8K\n");
1085 k9::snapshot!(
1086 part.body(),
1087 r#"
1088Ok(
1089 Text(
1090 "hello
1091",
1092 ),
1093)
1094"#
1095 );
1096
1097 k9::snapshot!(
1098 part.rebuild().unwrap().to_message_string(),
1099 r#"
1100Content-Type: text/plain;\r
1101\tcharset="us-ascii"\r
1102Content-Transfer-Encoding: quoted-printable\r
1103Subject: hello there\r
1104From: Someone <someone@example.com>\r
1105Mime-Version: 1.0\r
1106\r
1107hello=0A\r
1108
1109"#
1110 );
1111 }
1112
1113 #[test]
1114 fn mime_multipart_1() {
1115 let message = concat!(
1116 "Subject: This is a test email\n",
1117 "Content-Type: multipart/alternative; boundary=foobar\n",
1118 "Mime-Version: 1.0\n",
1119 "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\n",
1120 "\n",
1121 "--foobar\n",
1122 "Content-Type: text/plain; charset=utf-8\n",
1123 "Content-Transfer-Encoding: quoted-printable\n",
1124 "\n",
1125 "This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\n",
1126 "--foobar\n",
1127 "Content-Type: text/html\n",
1128 "Content-Transfer-Encoding: base64\n",
1129 "\n",
1130 "PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \n",
1131 "dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \n",
1132 "--foobar--\n",
1133 "After the final boundary stuff gets ignored.\n"
1134 );
1135
1136 let part = MimePart::parse(message).unwrap();
1137
1138 k9::assert_equal!(message, part.to_message_string());
1139
1140 let children = part.child_parts();
1141 k9::assert_equal!(children.len(), 2);
1142
1143 k9::snapshot!(
1144 children[0].body(),
1145 r#"
1146Ok(
1147 Text(
1148 "This is the plaintext version, in utf-8. Proof by Euro: €\r
1149",
1150 ),
1151)
1152"#
1153 );
1154 k9::snapshot!(
1155 children[1].body(),
1156 r#"
1157Ok(
1158 Text(
1159 "<html><body>This is the <b>HTML</b> version, in us-ascii. Proof by Euro: €</body></html>
1160",
1161 ),
1162)
1163"#
1164 );
1165 }
1166
1167 #[test]
1168 fn mutate_1() {
1169 let message = concat!(
1170 "Subject: This is a test email\r\n",
1171 "Content-Type: multipart/alternative; boundary=foobar\r\n",
1172 "Mime-Version: 1.0\r\n",
1173 "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\r\n",
1174 "\r\n",
1175 "--foobar\r\n",
1176 "Content-Type: text/plain; charset=utf-8\r\n",
1177 "Content-Transfer-Encoding: quoted-printable\r\n",
1178 "\r\n",
1179 "This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\r\n",
1180 "--foobar\r\n",
1181 "Content-Type: text/html\r\n",
1182 "Content-Transfer-Encoding: base64\r\n",
1183 "\r\n",
1184 "PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \r\n",
1185 "dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \r\n",
1186 "--foobar--\r\n",
1187 "After the final boundary stuff gets ignored.\r\n"
1188 );
1189
1190 let mut part = MimePart::parse(message).unwrap();
1191 k9::assert_equal!(message, part.to_message_string());
1192 fn munge(part: &mut MimePart) {
1193 let headers = part.headers_mut();
1194 headers.push(Header::with_name_value("X-Woot", "Hello"));
1195 headers.insert(0, Header::with_name_value("X-First", "at the top"));
1196 headers.retain(|hdr| !hdr.get_name().eq_ignore_ascii_case("date"));
1197 }
1198 munge(&mut part);
1199
1200 let re_encoded = part.to_message_string();
1201 k9::snapshot!(
1202 re_encoded,
1203 r#"
1204X-First: at the top\r
1205Subject: This is a test email\r
1206Content-Type: multipart/alternative; boundary=foobar\r
1207Mime-Version: 1.0\r
1208X-Woot: Hello\r
1209\r
1210--foobar\r
1211Content-Type: text/plain; charset=utf-8\r
1212Content-Transfer-Encoding: quoted-printable\r
1213\r
1214This is the plaintext version, in utf-8. Proof by Euro: =E2=82=AC\r
1215--foobar\r
1216Content-Type: text/html\r
1217Content-Transfer-Encoding: base64\r
1218\r
1219PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \r
1220dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \r
1221--foobar--\r
1222After the final boundary stuff gets ignored.\r
1223
1224"#
1225 );
1226
1227 eprintln!("part before mutate:\n{part:#?}");
1228
1229 part.child_parts_mut().retain(|part| {
1230 let ct = part.headers().content_type().unwrap().unwrap();
1231 ct.value == "text/html"
1232 });
1233
1234 eprintln!("part with html removed is:\n{part:#?}");
1235
1236 let re_encoded = part.to_message_string();
1237 k9::snapshot!(
1238 re_encoded,
1239 r#"
1240X-First: at the top\r
1241Subject: This is a test email\r
1242Content-Type: multipart/alternative; boundary=foobar\r
1243Mime-Version: 1.0\r
1244X-Woot: Hello\r
1245\r
1246--foobar\r
1247Content-Type: text/html\r
1248Content-Transfer-Encoding: base64\r
1249\r
1250PGh0bWw+PGJvZHk+VGhpcyBpcyB0aGUgPGI+SFRNTDwvYj4gdmVyc2lvbiwgaW4g \r
1251dXMtYXNjaWkuIFByb29mIGJ5IEV1cm86ICZldXJvOzwvYm9keT48L2h0bWw+Cg== \r
1252--foobar--\r
1253After the final boundary stuff gets ignored.\r
1254
1255"#
1256 );
1257 }
1258
1259 #[test]
1260 fn replace_text_body() {
1261 let mut part = MimePart::new_text_plain("Hello 👻\r\n").unwrap();
1262 let encoded = part.to_message_string();
1263 k9::snapshot!(
1264 &encoded,
1265 r#"
1266Content-Type: text/plain;\r
1267\tcharset="utf-8"\r
1268Content-Transfer-Encoding: base64\r
1269\r
1270SGVsbG8g8J+Ruw0K\r
1271
1272"#
1273 );
1274
1275 part.replace_text_body("text/plain", "Hello 🚀\r\n")
1276 .unwrap();
1277 let encoded = part.to_message_string();
1278 k9::snapshot!(
1279 &encoded,
1280 r#"
1281Content-Type: text/plain;\r
1282\tcharset="utf-8"\r
1283Content-Transfer-Encoding: base64\r
1284\r
1285SGVsbG8g8J+agA0K\r
1286
1287"#
1288 );
1289 }
1290
1291 #[test]
1292 fn construct_1() {
1293 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";
1294
1295 let part = MimePart::new_text_plain(input_text).unwrap();
1296
1297 let encoded = part.to_message_string();
1298 k9::snapshot!(
1299 &encoded,
1300 r#"
1301Content-Type: text/plain;\r
1302\tcharset="utf-8"\r
1303Content-Transfer-Encoding: quoted-printable\r
1304\r
1305Well, hello there! This is the plaintext version, in utf-8. Here's a Euro: =\r
1306=E2=82=AC, and here are some emoji =F0=9F=91=BB =F0=9F=8D=89 =F0=9F=92=A9 a=\r
1307nd this long should be long enough that we wrap it in the returned part, le=\r
1308t's see how that turns out!\r
1309
1310"#
1311 );
1312
1313 let parsed_part = MimePart::parse(encoded.clone()).unwrap();
1314 k9::assert_equal!(encoded.as_str(), parsed_part.to_message_string().as_str());
1315 k9::assert_equal!(part.body().unwrap(), DecodedBody::Text(input_text.into()));
1316 k9::snapshot!(
1317 parsed_part.simplified_structure_pointers(),
1318 "
1319Ok(
1320 SimplifiedStructurePointers {
1321 text_part: Some(
1322 PartPointer(
1323 [],
1324 ),
1325 ),
1326 html_part: None,
1327 header_part: PartPointer(
1328 [],
1329 ),
1330 attachments: [],
1331 },
1332)
1333"
1334 );
1335 }
1336
1337 #[test]
1338 fn construct_2() {
1339 let msg = MimePart::new_multipart(
1340 "multipart/mixed",
1341 vec![
1342 MimePart::new_text_plain("plain text").unwrap(),
1343 MimePart::new_html("<b>rich</b> text").unwrap(),
1344 MimePart::new_binary(
1345 "application/octet-stream",
1346 &[0, 1, 2, 3],
1347 Some(&AttachmentOptions {
1348 file_name: Some("woot.bin".to_string()),
1349 inline: false,
1350 content_id: Some("woot.id".to_string()),
1351 }),
1352 )
1353 .unwrap(),
1354 ],
1355 Some("my-boundary"),
1356 )
1357 .unwrap();
1358 k9::snapshot!(
1359 msg.to_message_string(),
1360 r#"
1361Content-Type: multipart/mixed;\r
1362\tboundary="my-boundary"\r
1363\r
1364--my-boundary\r
1365Content-Type: text/plain;\r
1366\tcharset="us-ascii"\r
1367\r
1368plain text\r
1369--my-boundary\r
1370Content-Type: text/html;\r
1371\tcharset="us-ascii"\r
1372\r
1373<b>rich</b> text\r
1374--my-boundary\r
1375Content-Disposition: attachment;\r
1376\tfilename="woot.bin"\r
1377Content-ID: <woot.id>\r
1378Content-Type: application/octet-stream;\r
1379\tname="woot.bin"\r
1380Content-Transfer-Encoding: base64\r
1381\r
1382AAECAw==\r
1383--my-boundary--\r
1384
1385"#
1386 );
1387
1388 k9::snapshot!(
1389 msg.simplified_structure_pointers(),
1390 "
1391Ok(
1392 SimplifiedStructurePointers {
1393 text_part: Some(
1394 PartPointer(
1395 [
1396 0,
1397 ],
1398 ),
1399 ),
1400 html_part: Some(
1401 PartPointer(
1402 [
1403 1,
1404 ],
1405 ),
1406 ),
1407 header_part: PartPointer(
1408 [],
1409 ),
1410 attachments: [
1411 PartPointer(
1412 [
1413 2,
1414 ],
1415 ),
1416 ],
1417 },
1418)
1419"
1420 );
1421 }
1422
1423 #[test]
1424 fn attachment_name_order_prefers_content_disposition() {
1425 let message = concat!(
1426 "Content-Type: multipart/mixed;\r\n",
1427 " boundary=\"woot\"\r\n",
1428 "\r\n",
1429 "--woot\r\n",
1430 "Content-Type: text/plain;\r\n",
1431 " charset=\"us-ascii\"\r\n",
1432 "\r\n",
1433 "Hello, I am the main message content\r\n",
1434 "--woot\r\n",
1435 "Content-Disposition: attachment;\r\n",
1436 " filename=cdname\r\n",
1437 "Content-Type: application/octet-stream;\r\n",
1438 " name=ctname\r\n",
1439 "Content-Transfer-Encoding: base64\r\n",
1440 "\r\n",
1441 "u6o=\r\n",
1442 "--woot--\r\n"
1443 );
1444 let part = MimePart::parse(message).unwrap();
1445 let structure = part.simplified_structure().unwrap();
1446
1447 k9::assert_equal!(
1448 structure.attachments[0].rfc2045_info().attachment_options,
1449 Some(AttachmentOptions {
1450 content_id: None,
1451 inline: false,
1452 file_name: Some("cdname".to_string()),
1453 })
1454 );
1455 }
1456
1457 #[test]
1458 fn attachment_name_accepts_content_type_name() {
1459 let message = concat!(
1460 "Content-Type: multipart/mixed;\r\n",
1461 " boundary=\"woot\"\r\n",
1462 "\r\n",
1463 "--woot\r\n",
1464 "Content-Type: text/plain;\r\n",
1465 " charset=\"us-ascii\"\r\n",
1466 "\r\n",
1467 "Hello, I am the main message content\r\n",
1468 "--woot\r\n",
1469 "Content-Disposition: attachment\r\n",
1470 "Content-Type: application/octet-stream;\r\n",
1471 " name=ctname\r\n",
1472 "Content-Transfer-Encoding: base64\r\n",
1473 "\r\n",
1474 "u6o=\r\n",
1475 "--woot--\r\n"
1476 );
1477 let part = MimePart::parse(message).unwrap();
1478 let structure = part.simplified_structure().unwrap();
1479
1480 k9::assert_equal!(
1481 structure.attachments[0].rfc2045_info().attachment_options,
1482 Some(AttachmentOptions {
1483 content_id: None,
1484 inline: false,
1485 file_name: Some("ctname".to_string()),
1486 })
1487 );
1488 }
1489
1490 #[test]
1491 fn funky_headers() {
1492 let message = concat!(
1493 "Subject\r\n",
1494 "Other:\r\n",
1495 "Content-Type: multipart/alternative; boundary=foobar\r\n",
1496 "Mime-Version: 1.0\r\n",
1497 "Date: Sun, 02 Oct 2016 07:06:22 -0700 (PDT)\r\n",
1498 "\r\n",
1499 "The body.\r\n"
1500 );
1501
1502 let part = MimePart::parse(message).unwrap();
1503 assert!(part
1504 .conformance()
1505 .contains(MessageConformance::MISSING_COLON_VALUE));
1506 }
1507
1508 #[test]
1513 fn rebuild_binary() {
1514 let expect = &[0, 1, 2, 3, 0xbe, 4, 5];
1515 let part = MimePart::new_binary("applicat/octet-stream", expect, None).unwrap();
1516
1517 let rebuilt = part.rebuild().unwrap();
1518 let body = rebuilt.body().unwrap();
1519
1520 assert_eq!(body, DecodedBody::Binary(expect.to_vec()));
1521 }
1522}