mirror of
https://github.com/chatmail/core.git
synced 2026-04-19 06:26:30 +03:00
227 lines
8.0 KiB
Rust
227 lines
8.0 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 = false;
|
|
|
|
for c in line.chars() {
|
|
if c == ' ' {
|
|
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 3767 (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(crate) 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_no_prefix = line.strip_prefix('>');
|
|
let is_quote = line_no_prefix.is_some();
|
|
let line = line_no_prefix.unwrap_or(line).trim();
|
|
let prefix = if is_quote { "> " } else { "" };
|
|
|
|
if prefix.len() + line.len() > 78 {
|
|
result += &format_line_flowed(line, prefix);
|
|
} else {
|
|
result += prefix;
|
|
if prefix.is_empty() && line.starts_with('>') {
|
|
// Space stuffing, see RFC 3676
|
|
result.push(' ');
|
|
}
|
|
result += line;
|
|
}
|
|
}
|
|
|
|
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.
|
|
///
|
|
/// Quote processing is not supported, it is assumed that they are
|
|
/// deleted during simplification.
|
|
///
|
|
/// 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') {
|
|
// 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::*;
|
|
use crate::test_utils::TestContext;
|
|
|
|
#[test]
|
|
fn test_format_flowed() {
|
|
let text = "Foo bar baz";
|
|
assert_eq!(format_flowed(text), "Foo bar baz");
|
|
|
|
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");
|
|
|
|
// 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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_quotes() -> anyhow::Result<()> {
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
let chat = alice.create_chat(&bob).await;
|
|
|
|
let sent = alice.send_text(chat.id, "> First quote").await;
|
|
let received = bob.recv_msg(&sent).await;
|
|
assert_eq!(received.text.as_deref(), Some("> First quote"));
|
|
assert!(received.quoted_text().is_none());
|
|
assert!(received.quoted_message(&bob).await?.is_none());
|
|
|
|
let sent = alice.send_text(chat.id, "> Second quote").await;
|
|
let received = bob.recv_msg(&sent).await;
|
|
assert_eq!(received.text.as_deref(), Some("> Second quote"));
|
|
assert!(received.quoted_text().is_none());
|
|
assert!(received.quoted_message(&bob).await?.is_none());
|
|
|
|
Ok(())
|
|
}
|
|
}
|