From 72015da467863a0f09326e66735883c78f674460 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Sun, 2 Aug 2020 21:43:24 +0200 Subject: [PATCH] Add support for encrypted and signed multipart emails (#444) Co-authored-by: Tim Anderson --- src/message/mimebody.rs | 159 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 156 insertions(+), 3 deletions(-) diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs index 9f012f0..6d056e5 100644 --- a/src/message/mimebody.rs +++ b/src/message/mimebody.rs @@ -176,7 +176,7 @@ impl EmailFormat for SinglePart { /// The kind of multipart /// -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum MultiPartKind { /// Mixed kind to combine unrelated content parts /// @@ -192,6 +192,12 @@ pub enum MultiPartKind { /// /// 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. @@ -210,13 +216,21 @@ impl MultiPartKind { use self::MultiPartKind::*; format!( - "multipart/{}; boundary=\"{}\"", + "multipart/{}; boundary=\"{}\"{}", match self { Mixed => "mixed", Alternative => "alternative", Related => "related", + Encrypted { .. } => "encrypted", + Signed { .. } => "signed", }, - boundary + boundary, + match self { + Encrypted { protocol } => format!("; protocol=\"{}\"", protocol), + Signed { protocol, micalg } => + format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg), + _ => String::new(), + } ) .parse() .unwrap() @@ -228,6 +242,15 @@ impl MultiPartKind { "mixed" => Some(Mixed), "alternative" => Some(Alternative), "related" => Some(Related), + "signed" => m.get_param("protocol").and_then(|p| { + m.get_param("micalg").map(|micalg| Signed { + protocol: p.as_str().to_owned(), + micalg: micalg.as_str().to_owned(), + }) + }), + "encrypted" => m.get_param("protocol").map(|p| Encrypted { + protocol: p.as_str().to_owned(), + }), _ => None, } } @@ -340,6 +363,20 @@ impl MultiPart { 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 }) + } + /// Add part to multipart pub fn part(mut self, part: Part) -> Self { self.parts.push(part); @@ -524,6 +561,122 @@ mod test { "int main() { return 0; }\r\n", "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); } + #[test] + fn multi_part_encrypted() { + let part = MultiPart::encrypted("application/pgp-encrypted".to_owned()) + .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") + .part(Part::Single( + SinglePart::builder() + .header(header::ContentType( + "application/pgp-encrypted".parse().unwrap(), + )) + .body(String::from("Version: 1")), + )) + .singlepart( + SinglePart::builder() + .header(ContentType( + "application/octet-stream; name=\"encrypted.asc\"" + .parse() + .unwrap(), + )) + .header(header::ContentDisposition { + disposition: header::DispositionType::Inline, + parameters: vec![header::DispositionParam::Filename( + header::Charset::Ext("utf-8".into()), + None, + "encrypted.asc".as_bytes().into(), + )], + }) + .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;", + " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";", + " protocol=\"application/pgp-encrypted\"\r\n", + "\r\n", + "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", + "Content-Type: application/pgp-encrypted\r\n", + "\r\n", + "Version: 1\r\n", + "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", + "Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n", + "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n", + "\r\n", + "-----BEGIN PGP MESSAGE-----\r\n", + "wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n", + "...\r\n", + "-----END PGP MESSAGE-----\r\n", + "\r\n", + "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); + } + #[test] + fn multi_part_signed() { + let part = MultiPart::signed( + "application/pgp-signature".to_owned(), + "pgp-sha256".to_owned(), + ) + .boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK") + .part(Part::Single( + SinglePart::builder() + .header(header::ContentType("text/plain".parse().unwrap())) + .body(String::from("Test email for signature")), + )) + .singlepart( + SinglePart::builder() + .header(ContentType( + "application/pgp-signature; name=\"signature.asc\"" + .parse() + .unwrap(), + )) + .header(header::ContentDisposition { + disposition: header::DispositionType::Attachment, + parameters: vec![header::DispositionParam::Filename( + header::Charset::Ext("utf-8".into()), + None, + "signature.asc".as_bytes().into(), + )], + }) + .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;", + " boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";", + " protocol=\"application/pgp-signature\";", + " micalg=\"pgp-sha256\"\r\n", + "\r\n", + "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", + "Content-Type: text/plain\r\n", + "\r\n", + "Test email for signature\r\n", + "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n", + "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n", + "Content-Disposition: attachment; filename=\"signature.asc\"\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", + "--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n")); + } #[test] fn multi_part_alternative() {