diff --git a/CHANGELOG.md b/CHANGELOG.md index b9ffb184f..6f8a72841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - limit the rate of MDN sending #3402 - ignore ratelimits for bots #3439 - remove `msgs_mdns` references to deleted messages during housekeeping #3387 +- format message lines starting with `>` as quotes #3434 ### Fixes - set a default error if NDN does not provide an error diff --git a/src/format_flowed.rs b/src/format_flowed.rs index 30249ecbd..a58bb8ea7 100644 --- a/src/format_flowed.rs +++ b/src/format_flowed.rs @@ -51,14 +51,26 @@ fn format_line_flowed(line: &str, prefix: &str) -> String { result + &buffer } -fn format_flowed_prefix(text: &str, prefix: &str) -> String { +/// 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 = line.trim_end(); + + 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 { @@ -70,23 +82,23 @@ fn format_flowed_prefix(text: &str, prefix: &str) -> String { result += line; } } - result -} -/// 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 fn format_flowed(text: &str) -> String { - format_flowed_prefix(text, "") + result } /// Same as format_flowed(), but adds "> " prefix to each line. pub fn format_flowed_quote(text: &str) -> String { - format_flowed_prefix(text, "> ") + 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. @@ -129,6 +141,7 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String { #[cfg(test)] mod tests { use super::*; + use crate::test_utils::TestContext; #[test] fn test_format_flowed() { @@ -144,18 +157,18 @@ mod tests { client and enter the setup code presented on the generating device."; assert_eq!(format_flowed(text), expected); - let text = "> Not a quote"; - assert_eq!(format_flowed(text), " > Not a quote"); + 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 = "\x20> This is the Autocrypt Setup Message used to transfer your key between \r\n\ - clients.\r\n\ - \x20>\r\n\ - \x20> 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."; + 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); } @@ -175,6 +188,10 @@ mod tests { 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); @@ -185,4 +202,25 @@ mod tests { > unwrapped on the receiver"; assert_eq!(format_flowed_quote(quote), expected); } + + #[async_std::test] + 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(()) + } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 4dd2c3021..fd8cde331 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1085,10 +1085,15 @@ impl<'a> MimeFactory<'a> { } }; - let quoted_text = self + let mut quoted_text = self .msg .quoted_text() .map(|quote| format_flowed_quote("e) + "\r\n\r\n"); + if quoted_text.is_none() && final_text.starts_with('>') { + // Insert empty line to avoid receiver treating user-sent quote as topquote inserted by + // Delta Chat. + quoted_text = Some("\r\n".to_string()); + } let flowed_text = format_flowed(final_text); let footer = &self.selfstatus; diff --git a/src/simplify.rs b/src/simplify.rs index f3f2a555c..36daa333b 100644 --- a/src/simplify.rs +++ b/src/simplify.rs @@ -204,13 +204,11 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option) { first_quoted_line = l; } last_quoted_line = Some(l) - } else if !is_empty_line(line) { - if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() { - has_quoted_headline = true - } else { - /* non-quoting line found */ - break; - } + } else if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() { + has_quoted_headline = true + } else { + /* non-quoting line found */ + break; } } if let Some(last_quoted_line) = last_quoted_line {