1use std::{io::Write, iter::repeat_with};
2
3use mime::Mime;
4
5use crate::message::{
6 header::{self, ContentTransferEncoding, ContentType, Header, Headers},
7 EmailFormat, IntoBody,
8};
9
10#[derive(Debug, Clone)]
12pub(super) enum Part {
13 Single(SinglePart),
15
16 Multi(MultiPart),
18}
19
20impl Part {
21 #[cfg(feature = "dkim")]
22 pub(super) fn format_body(&self, out: &mut Vec<u8>) {
23 match self {
24 Part::Single(part) => part.format_body(out),
25 Part::Multi(part) => part.format_body(out),
26 }
27 }
28}
29
30impl EmailFormat for Part {
31 fn format(&self, out: &mut Vec<u8>) {
32 match self {
33 Part::Single(part) => part.format(out),
34 Part::Multi(part) => part.format(out),
35 }
36 }
37}
38
39#[derive(Debug, Clone)]
41pub struct SinglePartBuilder {
42 headers: Headers,
43}
44
45impl SinglePartBuilder {
46 pub fn new() -> Self {
48 Self {
49 headers: Headers::new(),
50 }
51 }
52
53 pub fn header<H: Header>(mut self, header: H) -> Self {
55 self.headers.set(header);
56 self
57 }
58
59 pub fn content_type(mut self, content_type: ContentType) -> Self {
61 self.headers.set(content_type);
62 self
63 }
64
65 pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
67 let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
68 let body = body.into_body(maybe_encoding);
69
70 self.headers.set(body.encoding());
71
72 SinglePart {
73 headers: self.headers,
74 body: body.into_vec(),
75 }
76 }
77}
78
79impl Default for SinglePartBuilder {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85#[derive(Debug, Clone)]
101pub struct SinglePart {
102 headers: Headers,
103 body: Vec<u8>,
104}
105
106impl SinglePart {
107 #[inline]
109 pub fn builder() -> SinglePartBuilder {
110 SinglePartBuilder::new()
111 }
112
113 pub fn plain<T: IntoBody>(body: T) -> Self {
115 Self::builder()
116 .header(header::ContentType::TEXT_PLAIN)
117 .body(body)
118 }
119
120 pub fn html<T: IntoBody>(body: T) -> Self {
122 Self::builder()
123 .header(header::ContentType::TEXT_HTML)
124 .body(body)
125 }
126
127 #[inline]
129 pub fn headers(&self) -> &Headers {
130 &self.headers
131 }
132
133 #[inline]
135 pub fn raw_body(&self) -> &[u8] {
136 &self.body
137 }
138
139 pub fn formatted(&self) -> Vec<u8> {
141 let mut out = Vec::new();
142 self.format(&mut out);
143 out
144 }
145
146 fn format_body(&self, out: &mut Vec<u8>) {
148 out.extend_from_slice(&self.body);
149 out.extend_from_slice(b"\r\n");
150 }
151}
152
153impl EmailFormat for SinglePart {
154 fn format(&self, out: &mut Vec<u8>) {
155 write!(out, "{}", self.headers)
156 .expect("A Write implementation panicked while formatting headers");
157 out.extend_from_slice(b"\r\n");
158 self.format_body(out);
159 }
160}
161
162#[derive(Debug, Clone)]
164pub enum MultiPartKind {
165 Mixed,
169
170 Alternative,
174
175 Related,
179
180 Encrypted { protocol: String },
182
183 Signed { protocol: String, micalg: String },
185}
186
187fn make_boundary() -> String {
190 repeat_with(fastrand::alphanumeric).take(40).collect()
191}
192
193impl MultiPartKind {
194 pub(crate) fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
195 let boundary = boundary.map_or_else(make_boundary, Into::into);
196
197 format!(
198 "multipart/{}; boundary=\"{}\"{}",
199 match self {
200 Self::Mixed => "mixed",
201 Self::Alternative => "alternative",
202 Self::Related => "related",
203 Self::Encrypted { .. } => "encrypted",
204 Self::Signed { .. } => "signed",
205 },
206 boundary,
207 match self {
208 Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
209 Self::Signed { protocol, micalg } =>
210 format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
211 _ => String::new(),
212 }
213 )
214 .parse()
215 .unwrap()
216 }
217
218 fn from_mime(m: &Mime) -> Option<Self> {
219 match m.subtype().as_ref() {
220 "mixed" => Some(Self::Mixed),
221 "alternative" => Some(Self::Alternative),
222 "related" => Some(Self::Related),
223 "signed" => m.get_param("protocol").and_then(|p| {
224 m.get_param("micalg").map(|micalg| Self::Signed {
225 protocol: p.as_str().to_owned(),
226 micalg: micalg.as_str().to_owned(),
227 })
228 }),
229 "encrypted" => m.get_param("protocol").map(|p| Self::Encrypted {
230 protocol: p.as_str().to_owned(),
231 }),
232 _ => None,
233 }
234 }
235}
236
237#[derive(Debug, Clone)]
239pub struct MultiPartBuilder {
240 headers: Headers,
241}
242
243impl MultiPartBuilder {
244 pub fn new() -> Self {
246 Self {
247 headers: Headers::new(),
248 }
249 }
250
251 pub fn header<H: Header>(mut self, header: H) -> Self {
253 self.headers.set(header);
254 self
255 }
256
257 pub fn kind(self, kind: MultiPartKind) -> Self {
259 self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
260 }
261
262 pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
264 let kind = {
265 let content_type = self.headers.get::<ContentType>().unwrap();
266 MultiPartKind::from_mime(content_type.as_ref()).unwrap()
267 };
268 let mime = kind.to_mime(Some(boundary));
269 self.header(ContentType::from_mime(mime))
270 }
271
272 pub fn build(self) -> MultiPart {
274 MultiPart {
275 headers: self.headers,
276 parts: Vec::new(),
277 }
278 }
279
280 pub fn singlepart(self, part: SinglePart) -> MultiPart {
282 self.build().singlepart(part)
283 }
284
285 pub fn multipart(self, part: MultiPart) -> MultiPart {
287 self.build().multipart(part)
288 }
289}
290
291impl Default for MultiPartBuilder {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297#[derive(Debug, Clone)]
299pub struct MultiPart {
300 headers: Headers,
301 parts: Vec<Part>,
302}
303
304impl MultiPart {
305 pub fn builder() -> MultiPartBuilder {
307 MultiPartBuilder::new()
308 }
309
310 pub fn mixed() -> MultiPartBuilder {
314 MultiPart::builder().kind(MultiPartKind::Mixed)
315 }
316
317 pub fn alternative() -> MultiPartBuilder {
321 MultiPart::builder().kind(MultiPartKind::Alternative)
322 }
323
324 pub fn related() -> MultiPartBuilder {
328 MultiPart::builder().kind(MultiPartKind::Related)
329 }
330
331 pub fn encrypted(protocol: String) -> MultiPartBuilder {
335 MultiPart::builder().kind(MultiPartKind::Encrypted { protocol })
336 }
337
338 pub fn signed(protocol: String, micalg: String) -> MultiPartBuilder {
342 MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
343 }
344
345 pub fn alternative_plain_html<T: IntoBody, V: IntoBody>(plain: T, html: V) -> Self {
347 Self::alternative()
348 .singlepart(SinglePart::plain(plain))
349 .singlepart(SinglePart::html(html))
350 }
351
352 pub fn singlepart(mut self, part: SinglePart) -> Self {
354 self.parts.push(Part::Single(part));
355 self
356 }
357
358 pub fn multipart(mut self, part: MultiPart) -> Self {
360 self.parts.push(Part::Multi(part));
361 self
362 }
363
364 pub fn boundary(&self) -> String {
366 let content_type = self.headers.get::<ContentType>().unwrap();
367 content_type
368 .as_ref()
369 .get_param("boundary")
370 .unwrap()
371 .as_str()
372 .into()
373 }
374
375 pub fn headers(&self) -> &Headers {
377 &self.headers
378 }
379
380 pub fn headers_mut(&mut self) -> &mut Headers {
382 &mut self.headers
383 }
384
385 pub fn formatted(&self) -> Vec<u8> {
387 let mut out = Vec::new();
388 self.format(&mut out);
389 out
390 }
391
392 fn format_body(&self, out: &mut Vec<u8>) {
394 let boundary = self.boundary();
395
396 for part in &self.parts {
397 out.extend_from_slice(b"--");
398 out.extend_from_slice(boundary.as_bytes());
399 out.extend_from_slice(b"\r\n");
400 part.format(out);
401 }
402
403 out.extend_from_slice(b"--");
404 out.extend_from_slice(boundary.as_bytes());
405 out.extend_from_slice(b"--\r\n");
406 }
407}
408
409impl EmailFormat for MultiPart {
410 fn format(&self, out: &mut Vec<u8>) {
411 write!(out, "{}", self.headers)
412 .expect("A Write implementation panicked while formatting headers");
413 out.extend_from_slice(b"\r\n");
414 self.format_body(out);
415 }
416}
417
418#[cfg(test)]
419mod test {
420 use pretty_assertions::assert_eq;
421
422 use super::*;
423
424 #[test]
425 fn single_part_binary() {
426 let part = SinglePart::builder()
427 .header(header::ContentType::TEXT_PLAIN)
428 .header(header::ContentTransferEncoding::Binary)
429 .body(String::from("Текст письма в уникоде"));
430
431 assert_eq!(
432 String::from_utf8(part.formatted()).unwrap(),
433 concat!(
434 "Content-Type: text/plain; charset=utf-8\r\n",
435 "Content-Transfer-Encoding: binary\r\n",
436 "\r\n",
437 "Текст письма в уникоде\r\n"
438 )
439 );
440 }
441
442 #[test]
443 fn single_part_quoted_printable() {
444 let part = SinglePart::builder()
445 .header(header::ContentType::TEXT_PLAIN)
446 .header(header::ContentTransferEncoding::QuotedPrintable)
447 .body(String::from("Текст письма в уникоде"));
448
449 assert_eq!(
450 String::from_utf8(part.formatted()).unwrap(),
451 concat!(
452 "Content-Type: text/plain; charset=utf-8\r\n",
453 "Content-Transfer-Encoding: quoted-printable\r\n",
454 "\r\n",
455 "=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
456 "=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\r\n"
457 )
458 );
459 }
460
461 #[test]
462 fn single_part_base64() {
463 let part = SinglePart::builder()
464 .header(header::ContentType::TEXT_PLAIN)
465 .header(header::ContentTransferEncoding::Base64)
466 .body(String::from("Текст письма в уникоде"));
467
468 assert_eq!(
469 String::from_utf8(part.formatted()).unwrap(),
470 concat!(
471 "Content-Type: text/plain; charset=utf-8\r\n",
472 "Content-Transfer-Encoding: base64\r\n",
473 "\r\n",
474 "0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
475 )
476 );
477 }
478
479 #[test]
480 fn multi_part_mixed() {
481 let part = MultiPart::mixed()
482 .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
483 .singlepart(
484 SinglePart::builder()
485 .header(header::ContentType::TEXT_PLAIN)
486 .header(header::ContentTransferEncoding::Binary)
487 .body(String::from("Текст письма в уникоде")),
488 )
489 .singlepart(
490 SinglePart::builder()
491 .header(header::ContentType::TEXT_PLAIN)
492 .header(header::ContentDisposition::attachment("example.c"))
493 .header(header::ContentTransferEncoding::Binary)
494 .body(String::from("int main() { return 0; }")),
495 );
496
497 assert_eq!(
498 String::from_utf8(part.formatted()).unwrap(),
499 concat!(
500 "Content-Type: multipart/mixed;\r\n",
501 " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
502 "\r\n",
503 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
504 "Content-Type: text/plain; charset=utf-8\r\n",
505 "Content-Transfer-Encoding: binary\r\n",
506 "\r\n",
507 "Текст письма в уникоде\r\n",
508 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
509 "Content-Type: text/plain; charset=utf-8\r\n",
510 "Content-Disposition: attachment; filename=\"example.c\"\r\n",
511 "Content-Transfer-Encoding: binary\r\n",
512 "\r\n",
513 "int main() { return 0; }\r\n",
514 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
515 )
516 );
517 }
518 #[test]
519 fn multi_part_encrypted() {
520 let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
521 .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
522 .singlepart(
523 SinglePart::builder()
524 .header(header::ContentType::parse("application/pgp-encrypted").unwrap())
525 .body(String::from("Version: 1")),
526 )
527 .singlepart(
528 SinglePart::builder()
529 .header(
530 ContentType::parse("application/octet-stream; name=\"encrypted.asc\"")
531 .unwrap(),
532 )
533 .header(header::ContentDisposition::inline_with_name(
534 "encrypted.asc",
535 ))
536 .body(String::from(concat!(
537 "-----BEGIN PGP MESSAGE-----\r\n",
538 "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
539 "...\r\n",
540 "-----END PGP MESSAGE-----\r\n"
541 ))),
542 );
543
544 assert_eq!(
545 String::from_utf8(part.formatted()).unwrap(),
546 concat!(
547 "Content-Type: multipart/encrypted;\r\n",
548 " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
549 " protocol=\"application/pgp-encrypted\"\r\n",
550 "\r\n",
551 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
552 "Content-Type: application/pgp-encrypted\r\n",
553 "Content-Transfer-Encoding: 7bit\r\n",
554 "\r\n",
555 "Version: 1\r\n",
556 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
557 "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
558 "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
559 "Content-Transfer-Encoding: 7bit\r\n",
560 "\r\n",
561 "-----BEGIN PGP MESSAGE-----\r\n",
562 "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
563 "...\r\n",
564 "-----END PGP MESSAGE-----\r\n",
565 "\r\n",
566 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
567 )
568 );
569 }
570 #[test]
571 fn multi_part_signed() {
572 let part = MultiPart::signed(
573 "application/pgp-signature".to_owned(),
574 "pgp-sha256".to_owned(),
575 )
576 .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
577 .singlepart(
578 SinglePart::builder()
579 .header(header::ContentType::TEXT_PLAIN)
580 .body(String::from("Test email for signature")),
581 )
582 .singlepart(
583 SinglePart::builder()
584 .header(
585 ContentType::parse("application/pgp-signature; name=\"signature.asc\"")
586 .unwrap(),
587 )
588 .header(header::ContentDisposition::attachment("signature.asc"))
589 .body(String::from(concat!(
590 "-----BEGIN PGP SIGNATURE-----\r\n",
591 "\r\n",
592 "iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
593 "udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
594 "PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
595 "=3FYZ\r\n",
596 "-----END PGP SIGNATURE-----\r\n",
597 ))),
598 );
599
600 assert_eq!(
601 String::from_utf8(part.formatted()).unwrap(),
602 concat!(
603 "Content-Type: multipart/signed;\r\n",
604 " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
605 " protocol=\"application/pgp-signature\";",
606 " micalg=\"pgp-sha256\"\r\n",
607 "\r\n",
608 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
609 "Content-Type: text/plain; charset=utf-8\r\n",
610 "Content-Transfer-Encoding: 7bit\r\n",
611 "\r\n",
612 "Test email for signature\r\n",
613 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
614 "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
615 "Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
616 "Content-Transfer-Encoding: 7bit\r\n",
617 "\r\n",
618 "-----BEGIN PGP SIGNATURE-----\r\n",
619 "\r\n",
620 "iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
621 "udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
622 "PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
623 "=3FYZ\r\n",
624 "-----END PGP SIGNATURE-----\r\n",
625 "\r\n",
626 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
627 )
628 );
629 }
630
631 #[test]
632 fn multi_part_alternative() {
633 let part = MultiPart::alternative()
634 .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
635 .singlepart(SinglePart::builder()
636 .header(header::ContentType::TEXT_PLAIN)
637 .header(header::ContentTransferEncoding::Binary)
638 .body(String::from("Текст письма в уникоде")))
639 .singlepart(SinglePart::builder()
640 .header(header::ContentType::TEXT_HTML)
641 .header(header::ContentTransferEncoding::Binary)
642 .body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
643
644 assert_eq!(String::from_utf8(part.formatted()).unwrap(),
645 concat!("Content-Type: multipart/alternative;\r\n",
646 " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
647 "\r\n",
648 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
649 "Content-Type: text/plain; charset=utf-8\r\n",
650 "Content-Transfer-Encoding: binary\r\n",
651 "\r\n",
652 "Текст письма в уникоде\r\n",
653 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
654 "Content-Type: text/html; charset=utf-8\r\n",
655 "Content-Transfer-Encoding: binary\r\n",
656 "\r\n",
657 "<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
658 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
659 }
660
661 #[test]
662 fn multi_part_mixed_related() {
663 let part = MultiPart::mixed()
664 .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
665 .multipart(MultiPart::related()
666 .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
667 .singlepart(SinglePart::builder()
668 .header(header::ContentType::TEXT_HTML)
669 .header(header::ContentTransferEncoding::Binary)
670 .body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
671 .singlepart(SinglePart::builder()
672 .header(header::ContentType::parse("image/png").unwrap())
673 .header(header::ContentLocation::from(String::from("/image.png")))
674 .header(header::ContentTransferEncoding::Base64)
675 .body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
676 .singlepart(SinglePart::builder()
677 .header(header::ContentType::TEXT_PLAIN)
678 .header(header::ContentDisposition::attachment("example.c"))
679 .header(header::ContentTransferEncoding::Binary)
680 .body(String::from("int main() { return 0; }")));
681
682 assert_eq!(String::from_utf8(part.formatted()).unwrap(),
683 concat!("Content-Type: multipart/mixed;\r\n",
684 " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
685 "\r\n",
686 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
687 "Content-Type: multipart/related;\r\n",
688 " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
689 "\r\n",
690 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
691 "Content-Type: text/html; charset=utf-8\r\n",
692 "Content-Transfer-Encoding: binary\r\n",
693 "\r\n",
694 "<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
695 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
696 "Content-Type: image/png\r\n",
697 "Content-Location: /image.png\r\n",
698 "Content-Transfer-Encoding: base64\r\n",
699 "\r\n",
700 "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
701 "ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
702 "NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
703 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n",
704 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
705 "Content-Type: text/plain; charset=utf-8\r\n",
706 "Content-Disposition: attachment; filename=\"example.c\"\r\n",
707 "Content-Transfer-Encoding: binary\r\n",
708 "\r\n",
709 "int main() { return 0; }\r\n",
710 "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
711 }
712
713 #[test]
714 fn test_make_boundary() {
715 let mut boundaries = std::collections::HashSet::with_capacity(10);
716 for _ in 0..1000 {
717 boundaries.insert(make_boundary());
718 }
719
720 assert_eq!(1000, boundaries.len());
722
723 for boundary in boundaries {
725 assert_eq!(40, boundary.len());
726 }
727 }
728}