1use std::{
13 borrow::Cow,
14 io::{self, Write},
15 time::{Duration, SystemTime, UNIX_EPOCH},
16};
17
18use rand::Rng;
19
20use crate::{
21 encoders::{
22 base64::base64_encode_mime,
23 encode::{EncodingType, get_encoding_type},
24 quoted_printable::quoted_printable_encode,
25 },
26 headers::{Header, HeaderType, content_type::ContentType, message_id::MessageId, raw::Raw, text::Text},
27};
28
29#[derive(Clone, Debug)]
31pub struct MimePart<'x> {
32 pub headers: Vec<(Cow<'x, str>, HeaderType<'x>)>,
33 pub contents: BodyPart<'x>,
34}
35
36#[derive(Clone, Debug)]
37pub enum BodyPart<'x> {
38 Text(Cow<'x, str>),
39 Binary(Cow<'x, [u8]>),
40 Multipart(Vec<MimePart<'x>>),
41}
42
43impl<'x> From<&'x str> for BodyPart<'x> {
44 fn from(value: &'x str) -> Self {
45 BodyPart::Text(value.into())
46 }
47}
48
49impl<'x> From<&'x [u8]> for BodyPart<'x> {
50 fn from(value: &'x [u8]) -> Self {
51 BodyPart::Binary(value.into())
52 }
53}
54
55impl<'x> From<String> for BodyPart<'x> {
56 fn from(value: String) -> Self {
57 BodyPart::Text(value.into())
58 }
59}
60
61impl<'x> From<&'x String> for BodyPart<'x> {
62 fn from(value: &'x String) -> Self {
63 BodyPart::Text(value.as_str().into())
64 }
65}
66
67impl<'x> From<Cow<'x, str>> for BodyPart<'x> {
68 fn from(value: Cow<'x, str>) -> Self {
69 BodyPart::Text(value)
70 }
71}
72
73impl<'x> From<Vec<u8>> for BodyPart<'x> {
74 fn from(value: Vec<u8>) -> Self {
75 BodyPart::Binary(value.into())
76 }
77}
78
79impl<'x> From<Vec<MimePart<'x>>> for BodyPart<'x> {
80 fn from(value: Vec<MimePart<'x>>) -> Self {
81 BodyPart::Multipart(value)
82 }
83}
84
85impl<'x> From<&'x str> for ContentType<'x> {
86 fn from(value: &'x str) -> Self {
87 ContentType::new(value)
88 }
89}
90
91impl<'x> From<String> for ContentType<'x> {
92 fn from(value: String) -> Self {
93 ContentType::new(value)
94 }
95}
96
97impl<'x> From<&'x String> for ContentType<'x> {
98 fn from(value: &'x String) -> Self {
99 ContentType::new(value.as_str())
100 }
101}
102
103pub fn make_boundary(separator: &str) -> String {
107 let unix_nano = SystemTime::now()
133 .duration_since(UNIX_EPOCH)
134 .unwrap_or_else(|_| Duration::new(0, 0))
135 .as_nanos();
136
137 let pid = rand::thread_rng().gen_range(0..999);
138 let rint = rand::thread_rng().gen_range(0..999);
139
140 return format!("{:x}{}{:x}{}{:x}", unix_nano, separator, pid, separator, rint);
141}
142
143impl<'x> MimePart<'x> {
144 pub fn new(content_type: impl Into<ContentType<'x>>, contents: impl Into<BodyPart<'x>>) -> Self {
146 let mut content_type = content_type.into();
147 let contents = contents.into();
148
149 if matches!(contents, BodyPart::Text(_)) && content_type.attributes.is_empty() {
150 content_type.attributes.push((Cow::from("charset"), Cow::from("utf-8")));
151 }
152
153 Self {
154 contents,
155 headers: vec![("Content-Type".into(), content_type.into())],
156 }
157 }
158
159 pub fn raw(contents: impl Into<BodyPart<'x>>) -> Self {
161 Self {
162 contents: contents.into(),
163 headers: vec![],
164 }
165 }
166
167 pub fn attachment(mut self, filename: impl Into<Cow<'x, str>>) -> Self {
169 self.headers.push((
170 "Content-Disposition".into(),
171 ContentType::new("attachment").attribute("filename", filename).into(),
172 ));
173 self
174 }
175
176 pub fn inline(mut self) -> Self {
178 self.headers
179 .push(("Content-Disposition".into(), ContentType::new("inline").into()));
180 self
181 }
182
183 pub fn language(mut self, value: impl Into<Cow<'x, str>>) -> Self {
185 self.headers.push(("Content-Language".into(), Text::new(value).into()));
186 self
187 }
188
189 pub fn cid(mut self, value: impl Into<Cow<'x, str>>) -> Self {
191 self.headers.push(("Content-ID".into(), MessageId::new(value).into()));
192 self
193 }
194
195 pub fn location(mut self, value: impl Into<Cow<'x, str>>) -> Self {
197 self.headers.push(("Content-Location".into(), Raw::new(value).into()));
198 self
199 }
200
201 pub fn transfer_encoding(mut self, value: impl Into<Cow<'x, str>>) -> Self {
203 self.headers
204 .push(("Content-Transfer-Encoding".into(), Raw::new(value).into()));
205 self
206 }
207
208 pub fn header(mut self, header: impl Into<Cow<'x, str>>, value: impl Into<HeaderType<'x>>) -> 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.as_content_type().map(|v| v.is_text()).unwrap_or(false);
280 } else if !is_attachment && header_name == "Content-Disposition" {
281 is_attachment = header_value
282 .as_content_type()
283 .map(|v| v.is_attachment())
284 .unwrap_or(false);
285 } else if !is_raw && header_name == "Content-Transfer-Encoding" {
286 is_raw = true;
287 }
288 header_value.write_header(&mut output, header_name.len() + 2)?;
289 }
290
291 if !is_raw {
292 if !is_text {
293 output.write_all(b"Content-Transfer-Encoding: base64\r\n\r\n")?;
294 base64_encode_mime(binary.as_ref(), &mut output, false)?;
295 } else {
296 detect_encoding(binary.as_ref(), &mut output, !is_attachment)?;
297 }
298 } else {
299 if !part.headers.is_empty() {
300 output.write_all(b"\r\n")?;
301 }
302 output.write_all(binary.as_ref())?;
303 }
304 }
305 BodyPart::Multipart(parts) => {
306 if boundary.is_some() {
307 stack.push((it, boundary.take()));
308 }
309
310 let mut found_ct = false;
311 for (header_name, header_value) in part.headers {
312 output.write_all(header_name.as_bytes())?;
313 output.write_all(b": ")?;
314
315 if !found_ct && header_name.eq_ignore_ascii_case("Content-Type") {
316 boundary = match header_value {
317 HeaderType::ContentType(mut ct) => {
318 let bpos = if let Some(pos) = ct
319 .attributes
320 .iter()
321 .position(|(a, _)| a.eq_ignore_ascii_case("boundary"))
322 {
323 pos
324 } else {
325 let pos = ct.attributes.len();
326 ct.attributes.push(("boundary".into(), make_boundary("_").into()));
327 pos
328 };
329 ct.write_header(&mut output, 14)?;
330 ct.attributes.swap_remove(bpos).1.into()
331 }
332 HeaderType::Raw(raw) => {
333 if let Some(pos) = raw.raw.find("boundary=\"") {
334 if let Some(boundary) = raw.raw[pos..].split('"').nth(1) {
335 Some(boundary.to_string().into())
336 } else {
337 Some(make_boundary("_").into())
338 }
339 } else {
340 let boundary = make_boundary("_");
341 output.write_all(raw.raw.as_bytes())?;
342 output.write_all(b"; boundary=\"")?;
343 output.write_all(boundary.as_bytes())?;
344 output.write_all(b"\"\r\n")?;
345 Some(boundary.into())
346 }
347 }
348 _ => panic!("Unsupported Content-Type header value."),
349 };
350 found_ct = true;
351 } else {
352 header_value.write_header(&mut output, header_name.len() + 2)?;
353 }
354 }
355
356 if !found_ct {
357 output.write_all(b"Content-Type: ")?;
358 let boundary_ = make_boundary("_");
359 ContentType::new("multipart/mixed")
360 .attribute("boundary", &boundary_)
361 .write_header(&mut output, 14)?;
362 boundary = Some(boundary_.into());
363 }
364
365 output.write_all(b"\r\n")?;
366 it = parts.into_iter();
367 }
368 }
369 }
370 if let Some(boundary) = boundary {
371 output.write_all(b"\r\n--")?;
372 output.write_all(boundary.as_bytes())?;
373 output.write_all(b"--\r\n")?;
374 }
375 if let Some((prev_it, prev_boundary)) = stack.pop() {
376 it = prev_it;
377 boundary = prev_boundary;
378 } else {
379 break;
380 }
381 }
382 Ok(0)
383 }
384}
385
386fn detect_encoding(input: &[u8], mut output: impl Write, is_body: bool) -> io::Result<()> {
387 match get_encoding_type(input, false, is_body) {
388 EncodingType::Base64 => {
389 output.write_all(b"Content-Transfer-Encoding: base64\r\n\r\n")?;
390 base64_encode_mime(input, &mut output, false)?;
391 }
392 EncodingType::QuotedPrintable(_) => {
393 output.write_all(b"Content-Transfer-Encoding: quoted-printable\r\n\r\n")?;
394 quoted_printable_encode(input, &mut output, false, is_body)?;
395 }
396 EncodingType::None => {
397 output.write_all(b"Content-Transfer-Encoding: 7bit\r\n\r\n")?;
398 if is_body {
399 let mut prev_ch = 0;
400 for ch in input {
401 if *ch == b'\n' && prev_ch != b'\r' {
402 output.write_all(b"\r")?;
403 }
404 output.write_all(&[*ch])?;
405 prev_ch = *ch;
406 }
407 } else {
408 output.write_all(input)?;
409 }
410 }
411 }
412 Ok(())
413}