use std::{io::Write, iter::repeat_with}; use mime::Mime; use crate::message::{ header::{self, ContentTransferEncoding, ContentType, Header, Headers}, EmailFormat, IntoBody, }; /// MIME part variants #[derive(Debug, Clone)] pub(super) enum Part { /// Single part with content Single(SinglePart), /// Multiple parts of content Multi(MultiPart), } impl EmailFormat for Part { fn format(&self, out: &mut Vec) { match self { Part::Single(part) => part.format(out), Part::Multi(part) => part.format(out), } } } /// Creates builder for single part #[derive(Debug, Clone)] pub struct SinglePartBuilder { headers: Headers, } impl SinglePartBuilder { /// Creates a default singlepart builder pub fn new() -> Self { Self { headers: Headers::new(), } } /// Set the header to singlepart pub fn header(mut self, header: H) -> Self { self.headers.set(header); self } /// Set the Content-Type header of the singlepart pub fn content_type(mut self, content_type: ContentType) -> Self { self.headers.set(content_type); self } /// Build singlepart using body pub fn body(mut self, body: T) -> SinglePart { let maybe_encoding = self.headers.get::(); let body = body.into_body(maybe_encoding); self.headers.set(body.encoding()); SinglePart { headers: self.headers, body: body.into_vec(), } } } impl Default for SinglePartBuilder { fn default() -> Self { Self::new() } } /// Single part /// /// # Example /// /// ``` /// use lettre::message::{header, SinglePart}; /// /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// let part = SinglePart::builder() /// .header(header::ContentType::TEXT_PLAIN) /// .body(String::from("Текст письма в уникоде")); /// # Ok(()) /// # } /// ``` #[derive(Debug, Clone)] pub struct SinglePart { headers: Headers, body: Vec, } impl SinglePart { /// Creates a builder for singlepart #[inline] pub fn builder() -> SinglePartBuilder { SinglePartBuilder::new() } /// Directly create a `SinglePart` from an plain UTF-8 content pub fn plain(body: T) -> Self { Self::builder() .header(header::ContentType::TEXT_PLAIN) .body(body) } /// Directly create a `SinglePart` from an UTF-8 HTML content pub fn html(body: T) -> Self { Self::builder() .header(header::ContentType::TEXT_HTML) .body(body) } /// Get the headers from singlepart #[inline] pub fn headers(&self) -> &Headers { &self.headers } /// Get the encoded body #[inline] pub fn raw_body(&self) -> &[u8] { &self.body } /// Get message content formatted for sending pub fn formatted(&self) -> Vec { let mut out = Vec::new(); self.format(&mut out); out } } impl EmailFormat for SinglePart { fn format(&self, out: &mut Vec) { write!(out, "{}", self.headers) .expect("A Write implementation panicked while formatting headers"); out.extend_from_slice(b"\r\n"); out.extend_from_slice(&self.body); out.extend_from_slice(b"\r\n"); } } /// The kind of multipart #[derive(Debug, Clone)] pub enum MultiPartKind { /// Mixed kind to combine unrelated content parts /// /// For example this kind can be used to mix email message and attachments. Mixed, /// Alternative kind to join several variants of same email contents. /// /// That kind is recommended to use for joining plain (text) and rich (HTML) messages into single email message. Alternative, /// Related kind to mix content and related resources. /// /// For example, you can include images into HTML content using that. Related, /// Encrypted kind for encrypted messages Encrypted { protocol: String }, /// Signed kind for signed messages Signed { protocol: String, micalg: String }, } /// Create a random MIME boundary. /// (Not cryptographically random) fn make_boundary() -> String { repeat_with(fastrand::alphanumeric).take(40).collect() } impl MultiPartKind { pub(crate) fn to_mime>(&self, boundary: Option) -> Mime { let boundary = boundary.map_or_else(make_boundary, Into::into); format!( "multipart/{}; boundary=\"{}\"{}", match self { Self::Mixed => "mixed", Self::Alternative => "alternative", Self::Related => "related", Self::Encrypted { .. } => "encrypted", Self::Signed { .. } => "signed", }, boundary, match self { Self::Encrypted { protocol } => format!("; protocol=\"{}\"", protocol), Self::Signed { protocol, micalg } => format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg), _ => String::new(), } ) .parse() .unwrap() } fn from_mime(m: &Mime) -> Option { match m.subtype().as_ref() { "mixed" => Some(Self::Mixed), "alternative" => Some(Self::Alternative), "related" => Some(Self::Related), "signed" => m.get_param("protocol").and_then(|p| { m.get_param("micalg").map(|micalg| Self::Signed { protocol: p.as_str().to_owned(), micalg: micalg.as_str().to_owned(), }) }), "encrypted" => m.get_param("protocol").map(|p| Self::Encrypted { protocol: p.as_str().to_owned(), }), _ => None, } } } /// Multipart builder #[derive(Debug, Clone)] pub struct MultiPartBuilder { headers: Headers, } impl MultiPartBuilder { /// Creates default multipart builder pub fn new() -> Self { Self { headers: Headers::new(), } } /// Set a header pub fn header(mut self, header: H) -> Self { self.headers.set(header); self } /// Set `Content-Type` header using [`MultiPartKind`] pub fn kind(self, kind: MultiPartKind) -> Self { self.header(ContentType::from_mime(kind.to_mime::(None))) } /// Set custom boundary pub fn boundary>(self, boundary: S) -> Self { let kind = { let content_type = self.headers.get::().unwrap(); MultiPartKind::from_mime(content_type.as_ref()).unwrap() }; let mime = kind.to_mime(Some(boundary)); self.header(ContentType::from_mime(mime)) } /// Creates multipart without parts pub fn build(self) -> MultiPart { MultiPart { headers: self.headers, parts: Vec::new(), } } /// Creates multipart using singlepart pub fn singlepart(self, part: SinglePart) -> MultiPart { self.build().singlepart(part) } /// Creates multipart using multipart pub fn multipart(self, part: MultiPart) -> MultiPart { self.build().multipart(part) } } impl Default for MultiPartBuilder { fn default() -> Self { Self::new() } } /// Multipart variant with parts #[derive(Debug, Clone)] pub struct MultiPart { headers: Headers, parts: Vec, } impl MultiPart { /// Creates multipart builder pub fn builder() -> MultiPartBuilder { MultiPartBuilder::new() } /// Creates mixed multipart builder /// /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)` pub fn mixed() -> MultiPartBuilder { MultiPart::builder().kind(MultiPartKind::Mixed) } /// Creates alternative multipart builder /// /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)` pub fn alternative() -> MultiPartBuilder { MultiPart::builder().kind(MultiPartKind::Alternative) } /// Creates related multipart builder /// /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)` pub fn related() -> MultiPartBuilder { MultiPart::builder().kind(MultiPartKind::Related) } /// Creates encrypted multipart builder /// /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Encrypted{ protocol })` pub fn encrypted(protocol: String) -> MultiPartBuilder { MultiPart::builder().kind(MultiPartKind::Encrypted { protocol }) } /// Creates signed multipart builder /// /// Shortcut for `MultiPart::builder().kind(MultiPartKind::Signed{ protocol, micalg })` pub fn signed(protocol: String, micalg: String) -> MultiPartBuilder { MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg }) } /// Alias for HTML and plain text versions of an email pub fn alternative_plain_html(plain: T, html: V) -> Self { Self::alternative() .singlepart(SinglePart::plain(plain)) .singlepart(SinglePart::html(html)) } /// Add single part to multipart pub fn singlepart(mut self, part: SinglePart) -> Self { self.parts.push(Part::Single(part)); self } /// Add multi part to multipart pub fn multipart(mut self, part: MultiPart) -> Self { self.parts.push(Part::Multi(part)); self } /// Get the boundary of multipart contents pub fn boundary(&self) -> String { let content_type = self.headers.get::().unwrap(); content_type .as_ref() .get_param("boundary") .unwrap() .as_str() .into() } /// Get the headers from the multipart pub fn headers(&self) -> &Headers { &self.headers } /// Get a mutable reference to the headers pub fn headers_mut(&mut self) -> &mut Headers { &mut self.headers } /// Get message content formatted for SMTP pub fn formatted(&self) -> Vec { let mut out = Vec::new(); self.format(&mut out); out } } impl EmailFormat for MultiPart { fn format(&self, out: &mut Vec) { write!(out, "{}", self.headers) .expect("A Write implementation panicked while formatting headers"); out.extend_from_slice(b"\r\n"); let boundary = self.boundary(); for part in &self.parts { out.extend_from_slice(b"--"); out.extend_from_slice(boundary.as_bytes()); out.extend_from_slice(b"\r\n"); part.format(out); } out.extend_from_slice(b"--"); out.extend_from_slice(boundary.as_bytes()); out.extend_from_slice(b"--\r\n"); } } #[cfg(test)] mod test { use pretty_assertions::assert_eq; use super::*; use crate::message::header; #[test] fn single_part_binary() { let part = SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .header(header::ContentTransferEncoding::Binary) .body(String::from("Текст письма в уникоде")); assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( "Content-Type: text/plain; charset=utf-8\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "Текст письма в уникоде\r\n" ) ); } #[test] fn single_part_quoted_printable() { let part = SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .header(header::ContentTransferEncoding::QuotedPrintable) .body(String::from("Текст письма в уникоде")); assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( "Content-Type: text/plain; charset=utf-8\r\n", "Content-Transfer-Encoding: quoted-printable\r\n", "\r\n", "=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", "=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\r\n" ) ); } #[test] fn single_part_base64() { let part = SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .header(header::ContentTransferEncoding::Base64) .body(String::from("Текст письма в уникоде")); assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( "Content-Type: text/plain; charset=utf-8\r\n", "Content-Transfer-Encoding: base64\r\n", "\r\n", "0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n" ) ); } #[test] fn multi_part_mixed() { let part = MultiPart::mixed() .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .singlepart( SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .header(header::ContentTransferEncoding::Binary) .body(String::from("Текст письма в уникоде")), ) .singlepart( SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .header(header::ContentDisposition::attachment("example.c")) .header(header::ContentTransferEncoding::Binary) .body(String::from("int main() { return 0; }")), ); assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( "Content-Type: multipart/mixed;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/plain; charset=utf-8\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "Текст письма в уникоде\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/plain; charset=utf-8\r\n", "Content-Disposition: attachment; filename=\"example.c\"\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "int main() { return 0; }\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n" ) ); } #[test] fn multi_part_encrypted() { let part = MultiPart::encrypted("application/pgp-encrypted".to_owned()) .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .singlepart( SinglePart::builder() .header(header::ContentType::parse("application/pgp-encrypted").unwrap()) .body(String::from("Version: 1")), ) .singlepart( SinglePart::builder() .header( ContentType::parse("application/octet-stream; name=\"encrypted.asc\"") .unwrap(), ) .header(header::ContentDisposition::inline_with_name( "encrypted.asc", )) .body(String::from(concat!( "-----BEGIN PGP MESSAGE-----\r\n", "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n", "...\r\n", "-----END PGP MESSAGE-----\r\n" ))), ); assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( "Content-Type: multipart/encrypted;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n", " protocol=\"application/pgp-encrypted\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: application/pgp-encrypted\r\n", "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "Version: 1\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n", "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n", "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "-----BEGIN PGP MESSAGE-----\r\n", "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n", "...\r\n", "-----END PGP MESSAGE-----\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n" ) ); } #[test] fn multi_part_signed() { let part = MultiPart::signed( "application/pgp-signature".to_owned(), "pgp-sha256".to_owned(), ) .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .singlepart( SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .body(String::from("Test email for signature")), ) .singlepart( SinglePart::builder() .header( ContentType::parse("application/pgp-signature; name=\"signature.asc\"") .unwrap(), ) .header(header::ContentDisposition::attachment("signature.asc")) .body(String::from(concat!( "-----BEGIN PGP SIGNATURE-----\r\n", "\r\n", "iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n", "udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n", "PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n", "=3FYZ\r\n", "-----END PGP SIGNATURE-----\r\n", ))), ); assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( "Content-Type: multipart/signed;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n", " protocol=\"application/pgp-signature\";", " micalg=\"pgp-sha256\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/plain; charset=utf-8\r\n", "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "Test email for signature\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n", "Content-Disposition: attachment; filename=\"signature.asc\"\r\n", "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "-----BEGIN PGP SIGNATURE-----\r\n", "\r\n", "iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n", "udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n", "PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n", "=3FYZ\r\n", "-----END PGP SIGNATURE-----\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n" ) ); } #[test] fn multi_part_alternative() { let part = MultiPart::alternative() .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .singlepart(SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .header(header::ContentTransferEncoding::Binary) .body(String::from("Текст письма в уникоде"))) .singlepart(SinglePart::builder() .header(header::ContentType::TEXT_HTML) .header(header::ContentTransferEncoding::Binary) .body(String::from("

