1use crate::{
8 encoders::{base64::base64_encode_wrapped, encode::write_encoded_body},
9 headers::{
10 Header, HeaderType, content_type::ContentType, message_id::MessageId, raw::Raw, text::Text,
11 },
12 writer::Writer,
13};
14use std::{
15 borrow::Cow,
16 cell::Cell,
17 sync::atomic::{AtomicU64, Ordering},
18 time::{SystemTime, UNIX_EPOCH},
19};
20
21#[derive(Clone, Debug)]
23pub struct MimePart<'x> {
24 pub headers: Vec<(Cow<'x, str>, HeaderType<'x>)>,
25 pub contents: BodyPart<'x>,
26}
27
28#[derive(Clone, Debug)]
29pub enum BodyPart<'x> {
30 Text(Cow<'x, str>),
31 Binary(Cow<'x, [u8]>),
32 Multipart(Vec<MimePart<'x>>),
33}
34
35impl<'x> From<&'x str> for BodyPart<'x> {
36 fn from(value: &'x str) -> Self {
37 BodyPart::Text(value.into())
38 }
39}
40
41impl<'x> From<&'x [u8]> for BodyPart<'x> {
42 fn from(value: &'x [u8]) -> Self {
43 BodyPart::Binary(value.into())
44 }
45}
46
47impl From<String> for BodyPart<'_> {
48 fn from(value: String) -> Self {
49 BodyPart::Text(value.into())
50 }
51}
52
53impl<'x> From<&'x String> for BodyPart<'x> {
54 fn from(value: &'x String) -> Self {
55 BodyPart::Text(value.as_str().into())
56 }
57}
58
59impl<'x> From<Cow<'x, str>> for BodyPart<'x> {
60 fn from(value: Cow<'x, str>) -> Self {
61 BodyPart::Text(value)
62 }
63}
64
65impl From<Vec<u8>> for BodyPart<'_> {
66 fn from(value: Vec<u8>) -> Self {
67 BodyPart::Binary(value.into())
68 }
69}
70
71impl<'x> From<Vec<MimePart<'x>>> for BodyPart<'x> {
72 fn from(value: Vec<MimePart<'x>>) -> Self {
73 BodyPart::Multipart(value)
74 }
75}
76
77impl<'x> From<&'x str> for ContentType<'x> {
78 fn from(value: &'x str) -> Self {
79 ContentType::new(value)
80 }
81}
82
83impl From<String> for ContentType<'_> {
84 fn from(value: String) -> Self {
85 ContentType::new(value)
86 }
87}
88
89impl<'x> From<&'x String> for ContentType<'x> {
90 fn from(value: &'x String) -> Self {
91 ContentType::new(value.as_str())
92 }
93}
94
95const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
96const GOLDEN_RATIO: u64 = 0x9E37_79B9_7F4A_7C15;
97const BOUNDARY_HEX_LEN: usize = 16;
98
99thread_local!(static BOUNDARY_STATE: Cell<(u64, u64)> = const { Cell::new((0, 0)) });
100static THREAD_SEQUENCE: AtomicU64 = AtomicU64::new(0);
101static PROCESS_ENTROPY: AtomicU64 = AtomicU64::new(0);
102
103#[inline(always)]
104fn splitmix64(seed: u64) -> u64 {
105 let mut z = seed;
106 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
107 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
108 z ^ (z >> 31)
109}
110
111#[inline(always)]
112fn push_hex(buffer: &mut [u8; BOUNDARY_HEX_LEN], end: usize, mut value: u64) -> usize {
113 let mut at = end;
114 while let Some(next) = at.checked_sub(1) {
115 at = next;
116 if let Some(slot) = buffer.get_mut(at) {
117 *slot = HEX_DIGITS[(value & 15) as usize];
118 }
119 value >>= 4;
120 if value == 0 {
121 break;
122 }
123 }
124 at
125}
126
127#[inline]
128fn unix_nanos() -> u64 {
129 SystemTime::now()
130 .duration_since(UNIX_EPOCH)
131 .map_or(0, |elapsed| elapsed.as_nanos() as u64)
132}
133
134fn process_entropy(address: u64, nanos: u64) -> u64 {
135 let known = PROCESS_ENTROPY.load(Ordering::Relaxed);
136 if known != 0 {
137 return known;
138 }
139 let candidate = splitmix64(address ^ nanos.rotate_left(17)) | 1;
140 match PROCESS_ENTROPY.compare_exchange(0, candidate, Ordering::Relaxed, Ordering::Relaxed) {
141 Ok(_) => candidate,
142 Err(existing) => existing,
143 }
144}
145
146#[inline]
147fn boundary_fields() -> (u64, u64, u64) {
148 let nanos = unix_nanos();
149 BOUNDARY_STATE.with(|state| {
150 let (mut thread_seed, counter) = state.get();
151 if thread_seed == 0 {
152 let address = std::ptr::from_ref(state) as usize as u64;
153 let sequence = THREAD_SEQUENCE.fetch_add(1, Ordering::Relaxed);
154 thread_seed = splitmix64(sequence) ^ process_entropy(address, nanos);
155 if thread_seed == 0 {
156 thread_seed = GOLDEN_RATIO;
157 }
158 }
159 let counter = counter.wrapping_add(1);
160 state.set((thread_seed, counter));
161 (
162 nanos,
163 splitmix64(thread_seed ^ counter.wrapping_mul(GOLDEN_RATIO)),
164 thread_seed,
165 )
166 })
167}
168
169pub fn write_boundary(output: &mut impl Writer, separator: &str) {
174 let (nanos, unique, thread_seed) = boundary_fields();
175
176 let mut buffer = [0u8; BOUNDARY_HEX_LEN];
177 let at = push_hex(&mut buffer, BOUNDARY_HEX_LEN, nanos);
178 output.write(buffer.get(at..).unwrap_or_default());
179 output.write(separator.as_bytes());
180 let at = push_hex(&mut buffer, BOUNDARY_HEX_LEN, unique);
181 output.write(buffer.get(at..).unwrap_or_default());
182 output.write(separator.as_bytes());
183 let at = push_hex(&mut buffer, BOUNDARY_HEX_LEN, thread_seed);
184 output.write(buffer.get(at..).unwrap_or_default());
185}
186
187pub fn make_boundary(separator: &str) -> String {
188 let mut boundary = Vec::with_capacity(BOUNDARY_HEX_LEN * 3 + separator.len() * 2);
189 write_boundary(&mut boundary, separator);
190 String::from_utf8(boundary).unwrap_or_default()
191}
192
193impl<'x> MimePart<'x> {
194 pub fn new(
196 content_type: impl Into<ContentType<'x>>,
197 contents: impl Into<BodyPart<'x>>,
198 ) -> Self {
199 let mut content_type = content_type.into();
200 let contents = contents.into();
201
202 if matches!(contents, BodyPart::Text(_)) && content_type.attributes.is_empty() {
203 content_type.attributes.reserve_exact(1);
204 content_type
205 .attributes
206 .push((Cow::from("charset"), Cow::from("utf-8")));
207 }
208
209 let mut headers = Vec::with_capacity(2);
210 headers.push((Cow::from("Content-Type"), content_type.into()));
211
212 Self { contents, headers }
213 }
214
215 pub fn raw(contents: impl Into<BodyPart<'x>>) -> Self {
217 Self {
218 contents: contents.into(),
219 headers: vec![],
220 }
221 }
222
223 pub fn attachment(mut self, filename: impl Into<Cow<'x, str>>) -> Self {
225 self.headers.push((
226 "Content-Disposition".into(),
227 ContentType::new("attachment")
228 .attribute("filename", filename)
229 .into(),
230 ));
231 self
232 }
233
234 pub fn inline(mut self) -> Self {
236 self.headers.push((
237 "Content-Disposition".into(),
238 ContentType::new("inline").into(),
239 ));
240 self
241 }
242
243 pub fn language(mut self, value: impl Into<Cow<'x, str>>) -> Self {
245 self.headers
246 .push(("Content-Language".into(), Text::new(value).into()));
247 self
248 }
249
250 pub fn cid(mut self, value: impl Into<Cow<'x, str>>) -> Self {
252 self.headers
253 .push(("Content-ID".into(), MessageId::new(value).into()));
254 self
255 }
256
257 pub fn location(mut self, value: impl Into<Cow<'x, str>>) -> Self {
259 self.headers
260 .push(("Content-Location".into(), Raw::new(value).into()));
261 self
262 }
263
264 pub fn transfer_encoding(mut self, value: impl Into<Cow<'x, str>>) -> Self {
266 self.headers
267 .push(("Content-Transfer-Encoding".into(), Raw::new(value).into()));
268 self
269 }
270
271 pub fn header(
273 mut self,
274 header: impl Into<Cow<'x, str>>,
275 value: impl Into<HeaderType<'x>>,
276 ) -> Self {
277 self.headers.push((header.into(), value.into()));
278 self
279 }
280
281 pub fn size(&self) -> usize {
283 match &self.contents {
284 BodyPart::Text(b) => b.len(),
285 BodyPart::Binary(b) => b.len(),
286 BodyPart::Multipart(bl) => bl.iter().map(|b| b.size()).sum(),
287 }
288 }
289
290 pub(crate) fn estimated_len(&self) -> usize {
291 self.estimated_len_at_depth(0)
292 }
293
294 fn estimated_len_at_depth(&self, depth: usize) -> usize {
295 let headers = estimated_headers_len(&self.headers);
296 match &self.contents {
297 BodyPart::Text(text) => headers
298 .saturating_add(base64_len(text.len()))
299 .saturating_add(LEAF_OVERHEAD),
300 BodyPart::Binary(binary) => headers
301 .saturating_add(base64_len(binary.len()))
302 .saturating_add(LEAF_OVERHEAD),
303 BodyPart::Multipart(parts) if depth < MAX_ESTIMATE_DEPTH => {
304 parts
305 .iter()
306 .fold(headers.saturating_add(MULTIPART_OVERHEAD), |total, part| {
307 total
308 .saturating_add(part.estimated_len_at_depth(depth + 1))
309 .saturating_add(BOUNDARY_OVERHEAD)
310 })
311 }
312 BodyPart::Multipart(parts) => headers
313 .saturating_add(MULTIPART_OVERHEAD)
314 .saturating_add(parts.len().saturating_mul(BOUNDARY_OVERHEAD)),
315 }
316 }
317
318 pub fn add_part(&mut self, part: MimePart<'x>) {
320 if let BodyPart::Multipart(ref mut parts) = self.contents {
321 parts.push(part);
322 }
323 }
324
325 pub fn write_part(self, output: &mut impl Writer) {
327 let children = match self.contents {
328 BodyPart::Text(text) => {
329 return write_leaf_part(&self.headers, text.as_bytes(), true, output);
330 }
331 BodyPart::Binary(binary) => {
332 return write_leaf_part(&self.headers, binary.as_ref(), false, output);
333 }
334 BodyPart::Multipart(children) => children,
335 };
336
337 let mut boundary = write_multipart_headers(self.headers, output);
338 let mut parts = children.into_iter();
339 let mut stack: Vec<(std::vec::IntoIter<MimePart<'x>>, Cow<'x, str>)> = Vec::new();
340
341 loop {
342 while let Some(part) = parts.next() {
343 output.write(b"\r\n--");
344 output.write(boundary.as_bytes());
345 output.write(b"\r\n");
346
347 match part.contents {
348 BodyPart::Text(text) => {
349 write_leaf_part(&part.headers, text.as_bytes(), true, output);
350 }
351 BodyPart::Binary(binary) => {
352 write_leaf_part(&part.headers, binary.as_ref(), false, output);
353 }
354 BodyPart::Multipart(children) => {
355 stack.push((parts, boundary));
356 boundary = write_multipart_headers(part.headers, output);
357 parts = children.into_iter();
358 }
359 }
360 }
361
362 output.write(b"\r\n--");
363 output.write(boundary.as_bytes());
364 output.write(b"--\r\n");
365
366 match stack.pop() {
367 Some((previous_parts, previous_boundary)) => {
368 parts = previous_parts;
369 boundary = previous_boundary;
370 }
371 None => return,
372 }
373 }
374 }
375}
376
377const MAX_ESTIMATE_DEPTH: usize = 32;
378const LEAF_OVERHEAD: usize = 96;
379const MULTIPART_OVERHEAD: usize = 128;
380const BOUNDARY_OVERHEAD: usize = 64;
381
382#[inline(always)]
383pub(crate) fn base64_len(len: usize) -> usize {
384 let encoded = len.div_ceil(3).saturating_mul(4);
385 encoded.saturating_add(encoded.div_ceil(76).saturating_mul(2))
386}
387
388pub(crate) fn estimated_headers_len(headers: &[(Cow<'_, str>, HeaderType<'_>)]) -> usize {
389 headers
390 .iter()
391 .map(|(name, _)| name.len() + HEADER_VALUE_ESTIMATE)
392 .sum()
393}
394
395const HEADER_VALUE_ESTIMATE: usize = 64;
396
397fn write_leaf_part(
398 headers: &[(Cow<'_, str>, HeaderType<'_>)],
399 body: &[u8],
400 is_text_body: bool,
401 output: &mut impl Writer,
402) {
403 let mut is_text = is_text_body;
404 let mut is_attachment = false;
405 let mut is_raw = headers.is_empty();
406
407 for (header_name, header_value) in headers {
408 write_header_name(header_name, output);
409
410 if !is_text && header_name == "Content-Type" {
411 is_text = header_value
412 .as_content_type()
413 .is_some_and(|value| value.is_text());
414 } else if !is_attachment && header_name == "Content-Disposition" {
415 is_attachment = header_value
416 .as_content_type()
417 .is_some_and(|value| value.is_attachment());
418 } else if !is_raw && header_name == "Content-Transfer-Encoding" {
419 is_raw = true;
420 }
421
422 header_value.write_header(output, header_name.len() + 2);
423 }
424
425 if !is_raw {
426 if is_text {
427 write_encoded_body(body, output, !is_attachment);
428 } else {
429 output.write(b"Content-Transfer-Encoding: base64\r\n\r\n");
430 base64_encode_wrapped(body, output);
431 }
432 } else {
433 if !headers.is_empty() {
434 output.write(b"\r\n");
435 }
436 output.write(body);
437 }
438}
439
440fn write_multipart_headers<'x>(
441 headers: Vec<(Cow<'x, str>, HeaderType<'x>)>,
442 output: &mut impl Writer,
443) -> Cow<'x, str> {
444 let mut boundary: Option<Cow<'x, str>> = None;
445
446 for (header_name, header_value) in headers {
447 write_header_name(&header_name, output);
448
449 if boundary.is_none() && header_name.eq_ignore_ascii_case("Content-Type") {
450 boundary = Some(match header_value {
451 HeaderType::ContentType(mut content_type) => {
452 let position = match content_type
453 .attributes
454 .iter()
455 .position(|(attribute, _)| attribute.eq_ignore_ascii_case("boundary"))
456 {
457 Some(position) => position,
458 None => {
459 let position = content_type.attributes.len();
460 content_type
461 .attributes
462 .push(("boundary".into(), make_boundary("_").into()));
463 position
464 }
465 };
466 content_type.write_header(output, 14);
467 content_type.attributes.swap_remove(position).1
468 }
469 HeaderType::Raw(raw) => raw_boundary(raw, output),
470 HeaderType::Text(text) => raw_boundary(Raw::new(text.text), output),
471 other => {
472 other.write_header(output, header_name.len() + 2);
473 continue;
474 }
475 });
476 } else {
477 header_value.write_header(output, header_name.len() + 2);
478 }
479 }
480
481 let boundary = boundary.unwrap_or_else(|| {
482 output.write(b"Content-Type: ");
483 let boundary = make_boundary("_");
484 ContentType::new("multipart/mixed")
485 .attribute("boundary", &boundary)
486 .write_header(output, 14);
487 boundary.into()
488 });
489
490 output.write(b"\r\n");
491 boundary
492}
493
494fn raw_boundary<'x>(raw: Raw<'x>, output: &mut impl Writer) -> Cow<'x, str> {
495 match raw.raw.find("boundary=\"") {
496 Some(position) => {
497 raw.write_header(output, 14);
498 match raw.raw {
499 Cow::Borrowed(value) => value
500 .get(position..)
501 .and_then(|tail| tail.split('"').nth(1))
502 .map_or_else(|| make_boundary("_").into(), Cow::Borrowed),
503 Cow::Owned(value) => value
504 .get(position..)
505 .and_then(|tail| tail.split('"').nth(1))
506 .map_or_else(|| make_boundary("_"), str::to_string)
507 .into(),
508 }
509 }
510 None => {
511 let boundary = make_boundary("_");
512 output.write(raw.raw.as_bytes());
513 output.write(b"; boundary=\"");
514 output.write(boundary.as_bytes());
515 output.write(b"\"\r\n");
516 boundary.into()
517 }
518 }
519}
520
521#[inline(always)]
522pub(crate) fn write_header_name(name: &str, output: &mut impl Writer) {
523 output.write(name.as_bytes());
524 output.write(b": ");
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530
531 #[test]
532 fn unexpected_multipart_content_type_values_do_not_panic() {
533 let mut output = Vec::new();
534 MimePart::raw(vec![MimePart::new("text/plain", "hello")])
535 .header("Content-Type", Text::new("multipart/mixed"))
536 .write_part(&mut output);
537 let output = String::from_utf8(output).unwrap();
538 assert!(
539 output.starts_with("Content-Type: multipart/mixed; boundary=\""),
540 "{output:?}"
541 );
542 assert!(output.ends_with("--\r\n"), "{output:?}");
543
544 let mut output = Vec::new();
545 MimePart::raw(vec![MimePart::new("text/plain", "hello")])
546 .header("Content-Type", MessageId::new("id@example.org"))
547 .write_part(&mut output);
548 let output = String::from_utf8(output).unwrap();
549 assert!(
550 output.contains("Content-Type: multipart/mixed;") && output.contains("boundary=\""),
551 "{output:?}"
552 );
553 }
554
555 #[test]
556 fn raw_content_type_with_boundary_is_written_and_reused() {
557 let mut output = Vec::new();
558 MimePart::raw(vec![MimePart::new("text/plain", "hello")])
559 .header(
560 "Content-Type",
561 Raw::new("multipart/mixed; boundary=\"abc\""),
562 )
563 .write_part(&mut output);
564 let output = String::from_utf8(output).unwrap();
565 assert!(
566 output.starts_with(
567 "Content-Type: multipart/mixed; boundary=\"abc\"\r\n\r\n\r\n--abc\r\n"
568 ),
569 "{output:?}"
570 );
571 assert!(output.ends_with("\r\n--abc--\r\n"), "{output:?}");
572 }
573 use std::collections::HashSet;
574
575 fn is_boundary_safe(value: &str) -> bool {
576 value.bytes().all(|byte| {
577 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-' | b'+' | b'=' | b':')
578 })
579 }
580
581 #[test]
582 fn hex_digits_match_the_formatter() {
583 for value in [
584 0u64,
585 1,
586 9,
587 10,
588 15,
589 16,
590 255,
591 256,
592 0xFFFF_FFFF,
593 0x1_0000_0000,
594 u64::MAX,
595 u64::MAX - 1,
596 0x0123_4567_89AB_CDEF,
597 ] {
598 let mut buffer = [0u8; BOUNDARY_HEX_LEN];
599 let at = push_hex(&mut buffer, BOUNDARY_HEX_LEN, value);
600 let digits = std::str::from_utf8(buffer.get(at..).unwrap_or_default()).unwrap();
601 assert_eq!(digits, format!("{value:x}"), "value {value}");
602 }
603 }
604
605 #[test]
606 fn boundary_keeps_its_shape() {
607 let boundary = make_boundary("_");
608 let fields = boundary.split('_').collect::<Vec<_>>();
609 assert_eq!(fields.len(), 3, "{boundary}");
610 assert!(
611 fields.iter().all(
612 |field| !field.is_empty() && field.bytes().all(|byte| byte.is_ascii_hexdigit())
613 ),
614 "{boundary}"
615 );
616 assert!(boundary.len() <= 70, "{boundary}");
617 assert!(is_boundary_safe(&boundary), "{boundary}");
618 let dotted = make_boundary(".");
619 assert_eq!(dotted.split('.').count(), 3, "{dotted}");
620 assert!(is_boundary_safe(&dotted), "{dotted}");
621 let long = make_boundary("_separator_");
622 assert_eq!(long.split("_separator_").count(), 3, "{long}");
623 }
624
625 #[test]
626 fn boundaries_are_unique_within_a_thread() {
627 let count = 1_000_000;
628 let mut seen = HashSet::with_capacity(count);
629 for _ in 0..count {
630 let boundary = make_boundary("_");
631 assert!(is_boundary_safe(&boundary), "{boundary}");
632 assert!(seen.insert(boundary), "duplicate boundary");
633 }
634 assert_eq!(seen.len(), count);
635 }
636
637 #[test]
638 fn boundaries_are_unique_across_threads() {
639 let per_thread = 50_000;
640 let threads = (0..8)
641 .map(|_| {
642 std::thread::spawn(move || {
643 (0..per_thread)
644 .map(|_| make_boundary("_"))
645 .collect::<Vec<_>>()
646 })
647 })
648 .collect::<Vec<_>>();
649 let mut seen = HashSet::with_capacity(per_thread * 8);
650 for thread in threads {
651 for boundary in thread.join().expect("thread panicked") {
652 assert!(is_boundary_safe(&boundary), "{boundary}");
653 assert!(seen.insert(boundary), "duplicate boundary");
654 }
655 }
656 assert_eq!(seen.len(), per_thread * 8);
657 }
658
659 #[test]
660 fn message_id_boundaries_are_unique_across_threads() {
661 let per_thread = 20_000;
662 let threads = (0..8)
663 .map(|_| {
664 std::thread::spawn(move || {
665 (0..per_thread)
666 .map(|_| make_boundary("."))
667 .collect::<Vec<_>>()
668 })
669 })
670 .collect::<Vec<_>>();
671 let mut seen = HashSet::with_capacity(per_thread * 8);
672 for thread in threads {
673 for boundary in thread.join().expect("thread panicked") {
674 assert!(seen.insert(boundary), "duplicate boundary");
675 }
676 }
677 assert_eq!(seen.len(), per_thread * 8);
678 }
679
680 #[test]
681 fn estimates_cover_the_output() {
682 let ascii = "lorem ipsum dolor sit amet ".repeat(40_000);
683 let latin = "réunion d'équipe: résumé ".repeat(40_000);
684 let cjk = "안녕하세요 세계 ".repeat(40_000);
685 let binary: Vec<u8> = (0..1_000_000u32).map(|value| value as u8).collect();
686 let parts = vec![
687 MimePart::new("text/plain", ascii.as_str()),
688 MimePart::new("text/plain", latin.as_str()),
689 MimePart::new("text/plain", cjk.as_str()),
690 MimePart::new("image/png", binary.as_slice()),
691 MimePart::new("text/plain", cjk.as_str()).attachment("report.txt"),
692 MimePart::new("image/png", binary.as_slice()).attachment("photo.png"),
693 MimePart::new(
694 "multipart/alternative",
695 vec![
696 MimePart::new("text/plain", "short"),
697 MimePart::new("text/html", "<p>short</p>"),
698 ],
699 ),
700 ];
701 for part in parts {
702 let estimate = part.estimated_len();
703 let mut output = Vec::new();
704 part.write_part(&mut output);
705 assert!(
706 estimate >= output.len(),
707 "estimate {estimate} below output {}",
708 output.len()
709 );
710 assert!(
711 estimate <= output.len() * 2,
712 "estimate {estimate} more than twice the output {}",
713 output.len()
714 );
715 }
716 }
717}