From b33dd562fce43cc292d59b51ab72a6550595ca40 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Fri, 3 Jun 2022 15:24:53 +0200 Subject: [PATCH] Fix and improve header wrapping (#774) Instead of injecting spaces to ensure that lines stay under 76 characters only wrap at whitespace characters. This avoids changing the headers. A best-effort to keep lines under 76 characters is still done, however it is only done at whitespace. Notably there is no hard wrap enforced. This means that it is possible for headers to break the 1000 character line-length limit in the specification. It is just hoped that the receiver will allow long lines in this case. Closes #688 Co-authored-by: Kevin Cox --- Cargo.toml | 6 +- src/message/dkim.rs | 33 ++-- src/message/header/mod.rs | 300 +++++++++++------------------------- src/message/mimebody.rs | 16 +- src/message/mod.rs | 2 +- testdata/email_with_png.eml | 2 +- 6 files changed, 117 insertions(+), 242 deletions(-) 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