Текст письма в уникоде

"))); assert_eq!(String::from_utf8(part.formatted()).unwrap(), concat!("Content-Type: multipart/alternative;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/plain; charset=utf-8\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "Текст письма в уникоде\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/html; charset=utf-8\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "

Текст письма в уникоде

\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n")); } #[test] fn multi_part_mixed_related() { let part = MultiPart::mixed() .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .multipart(MultiPart::related() .boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1") .singlepart(SinglePart::builder() .header(header::ContentType::TEXT_HTML) .header(header::ContentTransferEncoding::Binary) .body(String::from("

Текст письма в уникоде

"))) .singlepart(SinglePart::builder() .header(header::ContentType::parse("image/png").unwrap()) .header(header::ContentLocation::from(String::from("/image.png"))) .header(header::ContentTransferEncoding::Base64) .body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")))) .singlepart(SinglePart::builder() .header(header::ContentType::TEXT_PLAIN) .header(header::ContentDisposition::attachment("example.c")) .header(header::ContentTransferEncoding::Binary) .body(String::from("int main() { return 0; }"))); assert_eq!(String::from_utf8(part.formatted()).unwrap(), concat!("Content-Type: multipart/mixed;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: multipart/related;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/html; charset=utf-8\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "

Текст письма в уникоде

\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: image/png\r\n", "Content-Location: /image.png\r\n", "Content-Transfer-Encoding: base64\r\n", "\r\n", "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n", "ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n", "NTY3ODkwMTIzNDU2Nzg5MA==\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", "Content-Type: text/plain; charset=utf-8\r\n", "Content-Disposition: attachment; filename=\"example.c\"\r\n", "Content-Transfer-Encoding: binary\r\n", "\r\n", "int main() { return 0; }\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n")); } #[test] fn test_make_boundary() { let mut boundaries = std::collections::HashSet::with_capacity(10); for _ in 0..1000 { boundaries.insert(make_boundary()); } // Ensure there are no duplicates assert_eq!(1000, boundaries.len()); // Ensure correct length for boundary in boundaries { assert_eq!(40, boundary.len()); } } }