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 <kevincox@kevincox.ca>
This commit is contained in:
@@ -30,7 +30,7 @@ fastrand = { version = "1.4", optional = true }
|
|||||||
quoted_printable = { version = "0.4", optional = true }
|
quoted_printable = { version = "0.4", optional = true }
|
||||||
base64 = { version = "0.13", optional = true }
|
base64 = { version = "0.13", optional = true }
|
||||||
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
|
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
|
# file transport
|
||||||
uuid = { version = "1", features = ["v4"], optional = true }
|
uuid = { version = "1", features = ["v4"], optional = true }
|
||||||
@@ -88,7 +88,7 @@ name = "transport_smtp"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
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"]
|
mime03 = ["mime"]
|
||||||
|
|
||||||
# transports
|
# 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-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
||||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
||||||
|
|
||||||
dkim = ["sha2", "rsa", "ed25519-dalek"]
|
dkim = ["base64", "sha2", "rsa", "ed25519-dalek"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
|||||||
fn test_headers_simple_canonicalize() {
|
fn test_headers_simple_canonicalize() {
|
||||||
let message = test_message();
|
let message = test_message();
|
||||||
dbg!(message.headers.to_string());
|
dbg!(message.headers.to_string());
|
||||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\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?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -521,18 +521,14 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
|||||||
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
|
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
|
||||||
"To: Test2 <test2@example.org>\r\n",
|
"To: Test2 <test2@example.org>\r\n",
|
||||||
"Date: Thu, 01 Jan 1970 00:00:00 +0000\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",
|
" folded to several lines \r\n",
|
||||||
"Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
|
"Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest; \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",
|
" c=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To;\r\n",
|
||||||
" bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=; b=UQWpUooKjVzgC7jtuEKdpCbz\r\n",
|
" bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=;\r\n",
|
||||||
" sSDnOqZFg8+S7rZj89n/+AVsdcwxumeLCUYLeko2TZgVFJA7kGz+wLzH2wpzB4XnyUqrkF6PrFA\r\n",
|
" b=NhoIMMAALoSgu5lKAR0+MUQunOWnU7wpF9ORUFtpxq9sGZDo9AX43AMhFemyM5W204jpFwMU6pm7AMR1nOYBdSYye4yUALtvT2nqbJBwSh7JeYu+z22t1RFKp7qQR1il8aSrkbZuNMFHYuSEwW76QtKwcNqP4bQOzS9CzgQp0ABu8qwYPBr/EypykPTfqjtyN+ywrfdqjjGOzTpRGolH0hc3CrAETNjjHbNBgKgucXmXTN7hMRdzqWjeFPxizXwouwNAavFClPG0l33gXVArFWn+CkgA84G/s4zuJiF7QPZR87Pu4pw/vIlSXxH4a42W3tT19v9iBTH7X7ldYegtmQ==\r\n",
|
||||||
" 9K11K365JDtzfMSc5eRVS8crO6F/A9QtXPndnzrXQ5HrtFgfxUlJ9cX6pTOor1NVCpfYUNBviIg\r\n",
|
|
||||||
" Am0LnUOKdlJ8z82kLFRpIqawMKNVfyqP8Es6H3NHM4Y1uwGgls9DM1+lZxNXxkMpoGV3rL/n/ai\r\n",
|
|
||||||
" s8+VifrRxPLB0mr9gbavSkCQ2QzUA/+iq8DgPCGpXDrdDrwTcrV3pL/iHyEjQZWwFSQkx+r/CGb\r\n",
|
|
||||||
" 8TQLqH6T3wfr69XWvg==\r\n",
|
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"test\r\n",
|
"test\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
@@ -575,18 +571,13 @@ cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
|||||||
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
|
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
|
||||||
"To: Test2 <test2@example.org>\r\n",
|
"To: Test2 <test2@example.org>\r\n",
|
||||||
"Date: Thu, 01 Jan 1970 00:00:00 +0000\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",
|
" folded to several lines \r\n","Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
|
||||||
"Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
|
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest; \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",
|
" c=relaxed/relaxed; q=dns/txt; t=0; h=date:from:subject:to;\r\n",
|
||||||
" bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=; b=YaVfmH8dbGEywoLJ4uhbvYqD\r\n",
|
" bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=;\r\n",
|
||||||
" yQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FE\r\n",
|
" b=YaVfmH8dbGEywoLJ4uhbvYqDyQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FEh6j3RQthqNbQblcfH/U8mazTuRbVjYBbTZQDaQCMPTz+8D+ZQfXo2oq6dGzTuGvmuYft0CVsq/BIp/EkhZHqiphDeVJSHD4iKW8+L2XwEWThoY92xOYc1G0TtBwz2UJgtiHX2YulH/kRBHeK3dKn9RTNVL3VZ+9ZrnFwIhET9TPGtU2I+q0EMSWF9H9bTrASMgW/U+E0VM2btqJlrTU6rQ7wlQeHdwecLnzXcyhCUInF1+veMNw==\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",
|
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"test\r\n",
|
"test\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt::{self, Display, Formatter},
|
fmt::{self, Display, Formatter, Write},
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use email_encoding::headers::EmailWriter;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
content::*,
|
content::*,
|
||||||
content_disposition::ContentDisposition,
|
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
|
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
|
||||||
struct HeaderValueEncoder {
|
struct HeaderValueEncoder<'a> {
|
||||||
line_len: usize,
|
writer: EmailWriter<'a>,
|
||||||
encode_buf: String,
|
encode_buf: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HeaderValueEncoder {
|
impl<'a> HeaderValueEncoder<'a> {
|
||||||
fn encode(name: &str, value: &str, f: &mut impl fmt::Write) -> fmt::Result {
|
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
|
||||||
let (words_iter, encoder) = Self::new(name, value);
|
let (words_iter, encoder) = Self::new(name, value, f);
|
||||||
encoder.format(words_iter, 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 },
|
WordsPlusFillIterator { s: value },
|
||||||
Self {
|
Self {
|
||||||
line_len: name.len() + ": ".len(),
|
writer,
|
||||||
encode_buf: String::new(),
|
encode_buf: String::new(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(
|
fn format(mut self, words_iter: WordsPlusFillIterator<'_>) -> fmt::Result {
|
||||||
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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for next_word in words_iter {
|
for next_word in words_iter {
|
||||||
let allowed = allowed_str(next_word);
|
let allowed = allowed_str(next_word);
|
||||||
|
|
||||||
@@ -371,161 +354,64 @@ impl HeaderValueEncoder {
|
|||||||
// This word only contains allowed characters
|
// This word only contains allowed characters
|
||||||
|
|
||||||
// the next word is allowed, but we may have accumulated some words to encode
|
// 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() {
|
self.writer.folding().write_str(next_word)?;
|
||||||
// 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();
|
|
||||||
} else {
|
} else {
|
||||||
// This word contains unallowed characters
|
// 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);
|
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.flush_encode_buf(f, false)?;
|
self.flush_encode_buf()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of bytes left for the current line
|
fn flush_encode_buf(&mut self) -> fmt::Result {
|
||||||
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 {
|
|
||||||
if self.encode_buf.is_empty() {
|
if self.encode_buf.is_empty() {
|
||||||
// nothing to encode
|
// nothing to encode
|
||||||
return Ok(());
|
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 {
|
let (prefix, to_encode, suffix) = match first_not_allowed {
|
||||||
// If the next word only contains allowed characters, and the string to encode
|
Some(first_not_allowed) => {
|
||||||
// ends with a space, take the space out of the part to encode
|
let last_not_allowed = last_not_allowed.unwrap();
|
||||||
|
|
||||||
let last_char = self.encode_buf.pop().expect("self.encode_buf isn't empty");
|
let (remaining, suffix) = self.encode_buf.split_at(last_not_allowed);
|
||||||
if is_space_like(last_char) {
|
let (prefix, to_encode) = remaining.split_at(first_not_allowed);
|
||||||
write_after = Some(last_char);
|
|
||||||
} else {
|
(prefix, to_encode, suffix)
|
||||||
self.encode_buf.push(last_char);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
None => ("", self.encode_buf.as_str(), ""),
|
||||||
|
};
|
||||||
|
|
||||||
f.write_str(ENCODING_START_PREFIX)?;
|
self.writer.folding().write_str(prefix)?;
|
||||||
let encoded = base64::display::Base64Display::with_config(
|
email_encoding::headers::rfc2047::encode(to_encode, &mut self.writer)?;
|
||||||
self.encode_buf.as_bytes(),
|
self.writer.folding().write_str(suffix)?;
|
||||||
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.encode_buf.clear();
|
self.encode_buf.clear();
|
||||||
Ok(())
|
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
|
/// Iterator yielding a string split by space, but spaces are included before the next word.
|
||||||
/// characters between it and the next word
|
|
||||||
struct WordsPlusFillIterator<'a> {
|
struct WordsPlusFillIterator<'a> {
|
||||||
s: &'a str,
|
s: &'a str,
|
||||||
}
|
}
|
||||||
@@ -540,31 +426,25 @@ impl<'a> Iterator for WordsPlusFillIterator<'a> {
|
|||||||
|
|
||||||
let next_word = self
|
let next_word = self
|
||||||
.s
|
.s
|
||||||
.char_indices()
|
.bytes()
|
||||||
|
.enumerate()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.skip_while(|&(_i, c)| !is_space_like(c))
|
.find(|&(_i, c)| c == b' ')
|
||||||
.find(|&(_i, c)| !is_space_like(c))
|
.map(|(i, _)| i)
|
||||||
.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()..];
|
self.s = &self.s[word.len()..];
|
||||||
Some(word)
|
Some(word)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn is_space_like(c: char) -> bool {
|
|
||||||
c == ',' || c == ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
fn allowed_str(s: &str) -> bool {
|
fn allowed_str(s: &str) -> bool {
|
||||||
s.chars().all(allowed_char)
|
s.bytes().all(allowed_char)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn allowed_char(c: char) -> bool {
|
const fn allowed_char(c: u8) -> bool {
|
||||||
c >= 1 as char && c <= 9 as char
|
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
|
||||||
|| c == 11 as char
|
|
||||||
|| c == 12 as char
|
|
||||||
|| c >= 14 as char && c <= 127 as char
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -654,8 +534,8 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith \r\n",
|
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith\r\n",
|
||||||
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand \r\n",
|
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand\r\n",
|
||||||
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
|
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -672,8 +552,8 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: Hello! This is lettre, and this \r\n ",
|
"Subject: Hello! This is lettre, and this\r\n",
|
||||||
"IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n",
|
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||||
" guess that's it!\r\n"
|
" guess that's it!\r\n"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -691,8 +571,9 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoY\r\n",
|
"Subject: Hello!\r\n",
|
||||||
" ouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know\r\n",
|
" IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n",
|
||||||
|
" I don't know\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -707,11 +588,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n",
|
||||||
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijkl\r\n",
|
|
||||||
" mnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdef\r\n",
|
|
||||||
" ghijklmnopqrstuvwxyz\r\n",
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,7 +602,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
|
"To: Se=?utf-8?b?w6E=?=n <sean@example.com>\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,11 +631,11 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= Everywhere\r\n",
|
||||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
|
" <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n",
|
||||||
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
|
" =?utf-8?b?0L7QstC40Yc=?= <ivanov@example.com>, J=?utf-8?b?xIFuaXMgQsST?=\r\n",
|
||||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
|
" =?utf-8?b?cnppxYbFoQ==?= <janis@example.com>, Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n",
|
||||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n"
|
" =?utf-8?b?ZGHDrQ==?= <sean@example.com>\r\n",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -774,7 +651,14 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
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!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: Hello! This is lettre, and this \r\n",
|
"Subject: Hello! This is lettre, and this\r\n",
|
||||||
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I \r\n",
|
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||||
" guess that's it!\r\n",
|
" guess that's it!\r\n",
|
||||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= \r\n",
|
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?= Everywhere\r\n",
|
||||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyIA==?=\r\n",
|
" <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9INCY0LLQsNC9?=\r\n",
|
||||||
" =?utf-8?b?0JjQstCw0L0g0JjQstCw0L3QvtCy0LjRhw==?= <ivanov@example.com>, \r\n",
|
" =?utf-8?b?0L7QstC40Yc=?= <ivanov@example.com>, J=?utf-8?b?xIFuaXMgQsST?=\r\n",
|
||||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, \r\n",
|
" =?utf-8?b?cnppxYbFoQ==?= <janis@example.com>, Se=?utf-8?b?w6FuIMOTIFJ1?=\r\n",
|
||||||
" =?utf-8?b?U2XDoW4gw5MgUnVkYcOt?= <sean@example.com>\r\n",
|
" =?utf-8?b?ZGHDrQ==?= <sean@example.com>\r\n",
|
||||||
"From: Someone <somewhere@example.com>\r\n",
|
"From: Someone <somewhere@example.com>\r\n",
|
||||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||||
)
|
)
|
||||||
@@ -844,8 +728,8 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
headers.to_string(),
|
headers.to_string(),
|
||||||
concat!(
|
concat!(
|
||||||
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; \r\n",
|
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go;\r\n",
|
||||||
" =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n"
|
" ;;;;;s;;;;;;;;;;;;;;;;fffeinmjggggggggg=?utf-8?b?772G44Gj?=\r\n"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ mod test {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8(part.formatted()).unwrap(),
|
String::from_utf8(part.formatted()).unwrap(),
|
||||||
concat!(
|
concat!(
|
||||||
"Content-Type: multipart/mixed; \r\n",
|
"Content-Type: multipart/mixed;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
@@ -526,8 +526,8 @@ mod test {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8(part.formatted()).unwrap(),
|
String::from_utf8(part.formatted()).unwrap(),
|
||||||
concat!(
|
concat!(
|
||||||
"Content-Type: multipart/encrypted; \r\n",
|
"Content-Type: multipart/encrypted;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
|
||||||
" protocol=\"application/pgp-encrypted\"\r\n",
|
" protocol=\"application/pgp-encrypted\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
@@ -582,8 +582,8 @@ mod test {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
String::from_utf8(part.formatted()).unwrap(),
|
String::from_utf8(part.formatted()).unwrap(),
|
||||||
concat!(
|
concat!(
|
||||||
"Content-Type: multipart/signed; \r\n",
|
"Content-Type: multipart/signed;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"; \r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
|
||||||
" protocol=\"application/pgp-signature\";",
|
" protocol=\"application/pgp-signature\";",
|
||||||
" micalg=\"pgp-sha256\"\r\n",
|
" micalg=\"pgp-sha256\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
@@ -624,7 +624,7 @@ mod test {
|
|||||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
|
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
|
||||||
|
|
||||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
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",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
@@ -662,11 +662,11 @@ mod test {
|
|||||||
.body(String::from("int main() { return 0; }")));
|
.body(String::from("int main() { return 0; }")));
|
||||||
|
|
||||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
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",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
"Content-Type: multipart/related; \r\n",
|
"Content-Type: multipart/related;\r\n",
|
||||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||||
|
|||||||
@@ -655,7 +655,7 @@ mod test {
|
|||||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
||||||
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
|
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
|
||||||
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
|
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvQ==?=!\r\n",
|
||||||
"Content-Transfer-Encoding: 7bit\r\n",
|
"Content-Transfer-Encoding: 7bit\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
"Happy new year!"
|
"Happy new year!"
|
||||||
|
|||||||
Reference in New Issue
Block a user