mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 13:36:30 +03:00
262 lines
9.3 KiB
Rust
262 lines
9.3 KiB
Rust
//! # format=flowed support.
|
|
//!
|
|
//! Format=flowed is defined in
|
|
//! [RFC 3676](https://tools.ietf.org/html/rfc3676).
|
|
//!
|
|
//! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
|
|
//! during formatting, i.e., DelSp parameter introduced in RFC 3676
|
|
//! is assumed to be set to "no".
|
|
//!
|
|
//! For received messages, DelSp parameter is honoured.
|
|
|
|
/// Wraps line to 72 characters using format=flowed soft breaks.
|
|
///
|
|
/// 72 characters is the limit recommended by RFC 3676.
|
|
///
|
|
/// The function breaks line only after SP and before non-whitespace
|
|
/// characters. It also does not insert breaks before ">" to avoid the
|
|
/// need to do space stuffing (see RFC 3676) for quotes.
|
|
///
|
|
/// If there are long words, line may still exceed the limits on line
|
|
/// length. However, this should be rare and should not result in
|
|
/// immediate mail rejection: SMTP (RFC 2821) limit is 998 characters,
|
|
/// and Spam Assassin limit is 78 characters.
|
|
fn format_line_flowed(line: &str, prefix: &str) -> String {
|
|
let mut result = String::new();
|
|
let mut buffer = prefix.to_string();
|
|
let mut after_space = prefix.ends_with(' ');
|
|
|
|
for c in line.chars() {
|
|
if c == ' ' {
|
|
if buffer.is_empty() {
|
|
// Space stuffing, see RFC 3676
|
|
buffer.push(' ');
|
|
}
|
|
buffer.push(c);
|
|
after_space = true;
|
|
} else if c == '>' {
|
|
if buffer.is_empty() {
|
|
// Space stuffing, see RFC 3676
|
|
buffer.push(' ');
|
|
}
|
|
buffer.push(c);
|
|
after_space = false;
|
|
} else {
|
|
if after_space && buffer.len() >= 72 && !c.is_whitespace() {
|
|
// Flush the buffer and insert soft break (SP CRLF).
|
|
result += &buffer;
|
|
result += "\r\n";
|
|
buffer = prefix.to_string();
|
|
}
|
|
buffer.push(c);
|
|
after_space = false;
|
|
}
|
|
}
|
|
result + &buffer
|
|
}
|
|
|
|
/// Returns text formatted according to RFC 3676 (format=flowed).
|
|
///
|
|
/// This function accepts text separated by LF, but returns text
|
|
/// separated by CRLF.
|
|
///
|
|
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
|
|
/// SHOULD be set to "no" when sending.
|
|
pub fn format_flowed(text: &str) -> String {
|
|
let mut result = String::new();
|
|
|
|
for line in text.split('\n') {
|
|
if !result.is_empty() {
|
|
result += "\r\n";
|
|
}
|
|
|
|
let line = line.trim_end();
|
|
let quote_depth = line.chars().take_while(|&c| c == '>').count();
|
|
let (prefix, mut line) = line.split_at(quote_depth);
|
|
|
|
let mut prefix = prefix.to_string();
|
|
|
|
if quote_depth > 0 {
|
|
if let Some(s) = line.strip_prefix(' ') {
|
|
line = s;
|
|
prefix += " ";
|
|
}
|
|
}
|
|
|
|
result += &format_line_flowed(line, &prefix);
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Same as format_flowed(), but adds "> " prefix to each line.
|
|
pub fn format_flowed_quote(text: &str) -> String {
|
|
let mut result = String::new();
|
|
|
|
for line in text.split('\n') {
|
|
if !result.is_empty() {
|
|
result += "\n";
|
|
}
|
|
result += "> ";
|
|
result += line;
|
|
}
|
|
|
|
format_flowed(&result)
|
|
}
|
|
|
|
/// Joins lines in format=flowed text.
|
|
///
|
|
/// Lines must be separated by single LF.
|
|
///
|
|
/// Signature separator line is not processed here, it is assumed to
|
|
/// be stripped beforehand.
|
|
pub fn unformat_flowed(text: &str, delsp: bool) -> String {
|
|
let mut result = String::new();
|
|
let mut skip_newline = true;
|
|
|
|
for line in text.split('\n') {
|
|
let line = if !result.is_empty() && skip_newline {
|
|
line.trim_start_matches('>')
|
|
} else {
|
|
line
|
|
};
|
|
|
|
// Revert space-stuffing
|
|
let line = line.strip_prefix(' ').unwrap_or(line);
|
|
|
|
if !skip_newline {
|
|
result.push('\n');
|
|
}
|
|
|
|
if let Some(line) = line.strip_suffix(' ') {
|
|
// Flowed line
|
|
result += line;
|
|
if !delsp {
|
|
result.push(' ');
|
|
}
|
|
skip_newline = true;
|
|
} else {
|
|
// Fixed line
|
|
result += line;
|
|
skip_newline = false;
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_format_flowed() {
|
|
let text = "";
|
|
assert_eq!(format_flowed(text), "");
|
|
|
|
let text = "Foo bar baz";
|
|
assert_eq!(format_flowed(text), text);
|
|
|
|
let text = ">Foo bar";
|
|
assert_eq!(format_flowed(text), text);
|
|
|
|
let text = "> Foo bar";
|
|
assert_eq!(format_flowed(text), text);
|
|
|
|
let text = ">\n\nA";
|
|
assert_eq!(format_flowed(text), ">\r\n\r\nA");
|
|
|
|
let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
|
\n\
|
|
To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
|
|
let expected = "This is the Autocrypt Setup Message used to transfer your key between clients.\r\n\
|
|
\r\n\
|
|
To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
|
|
client and enter the setup code presented on the generating device.";
|
|
assert_eq!(format_flowed(text), expected);
|
|
|
|
let text = "> A quote";
|
|
assert_eq!(format_flowed(text), "> A quote");
|
|
|
|
let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
|
|
assert_eq!(
|
|
format_flowed(text),
|
|
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \r\n> A"
|
|
);
|
|
|
|
// Test space stuffing of wrapped lines
|
|
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
|
> \n\
|
|
> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
|
|
let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\
|
|
> clients.\r\n\
|
|
>\r\n\
|
|
> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
|
|
> client and enter the setup code presented on the generating device.";
|
|
assert_eq!(format_flowed(text), expected);
|
|
|
|
let text = ">> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
|
>> \n\
|
|
>> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
|
|
let expected = ">> This is the Autocrypt Setup Message used to transfer your key between \r\n\
|
|
>> clients.\r\n\
|
|
>>\r\n\
|
|
>> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
|
|
>> client and enter the setup code presented on the generating device.";
|
|
assert_eq!(format_flowed(text), expected);
|
|
|
|
// Test space stuffing of spaces.
|
|
let text = " Foo bar baz";
|
|
assert_eq!(format_flowed(text), " Foo bar baz");
|
|
|
|
let text = " Foo bar baz";
|
|
assert_eq!(format_flowed(text), " Foo bar baz");
|
|
|
|
let text =
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA";
|
|
let expected =
|
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \r\nAAAAAA";
|
|
assert_eq!(format_flowed(text), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_unformat_flowed() {
|
|
let text = "this is a very long message that should be wrapped using format=flowed and \n\
|
|
unwrapped on the receiver";
|
|
let expected =
|
|
"this is a very long message that should be wrapped using format=flowed and \
|
|
unwrapped on the receiver";
|
|
assert_eq!(unformat_flowed(text, false), expected);
|
|
|
|
let text = " Foo bar";
|
|
let expected = " Foo bar";
|
|
assert_eq!(unformat_flowed(text, false), expected);
|
|
|
|
let text =
|
|
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \n> A";
|
|
let expected =
|
|
"> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
|
|
assert_eq!(unformat_flowed(text, false), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_flowed_quote() {
|
|
let quote = "this is a quoted line";
|
|
let expected = "> this is a quoted line";
|
|
assert_eq!(format_flowed_quote(quote), expected);
|
|
|
|
let quote = "first quoted line\nsecond quoted line";
|
|
let expected = "> first quoted line\r\n> second quoted line";
|
|
assert_eq!(format_flowed_quote(quote), expected);
|
|
|
|
let quote = "> foo bar baz";
|
|
let expected = "> > foo bar baz";
|
|
assert_eq!(format_flowed_quote(quote), expected);
|
|
|
|
let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver";
|
|
let expected =
|
|
"> this is a very long quote that should be wrapped using format=flowed and \r\n\
|
|
> unwrapped on the receiver";
|
|
assert_eq!(format_flowed_quote(quote), expected);
|
|
}
|
|
}
|