diff --git a/Cargo.toml b/Cargo.toml index f27c33d..b6dfa6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,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 = "7b4fdf44a6e8715753d9012317c019271bcbc431", optional = true } +email-encoding = { git = "https://github.com/lettre/email-encoding.git", rev = "ac7ab8630b8d012ade63869c89b2855e27b3ce7a", optional = true } # file transport uuid = { version = "1", features = ["v4"], optional = true } @@ -88,7 +88,7 @@ name = "transport_smtp" [features] default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"] -builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable", "email-encoding"] +builder = ["httpdate", "mime", "fastrand", "quoted_printable", "email-encoding"] mime03 = ["mime"] # transports @@ -109,7 +109,7 @@ tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"] tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"] tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"] -dkim = ["sha2", "rsa", "ed25519-dalek"] +dkim = ["base64", "sha2", "rsa", "ed25519-dalek"] [package.metadata.docs.rs] all-features = true diff --git a/src/message/dkim.rs b/src/message/dkim.rs index 01f08d8..e6bf7b7 100644 --- a/src/message/dkim.rs +++ b/src/message/dkim.rs @@ -480,7 +480,7 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT fn test_headers_simple_canonicalize() { let message = test_message(); dbg!(message.headers.to_string()); - assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= \r\nTest: test test very very long with spaces and extra spaces \twill be \r\n folded to several lines \r\n") + assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= \r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n") } #[test] @@ -521,18 +521,14 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= \r\n", "To: Test2 \r\n", "Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n", - "Test: test test very very long with spaces and extra spaces \twill be \r\n", + "Test: test test very very long with spaces and extra spaces \twill be\r\n", " folded to several lines \r\n", "Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n", "Content-Transfer-Encoding: 7bit\r\n", - "DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest; \r\n", - " c=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To; \r\n", - " bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=; b=UQWpUooKjVzgC7jtuEKdpCbz\r\n", - " sSDnOqZFg8+S7rZj89n/+AVsdcwxumeLCUYLeko2TZgVFJA7kGz+wLzH2wpzB4XnyUqrkF6PrFA\r\n", - " 9K11K365JDtzfMSc5eRVS8crO6F/A9QtXPndnzrXQ5HrtFgfxUlJ9cX6pTOor1NVCpfYUNBviIg\r\n", - " Am0LnUOKdlJ8z82kLFRpIqawMKNVfyqP8Es6H3NHM4Y1uwGgls9DM1+lZxNXxkMpoGV3rL/n/ai\r\n", - " s8+VifrRxPLB0mr9gbavSkCQ2QzUA/+iq8DgPCGpXDrdDrwTcrV3pL/iHyEjQZWwFSQkx+r/CGb\r\n", - " 8TQLqH6T3wfr69XWvg==\r\n", + "DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n", + " c=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To;\r\n", + " bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=;\r\n", + " b=NhoIMMAALoSgu5lKAR0+MUQunOWnU7wpF9ORUFtpxq9sGZDo9AX43AMhFemyM5W204jpFwMU6pm7AMR1nOYBdSYye4yUALtvT2nqbJBwSh7JeYu+z22t1RFKp7qQR1il8aSrkbZuNMFHYuSEwW76QtKwcNqP4bQOzS9CzgQp0ABu8qwYPBr/EypykPTfqjtyN+ywrfdqjjGOzTpRGolH0hc3CrAETNjjHbNBgKgucXmXTN7hMRdzqWjeFPxizXwouwNAavFClPG0l33gXVArFWn+CkgA84G/s4zuJiF7QPZR87Pu4pw/vIlSXxH4a42W3tT19v9iBTH7X7ldYegtmQ==\r\n", "\r\n", "test\r\n", "\r\n", @@ -575,18 +571,13 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= \r\n", "To: Test2 \r\n", "Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n", - "Test: test test very very long with spaces and extra spaces \twill be \r\n", - " folded to several lines \r\n", - "Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n", + "Test: test test very very long with spaces and extra spaces \twill be\r\n", + " folded to several lines \r\n","Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n", "Content-Transfer-Encoding: 7bit\r\n", - "DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest; \r\n", - " c=relaxed/relaxed; q=dns/txt; t=0; h=date:from:subject:to; \r\n", - " bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=; b=YaVfmH8dbGEywoLJ4uhbvYqD\r\n", - " yQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FE\r\n", - " h6j3RQthqNbQblcfH/U8mazTuRbVjYBbTZQDaQCMPTz+8D+ZQfXo2oq6dGzTuGvmuYft0CVsq/B\r\n", - " Ip/EkhZHqiphDeVJSHD4iKW8+L2XwEWThoY92xOYc1G0TtBwz2UJgtiHX2YulH/kRBHeK3dKn9R\r\n", - " TNVL3VZ+9ZrnFwIhET9TPGtU2I+q0EMSWF9H9bTrASMgW/U+E0VM2btqJlrTU6rQ7wlQeHdwecL\r\n", - " nzXcyhCUInF1+veMNw==\r\n", + "DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n", + " c=relaxed/relaxed; q=dns/txt; t=0; h=date:from:subject:to;\r\n", + " bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=;\r\n", + " b=YaVfmH8dbGEywoLJ4uhbvYqDyQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FEh6j3RQthqNbQblcfH/U8mazTuRbVjYBbTZQDaQCMPTz+8D+ZQfXo2oq6dGzTuGvmuYft0CVsq/BIp/EkhZHqiphDeVJSHD4iKW8+L2XwEWThoY92xOYc1G0TtBwz2UJgtiHX2YulH/kRBHeK3dKn9RTNVL3VZ+9ZrnFwIhET9TPGtU2I+q0EMSWF9H9bTrASMgW/U+E0VM2btqJlrTU6rQ7wlQeHdwecLnzXcyhCUInF1+veMNw==\r\n", "\r\n", "test\r\n", "\r\n", diff --git a/src/message/header/mod.rs b/src/message/header/mod.rs index 6f5b1d4..422e1ac 100644 --- a/src/message/header/mod.rs +++ b/src/message/header/mod.rs @@ -3,10 +3,12 @@ use std::{ borrow::Cow, error::Error, - fmt::{self, Display, Formatter}, + fmt::{self, Display, Formatter, Write}, ops::Deref, }; +use email_encoding::headers::EmailWriter; + pub use self::{ content::*, content_disposition::ContentDisposition, @@ -315,55 +317,36 @@ impl HeaderValue { } } -const ENCODING_START_PREFIX: &str = "=?utf-8?b?"; -const ENCODING_END_SUFFIX: &str = "?="; -const MAX_LINE_LEN: usize = 76; - /// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder -struct HeaderValueEncoder { - line_len: usize, +struct HeaderValueEncoder<'a> { + writer: EmailWriter<'a>, encode_buf: String, } -impl HeaderValueEncoder { - fn encode(name: &str, value: &str, f: &mut impl fmt::Write) -> fmt::Result { - let (words_iter, encoder) = Self::new(name, value); - encoder.format(words_iter, f) +impl<'a> HeaderValueEncoder<'a> { + fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result { + let (words_iter, encoder) = Self::new(name, value, f); + encoder.format(words_iter) } - fn new<'a>(name: &str, value: &'a str) -> (WordsPlusFillIterator<'a>, Self) { + fn new( + name: &str, + value: &'a str, + writer: &'a mut dyn Write, + ) -> (WordsPlusFillIterator<'a>, Self) { + let line_len = name.len() + ": ".len(); + let writer = EmailWriter::new(writer, line_len, false); + ( WordsPlusFillIterator { s: value }, Self { - line_len: name.len() + ": ".len(), + writer, encode_buf: String::new(), }, ) } - fn format( - mut self, - words_iter: WordsPlusFillIterator<'_>, - f: &mut impl fmt::Write, - ) -> fmt::Result { - /// Estimate if an encoded string of `len` would fix in an empty line - fn would_fit_new_line(len: usize) -> bool { - len < (MAX_LINE_LEN - " ".len()) - } - - /// Estimate how long a string of `len` would be after base64 encoding plus - /// adding the encoding prefix and suffix to it - fn base64_len(len: usize) -> usize { - ENCODING_START_PREFIX.len() + (len * 4 / 3 + 4) + ENCODING_END_SUFFIX.len() - } - - /// Estimate how many more bytes we can fit in the current line - fn available_len_to_max_encode_len(len: usize) -> usize { - len.saturating_sub( - ENCODING_START_PREFIX.len() + (len * 3 / 4 + 4) + ENCODING_END_SUFFIX.len(), - ) - } - + fn format(mut self, words_iter: WordsPlusFillIterator<'_>) -> fmt::Result { for next_word in words_iter { let allowed = allowed_str(next_word); @@ -371,161 +354,64 @@ impl HeaderValueEncoder { // This word only contains allowed characters // the next word is allowed, but we may have accumulated some words to encode - self.flush_encode_buf(f, true)?; + self.flush_encode_buf()?; - if next_word.len() > self.remaining_line_len() { - // not enough space left on this line to encode word - - if self.something_written_to_this_line() && would_fit_new_line(next_word.len()) - { - // word doesn't fit this line, but something had already been written to it, - // and word would fit the next line, so go to a new line - // so go to new line - self.new_line(f)?; - } else { - // word neither fits this line and the next one, cut it - // in the middle and make it fit - - let mut next_word = next_word; - - while !next_word.is_empty() { - if self.remaining_line_len() == 0 { - self.new_line(f)?; - } - - let len = self.remaining_line_len().min(next_word.len()); - let first_part = &next_word[..len]; - next_word = &next_word[len..]; - - f.write_str(first_part)?; - self.line_len += first_part.len(); - } - - continue; - } - } - - // word fits, write it! - f.write_str(next_word)?; - self.line_len += next_word.len(); + self.writer.folding().write_str(next_word)?; } else { // This word contains unallowed characters - - if self.remaining_line_len() >= base64_len(self.encode_buf.len() + next_word.len()) - { - // next_word fits - self.encode_buf.push_str(next_word); - continue; - } - - // next_word doesn't fit this line - - if would_fit_new_line(base64_len(next_word.len())) { - // ...but it would fit the next one - - self.flush_encode_buf(f, false)?; - self.new_line(f)?; - - self.encode_buf.push_str(next_word); - continue; - } - - // ...and also wouldn't fit the next one. - // chop it up into pieces - - let mut next_word = next_word; - - while !next_word.is_empty() { - let mut len = available_len_to_max_encode_len(self.remaining_line_len()) - .min(next_word.len()); - - if len == 0 { - self.flush_encode_buf(f, false)?; - self.new_line(f)?; - } - - // avoid slicing on a char boundary - while !next_word.is_char_boundary(len) { - len += 1; - } - let first_part = &next_word[..len]; - next_word = &next_word[len..]; - - self.encode_buf.push_str(first_part); - } + self.encode_buf.push_str(next_word); } } - self.flush_encode_buf(f, false)?; + self.flush_encode_buf()?; Ok(()) } - /// Returns the number of bytes left for the current line - fn remaining_line_len(&self) -> usize { - MAX_LINE_LEN - self.line_len - } - - /// Returns true if something has been written to the current line - fn something_written_to_this_line(&self) -> bool { - self.line_len > 1 - } - - fn flush_encode_buf( - &mut self, - f: &mut impl fmt::Write, - switching_to_allowed: bool, - ) -> fmt::Result { + fn flush_encode_buf(&mut self) -> fmt::Result { if self.encode_buf.is_empty() { // nothing to encode return Ok(()); } - let mut write_after = None; + // It is important that we don't encode leading whitespace otherwise it breaks wrapping. + let first_not_allowed = self + .encode_buf + .bytes() + .enumerate() + .find(|(_i, c)| !allowed_char(*c)) + .map(|(i, _)| i); + // May as well also write the tail in plain text. + let last_not_allowed = self + .encode_buf + .bytes() + .enumerate() + .rev() + .find(|(_i, c)| !allowed_char(*c)) + .map(|(i, _)| i + 1); - if switching_to_allowed { - // If the next word only contains allowed characters, and the string to encode - // ends with a space, take the space out of the part to encode + let (prefix, to_encode, suffix) = match first_not_allowed { + Some(first_not_allowed) => { + let last_not_allowed = last_not_allowed.unwrap(); - let last_char = self.encode_buf.pop().expect("self.encode_buf isn't empty"); - if is_space_like(last_char) { - write_after = Some(last_char); - } else { - self.encode_buf.push(last_char); + let (remaining, suffix) = self.encode_buf.split_at(last_not_allowed); + let (prefix, to_encode) = remaining.split_at(first_not_allowed); + + (prefix, to_encode, suffix) } - } + None => ("", self.encode_buf.as_str(), ""), + }; - f.write_str(ENCODING_START_PREFIX)?; - let encoded = base64::display::Base64Display::with_config( - self.encode_buf.as_bytes(), - base64::STANDARD, - ); - write!(f, "{}", encoded)?; - f.write_str(ENCODING_END_SUFFIX)?; - - self.line_len += ENCODING_START_PREFIX.len(); - self.line_len += self.encode_buf.len() * 4 / 3 + 4; - self.line_len += ENCODING_END_SUFFIX.len(); - - if let Some(write_after) = write_after { - f.write_char(write_after)?; - self.line_len += 1; - } + self.writer.folding().write_str(prefix)?; + email_encoding::headers::rfc2047::encode(to_encode, &mut self.writer)?; + self.writer.folding().write_str(suffix)?; self.encode_buf.clear(); Ok(()) } - - fn new_line(&mut self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str("\r\n ")?; - self.line_len = 1; - - Ok(()) - } } -/// Iterator yielding a string split space by space, but including all space -/// characters between it and the next word +/// Iterator yielding a string split by space, but spaces are included before the next word. struct WordsPlusFillIterator<'a> { s: &'a str, } @@ -540,31 +426,25 @@ impl<'a> Iterator for WordsPlusFillIterator<'a> { let next_word = self .s - .char_indices() + .bytes() + .enumerate() .skip(1) - .skip_while(|&(_i, c)| !is_space_like(c)) - .find(|&(_i, c)| !is_space_like(c)) - .map(|(i, _)| i); + .find(|&(_i, c)| c == b' ') + .map(|(i, _)| i) + .unwrap_or(self.s.len()); - let word = &self.s[..next_word.unwrap_or(self.s.len())]; + let word = &self.s[..next_word]; self.s = &self.s[word.len()..]; Some(word) } } -const fn is_space_like(c: char) -> bool { - c == ',' || c == ' ' -} - fn allowed_str(s: &str) -> bool { - s.chars().all(allowed_char) + s.bytes().all(allowed_char) } -const fn allowed_char(c: char) -> bool { - c >= 1 as char && c <= 9 as char - || c == 11 as char - || c == 12 as char - || c >= 14 as char && c <= 127 as char +const fn allowed_char(c: u8) -> bool { + c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127 } #[cfg(test)] @@ -654,8 +534,8 @@ mod tests { assert_eq!( headers.to_string(), concat!( - "To: Ascii , John Doe , Pinco Pallino , Jemand \r\n", + "To: Ascii , John Doe , Pinco Pallino , Jemand\r\n", " , Jean Dupont \r\n" ) ); @@ -672,8 +552,8 @@ mod tests { assert_eq!( headers.to_string(), concat!( - "Subject: Hello! This is lettre, and this \r\n ", - "IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n", + "Subject: Hello! This is lettre, and this\r\n", + " IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n", " guess that's it!\r\n" ) ); @@ -691,8 +571,9 @@ mod tests { assert_eq!( headers.to_string(), concat!( - "Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n", - " ouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know\r\n", + "Subject: Hello!\r\n", + " IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n", + " I don't know\r\n", ) ); } @@ -707,11 +588,7 @@ mod tests { assert_eq!( headers.to_string(), - concat!( - "Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n", - " mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n", - " ghijklmnopqrstuvwxyz\r\n", - ) + "Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n", ); } @@ -725,7 +602,7 @@ mod tests { assert_eq!( headers.to_string(), - "To: =?utf-8?b?U2XDoW4=?= \r\n" + "To: Se=?utf-8?b?w6E=?=n \r\n" ); } @@ -754,11 +631,11 @@ mod tests { assert_eq!( headers.to_string(), concat!( - "To: =?utf-8?b?8J+MjQ==?= , =?utf-8?b?8J+mhg==?= \r\n", - " Everywhere , =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n", - " =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= , \r\n", - " =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= , \r\n", - " =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= \r\n" + "To: =?utf-8?b?8J+MjQ==?= , =?utf-8?b?8J+mhg==?= Everywhere\r\n", + " , =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n", + " =?utf-8?b?0L7QstC40Yc=?= , J=?utf-8?b?xIFuaXMgQsST?=\r\n", + " =?utf-8?b?cnppxYbFoQ==?= , Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n", + " =?utf-8?b?ZGHDrQ==?= \r\n", ) ); } @@ -774,7 +651,14 @@ mod tests { assert_eq!( headers.to_string(), - "Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n" + concat!( + "Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n", + " =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n", + " =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n", + " =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n", + " =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n", + " =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+lsw==?=\r\n" + ) ); } @@ -819,14 +703,14 @@ mod tests { assert_eq!( headers.to_string(), concat!( - "Subject: Hello! This is lettre, and this \r\n", - " IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n", + "Subject: Hello! This is lettre, and this\r\n", + " IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n", " guess that's it!\r\n", - "To: =?utf-8?b?8J+MjQ==?= , =?utf-8?b?8J+mhg==?= \r\n", - " Everywhere , =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n", - " =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= , \r\n", - " =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= , \r\n", - " =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= \r\n", + "To: =?utf-8?b?8J+MjQ==?= , =?utf-8?b?8J+mhg==?= Everywhere\r\n", + " , =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n", + " =?utf-8?b?0L7QstC40Yc=?= , J=?utf-8?b?xIFuaXMgQsST?=\r\n", + " =?utf-8?b?cnppxYbFoQ==?= , Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n", + " =?utf-8?b?ZGHDrQ==?= \r\n", "From: Someone \r\n", "Content-Transfer-Encoding: quoted-printable\r\n", ) @@ -844,8 +728,8 @@ mod tests { assert_eq!( headers.to_string(), concat!( - "Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n", - " =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n" + "Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go;\r\n", + " ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg=?utf-8?b?772G44Gj?=\r\n" ) ); } diff --git a/src/message/mimebody.rs b/src/message/mimebody.rs index 896626b..5b7ca5f 100644 --- a/src/message/mimebody.rs +++ b/src/message/mimebody.rs @@ -479,7 +479,7 @@ mod test { assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( - "Content-Type: multipart/mixed; \r\n", + "Content-Type: multipart/mixed;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", @@ -526,8 +526,8 @@ mod test { assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( - "Content-Type: multipart/encrypted; \r\n", - " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n", + "Content-Type: multipart/encrypted;\r\n", + " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n", " protocol=\"application/pgp-encrypted\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", @@ -582,8 +582,8 @@ mod test { assert_eq!( String::from_utf8(part.formatted()).unwrap(), concat!( - "Content-Type: multipart/signed; \r\n", - " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n", + "Content-Type: multipart/signed;\r\n", + " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n", " protocol=\"application/pgp-signature\";", " micalg=\"pgp-sha256\"\r\n", "\r\n", @@ -624,7 +624,7 @@ mod test { .body(String::from("

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

