Move format_flowed to a separate crate

This makes it possible to fuzz test the functions
without exposing the module interface in the deltachat core
interface.

Also ensure that format_flowed will not grow a dependency
on deltachat core types.
This commit is contained in:
link2xt
2022-12-26 13:34:38 +00:00
parent 6dc790f447
commit 89b7ce4c4e
9 changed files with 70 additions and 54 deletions

View File

@@ -1,270 +0,0 @@
//! # 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 == ' ' {
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 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('>')
.map(|line| line.strip_prefix(' ').unwrap_or(line));
let is_quote = line_no_prefix.is_some();
let line = line_no_prefix.unwrap_or(line).trim_end();
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('>') || 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;
use anyhow::Result;
#[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 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");
}
#[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);
}
#[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() -> 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(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_format_flowed_round_trip() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat = alice.create_chat(&bob).await;
let text = " Foo bar";
let sent = alice.send_text(chat.id, text).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(text));
let text = "Foo bar baz";
let sent = alice.send_text(chat.id, text).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(text));
let python_program = "\
def hello():
return 'Hello, world!'";
let sent = alice.send_text(chat.id, python_program).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(python_program));
Ok(())
}
}

View File

@@ -74,7 +74,6 @@ pub mod imex;
mod scheduler;
#[macro_use]
mod job;
mod format_flowed;
pub mod key;
mod keyring;
pub mod location;

View File

@@ -2340,4 +2340,51 @@ mod tests {
);
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_quotes() -> 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(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_format_flowed_round_trip() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat = alice.create_chat(&bob).await;
let text = " Foo bar";
let sent = alice.send_text(chat.id, text).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(text));
let text = "Foo bar baz";
let sent = alice.send_text(chat.id, text).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(text));
let python_program = "\
def hello():
return 'Hello, world!'";
let sent = alice.send_text(chat.id, python_program).await;
let received = bob.recv_msg(&sent).await;
assert_eq!(received.text.as_deref(), Some(python_program));
Ok(())
}
}

View File

@@ -4,6 +4,7 @@ use std::convert::TryInto;
use anyhow::{bail, ensure, Context as _, Result};
use chrono::TimeZone;
use format_flowed::{format_flowed, format_flowed_quote};
use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder};
use tokio::fs;
@@ -15,7 +16,6 @@ use crate::contact::Contact;
use crate::context::{get_version_str, Context};
use crate::e2ee::EncryptHelper;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::format_flowed::{format_flowed, format_flowed_quote};
use crate::html::new_html_mimepart;
use crate::location;
use crate::message::{self, Message, MsgId, Viewtype};

View File

@@ -9,6 +9,7 @@ use std::str;
use anyhow::{bail, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use format_flowed::unformat_flowed;
use lettre_email::mime::{self, Mime};
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use once_cell::sync::Lazy;
@@ -24,7 +25,6 @@ use crate::decrypt::{
};
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::format_flowed::unformat_flowed;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;