From 8c370e28c9a0b4053ff5a5942a3c7b5e84995a50 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Sat, 12 Feb 2022 11:17:44 +0100 Subject: [PATCH] Follow RFC 2231 in order to properly encode the `Content-Disposition` header (#685) Uses RFC 2231 to encode the Content-Disposition header --- Cargo.toml | 3 +- src/message/header/content_disposition.rs | 50 +++++++++++++++++++---- src/message/header/mod.rs | 14 ++++++- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b7e721b..5b7e95b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ fastrand = { version = "1.4", optional = true } quoted_printable = { version = "0.4", optional = true } base64 = { version = "0.13", optional = true } regex = { version = "1", default-features = false, features = ["std", "unicode-case"] } +email-encoding = { git = "https://github.com/lettre/email-encoding.git", rev = "9b55bae0ed02ca52f6fb86a82b4814baa712e45a", optional = true } # file transport uuid = { version = "0.8", features = ["v4"], optional = true } @@ -81,7 +82,7 @@ name = "transport_smtp" [features] default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"] -builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable"] +builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable", "email-encoding"] mime03 = ["mime"] # transports diff --git a/src/message/header/content_disposition.rs b/src/message/header/content_disposition.rs index f217bb6..d66fafd 100644 --- a/src/message/header/content_disposition.rs +++ b/src/message/header/content_disposition.rs @@ -1,3 +1,7 @@ +use std::fmt::Write; + +use email_encoding::headers::EmailWriter; + use super::{Header, HeaderName, HeaderValue}; use crate::BoxError; @@ -5,25 +9,47 @@ use crate::BoxError; /// /// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183) #[derive(Debug, Clone, PartialEq)] -pub struct ContentDisposition(String); +pub struct ContentDisposition(HeaderValue); impl ContentDisposition { /// An attachment which should be displayed inline into the message pub fn inline() -> Self { - Self("inline".into()) + Self(HeaderValue::dangerous_new_pre_encoded( + Self::name(), + "inline".to_string(), + "inline".to_string(), + )) } /// An attachment which should be displayed inline into the message, but that also /// species the filename in case it were to be downloaded pub fn inline_with_name(file_name: &str) -> Self { - debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'"); - Self(format!("inline; filename=\"{}\"", file_name)) + Self::with_name("inline", file_name) } /// An attachment which is separate from the body of the message, and can be downloaded separately pub fn attachment(file_name: &str) -> Self { - debug_assert!(!file_name.contains('"'), "file_name shouldn't contain '\"'"); - Self(format!("attachment; filename=\"{}\"", file_name)) + Self::with_name("attachment", file_name) + } + + fn with_name(kind: &str, file_name: &str) -> Self { + let raw_value = format!("{}; filename=\"{}\"", kind, file_name); + + let mut encoded_value = String::new(); + let line_len = "Content-Disposition: ".len(); + let mut w = EmailWriter::new(&mut encoded_value, line_len, false); + w.write_str(kind).expect("writing `kind` returned an error"); + w.write_char(';').expect("writing `;` returned an error"); + w.space(); + + email_encoding::headers::rfc2231::encode("filename", file_name, &mut w) + .expect("some Write implementation returned an error"); + + Self(HeaderValue::dangerous_new_pre_encoded( + Self::name(), + raw_value, + encoded_value, + )) } } @@ -33,11 +59,19 @@ impl Header for ContentDisposition { } fn parse(s: &str) -> Result { - Ok(Self(s.into())) + match (s.split_once(';'), s) { + (_, "inline") => Ok(Self::inline()), + (Some((kind @ ("inline" | "attachment"), file_name)), _) => file_name + .split_once(" filename=\"") + .and_then(|(_, file_name)| file_name.strip_suffix('"')) + .map(|file_name| Self::with_name(kind, file_name)) + .ok_or_else(|| "Unsupported ContentDisposition value".into()), + _ => Err("Unsupported ContentDisposition value".into()), + } } fn display(&self) -> HeaderValue { - HeaderValue::new(Self::name(), self.0.clone()) + self.0.clone() } } diff --git a/src/message/header/mod.rs b/src/message/header/mod.rs index 1b6242a..b823643 100644 --- a/src/message/header/mod.rs +++ b/src/message/header/mod.rs @@ -275,7 +275,7 @@ impl PartialEq for &str { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct HeaderValue { name: HeaderName, raw_value: String, @@ -293,6 +293,18 @@ impl HeaderValue { encoded_value, } } + + pub fn dangerous_new_pre_encoded( + name: HeaderName, + raw_value: String, + encoded_value: String, + ) -> Self { + Self { + name, + raw_value, + encoded_value, + } + } } const ENCODING_START_PREFIX: &str = "=?utf-8?b?";