"))); assert_eq!(String::from_utf8(part.formatted()).unwrap(), - concat!("Content-Type: multipart/alternative; \r\n", + concat!("Content-Type: multipart/alternative;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", @@ -662,11 +662,11 @@ mod test { .body(String::from("int main() { return 0; }"))); assert_eq!(String::from_utf8(part.formatted()).unwrap(), - concat!("Content-Type: multipart/mixed; \r\n", + concat!("Content-Type: multipart/mixed;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", - "Content-Type: multipart/related; \r\n", + "Content-Type: multipart/related;\r\n", " boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n", "\r\n", "--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n", diff --git a/src/message/mod.rs b/src/message/mod.rs index de0a041..60ffbe9 100644 --- a/src/message/mod.rs +++ b/src/message/mod.rs @@ -655,7 +655,7 @@ mod test { "Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n", "From: =?utf-8?b?0JrQsNC4?= \r\n", "To: \"Pony O.P.\" \r\n", - "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n", + "Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvQ==?=!\r\n", "Content-Transfer-Encoding: 7bit\r\n", "\r\n", "Happy new year!" diff --git a/testdata/email_with_png.eml b/testdata/email_with_png.eml index fc5f496..59c4d49 100644 --- a/testdata/email_with_png.eml +++ b/testdata/email_with_png.eml @@ -4,7 +4,7 @@ Reply-To: Yuin To: Hei Subject: Happy new year MIME-Version: 1.0 -Content-Type: multipart/related; +Content-Type: multipart/related; boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA" --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1