1use std::{
8 borrow::Cow,
9 cell::Cell,
10 collections::hash_map::DefaultHasher,
11 hash::{Hash, Hasher},
12 io::{self, Write},
13 thread,
14 time::{Duration, SystemTime, UNIX_EPOCH},
15};
16
17use crate::{
18 encoders::{
19 base64::base64_encode_mime,
20 encode::{get_encoding_type, EncodingType},
21 quoted_printable::quoted_printable_encode,
22 },
23 headers::{
24 content_type::ContentType, message_id::MessageId, raw::Raw, text::Text, Header, HeaderType,
25 },
26};
27
28#[derive(Clone, Debug)]
30pub struct MimePart<'x> {
31 pub headers: Vec<(Cow<'x, str>, HeaderType<'x>)>,
32 pub contents: BodyPart<'x>,
33}
34
35#[derive(Clone, Debug)]
36pub enum BodyPart<'x> {
37 Text(Cow<'x, str>),
38 Binary(Cow<'x, [u8]>),
39 Multipart(Vec<MimePart<'x>>),
40}
41
42impl<'x> From<&'x str> for BodyPart<'x> {
43 fn from(value: &'x str) -> Self {
44 BodyPart::Text(value.into())
45 }
46}
47
48impl<'x> From<&'x [u8]> for BodyPart<'x> {
49 fn from(value: &'x [u8]) -> Self {
50 BodyPart::Binary(value.into())
51 }
52}
53
54impl From<String> for BodyPart<'_> {
55 fn from(value: String) -> Self {
56 BodyPart::Text(value.into())
57 }
58}
59
60impl<'x> From<&'x String> for BodyPart<'x> {
61 fn from(value: &'x String) -> Self {
62 BodyPart::Text(value.as_str().into())
63 }
64}
65
66impl<'x> From<Cow<'x, str>> for BodyPart<'x> {
67 fn from(value: Cow<'x, str>) -> Self {
68 BodyPart::Text(value)
69 }
70}
71
72impl From<Vec<u8>> for BodyPart<'_> {
73 fn from(value: Vec<u8>) -> Self {
74 BodyPart::Binary(value.into())
75 }
76}
77
78impl<'x> From<Vec<MimePart<'x>>> for BodyPart<'x> {
79 fn from(value: Vec<MimePart<'x>>) -> Self {
80 BodyPart::Multipart(value)
81 }
82}
83
84impl<'x> From<&'x str> for ContentType<'x> {
85 fn from(value: &'x str) -> Self {
86 ContentType::new(value)
87 }
88}
89
90impl From<String> for ContentType<'_> {
91 fn from(value: String) -> Self {
92 ContentType::new(value)
93 }
94}
95
96impl<'x> From<&'x String> for ContentType<'x> {
97 fn from(value: &'x String) -> Self {
98 ContentType::new(value.as_str())
99 }
100}
101
102thread_local!(static COUNTER: Cell<u64> = const { Cell::new(0) });
103
104pub fn make_boundary(separator: &str) -> String {
105 let mut s = DefaultHasher::new();
107 ((&s as *const DefaultHasher) as usize).hash(&mut s);
108 thread::current().id().hash(&mut s);
109 let hash = s.finish();
110
111 format!(
112 "{:x}{}{:x}{}{:x}",
113 SystemTime::now()
114 .duration_since(UNIX_EPOCH)
115 .unwrap_or_else(|_| Duration::new(0, 0))
116 .as_nanos(),
117 separator,
118 COUNTER.with(|c| {
119 hash.wrapping_add(c.replace(c.get() + 1))
120 .wrapping_mul(11400714819323198485u64)
121 }),
122 separator,
123 hash,
124 )
125}
126
127impl<'x> MimePart<'x> {
128 pub fn new(
130 content_type: impl Into<ContentType<'x>>,
131 contents: impl Into<BodyPart<'x>>,
132 ) -> Self {
133 let mut content_type = content_type.into();
134 let contents = contents.into();
135
136 if matches!(contents, BodyPart::Text(_)) && content_type.attributes.is_empty() {
137 content_type
138 .attributes
139 .push((Cow::from("charset"), Cow::from("utf-8")));
140 }
141
142 Self {
143 contents,
144 headers: vec![("Content-Type".into(), content_type.into())],
145 }
146 }
147
148 pub fn raw(contents: impl Into<BodyPart<'x>>) -> Self {
150 Self {
151 contents: contents.into(),
152 headers: vec![],
153 }
154 }
155
156 pub fn attachment(mut self, filename: impl Into<Cow<'x, str>>) -> Self {
158 self.headers.push((
159 "Content-Disposition".into(),
160 ContentType::new("attachment")
161 .attribute("filename", filename)
162 .into(),
163 ));
164 self
165 }
166
167 pub fn inline(mut self) -> Self {
169 self.headers.push((
170 "Content-Disposition".into(),
171 ContentType::new("inline").into(),
172 ));
173 self
174 }
175
176 pub fn language(mut self, value: impl Into<Cow<'x, str>>) -> Self {
178 self.headers
179 .push(("Content-Language".into(), Text::new(value).into()));
180 self
181 }
182
183 pub fn cid(mut self, value: impl Into<Cow<'x, str>>) -> Self {
185 self.headers
186 .push(("Content-ID".into(), MessageId::new(value).into()));
187 self
188 }
189
190 pub fn location(mut self, value: impl Into<Cow<'x, str>>) -> Self {
192 self.headers
193 .push(("Content-Location".into(), Raw::new(value).into()));
194 self
195 }
196
197 pub fn transfer_encoding(mut self, value: impl Into<Cow<'x, str>>) -> Self {
199 self.headers
200 .push(("Content-Transfer-Encoding".into(), Raw::new(value).into()));
201 self
202 }
203
204 pub fn header(
206 mut self,
207 header: impl Into<Cow<'x, str>>,
208 value: impl Into<HeaderType<'x>>,
209 ) -> Self {
210 self.headers.push((header.into(), value.into()));
211 self
212 }
213
214 pub fn size(&self) -> usize {
216 match &self.contents {
217 BodyPart::Text(b) => b.len(),
218 BodyPart::Binary(b) => b.len(),
219 BodyPart::Multipart(bl) => bl.iter().map(|b| b.size()).sum(),
220 }
221 }
222
223 pub fn add_part(&mut self, part: MimePart<'x>) {
225 if let BodyPart::Multipart(ref mut parts) = self.contents {
226 parts.push(part);
227 }
228 }
229
230 pub fn write_part(self, mut output: impl Write) -> io::Result<usize> {
232 let mut stack = Vec::new();
233 let mut it = vec![self].into_iter();
234 let mut boundary: Option<Cow<'_, str>> = None;
235
236 loop {
237 while let Some(part) = it.next() {
238 if let Some(boundary) = boundary.as_ref() {
239 output.write_all(b"\r\n--")?;
240 output.write_all(boundary.as_bytes())?;
241 output.write_all(b"\r\n")?;
242 }
243 match part.contents {
244 BodyPart::Text(text) => {
245 let mut is_attachment = false;
246 let mut is_raw = part.headers.is_empty();
247
248 for (header_name, header_value) in &part.headers {
249 output.write_all(header_name.as_bytes())?;
250 output.write_all(b": ")?;
251 if !is_attachment && header_name == "Content-Disposition" {
252 is_attachment = header_value
253 .as_content_type()
254 .map(|v| v.is_attachment())
255 .unwrap_or(false);
256 } else if !is_raw && header_name == "Content-Transfer-Encoding" {
257 is_raw = true;
258 }
259 header_value.write_header(&mut output, header_name.len() + 2)?;
260 }
261 if !is_raw {
262 detect_encoding(text.as_bytes(), &mut output, !is_attachment)?;
263 } else {
264 if !part.headers.is_empty() {
265 output.write_all(b"\r\n")?;
266 }
267 output.write_all(text.as_bytes())?;
268 }
269 }
270 BodyPart::Binary(binary) => {
271 let mut is_text = false;
272 let mut is_attachment = false;
273 let mut is_raw = part.headers.is_empty();
274
275 for (header_name, header_value) in &part.headers {
276 output.write_all(header_name.as_bytes())?;
277 output.write_all(b": ")?;
278 if !is_text && header_name == "Content-Type" {
279 is_text = header_value
280 .as_content_type()
281 .map(|v| v.is_text())
282 .unwrap_or(false);
283 } else if !is_attachment && header_name == "Content-Disposition" {
284 is_attachment = header_value
285 .as_content_type()
286 .map(|v| v.is_attachment())
287 .unwrap_or(false);
288 } else if !is_raw && header_name == "Content-Transfer-Encoding" {
289 is_raw = true;
290 }
291 header_value.write_header(&mut output, header_name.len() + 2)?;
292 }
293
294 if !is_raw {
295 if !is_text {
296 output.write_all(b"Content-Transfer-Encoding: base64\r\n\r\n")?;
297 base64_encode_mime(binary.as_ref(), &mut output, false)?;
298 } else {
299 detect_encoding(binary.as_ref(), &mut output, !is_attachment)?;
300 }
301 } else {
302 if !part.headers.is_empty() {
303 output.write_all(b"\r\n")?;
304 }
305 output.write_all(binary.as_ref())?;
306 }
307 }
308 BodyPart::Multipart(parts) => {
309 if boundary.is_some() {
310 stack.push((it, boundary.take()));
311 }
312
313 let mut found_ct = false;
314 for (header_name, header_value) in part.headers {
315 output.write_all(header_name.as_bytes())?;
316 output.write_all(b": ")?;
317
318 if !found_ct && header_name.eq_ignore_ascii_case("Content-Type") {
319 boundary = match header_value {
320 HeaderType::ContentType(mut ct) => {
321 let bpos = if let Some(pos) = ct
322 .attributes
323 .iter()
324 .position(|(a, _)| a.eq_ignore_ascii_case("boundary"))
325 {
326 pos
327 } else {
328 let pos = ct.attributes.len();
329 ct.attributes.push((
330 "boundary".into(),
331 make_boundary("_").into(),
332 ));
333 pos
334 };
335 ct.write_header(&mut output, 14)?;
336 ct.attributes.swap_remove(bpos).1.into()
337 }
338 HeaderType::Raw(raw) => {
339 if let Some(pos) = raw.raw.find("boundary=\"") {
340 if let Some(boundary) = raw.raw[pos..].split('"').nth(1)
341 {
342 Some(boundary.to_string().into())
343 } else {
344 Some(make_boundary("_").into())
345 }
346 } else {
347 let boundary = make_boundary("_");
348 output.write_all(raw.raw.as_bytes())?;
349 output.write_all(b"; boundary=\"")?;
350 output.write_all(boundary.as_bytes())?;
351 output.write_all(b"\"\r\n")?;
352 Some(boundary.into())
353 }
354 }
355 _ => panic!("Unsupported Content-Type header value."),
356 };
357 found_ct = true;
358 } else {
359 header_value.write_header(&mut output, header_name.len() + 2)?;
360 }
361 }
362
363 if !found_ct {
364 output.write_all(b"Content-Type: ")?;
365 let boundary_ = make_boundary("_");
366 ContentType::new("multipart/mixed")
367 .attribute("boundary", &boundary_)
368 .write_header(&mut output, 14)?;
369 boundary = Some(boundary_.into());
370 }
371
372 output.write_all(b"\r\n")?;
373 it = parts.into_iter();
374 }
375 }
376 }
377 if let Some(boundary) = boundary {
378 output.write_all(b"\r\n--")?;
379 output.write_all(boundary.as_bytes())?;
380 output.write_all(b"--\r\n")?;
381 }
382 if let Some((prev_it, prev_boundary)) = stack.pop() {
383 it = prev_it;
384 boundary = prev_boundary;
385 } else {
386 break;
387 }
388 }
389 Ok(0)
390 }
391}
392
393fn detect_encoding(input: &[u8], mut output: impl Write, is_body: bool) -> io::Result<()> {
394 match get_encoding_type(input, false, is_body) {
395 EncodingType::Base64 => {
396 output.write_all(b"Content-Transfer-Encoding: base64\r\n\r\n")?;
397 base64_encode_mime(input, &mut output, false)?;
398 }
399 EncodingType::QuotedPrintable(_) => {
400 output.write_all(b"Content-Transfer-Encoding: quoted-printable\r\n\r\n")?;
401 quoted_printable_encode(input, &mut output, is_body)?;
402 }
403 EncodingType::None => {
404 output.write_all(b"Content-Transfer-Encoding: 7bit\r\n\r\n")?;
405 if is_body {
406 let mut prev_ch = 0;
407 for ch in input {
408 if *ch == b'\n' && prev_ch != b'\r' {
409 output.write_all(b"\r")?;
410 }
411 output.write_all(&[*ch])?;
412 prev_ch = *ch;
413 }
414 } else {
415 output.write_all(input)?;
416 }
417 }
418 }
419 Ok(())
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn test_detect_encoding() {
428 let mut output = Vec::new();
429 detect_encoding(b"a b c\r\n", &mut output, false).unwrap();
430 assert_eq!(output, b"Content-Transfer-Encoding: 7bit\r\n\r\na b c\r\n");
431
432 let mut output = Vec::new();
433 detect_encoding(
434 b"a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\r\n",
435 &mut output,
436 false,
437 )
438 .unwrap();
439 assert_eq!(output, b"Content-Transfer-Encoding: 7bit\r\n\r\na a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\r\n");
440
441 let mut output = Vec::new();
442 detect_encoding(
443 b"a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a\r\n",
444 &mut output,
445 false,
446 )
447 .unwrap();
448 assert_eq!(output, b"Content-Transfer-Encoding: quoted-printable\r\n\r\na a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a =\r\na=0D=0A");
449
450 let mut output = Vec::new();
451 let long_line = "a".repeat(100);
452 detect_encoding(long_line.as_bytes(), &mut output, false).unwrap();
453 let expected = format!(
454 "Content-Transfer-Encoding: quoted-printable\r\n\r\n{}",
455 "a".repeat(76) + "=\r\n" + &"a".repeat(24)
456 );
457 assert_eq!(output, expected.as_bytes());
458 }
459}