mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 17:36:29 +03:00
Format message lines starting with > as quotes
This makes quotes created by user display properly in other MUAs like Thunderbird and not start with space in MUAs that don't support format=flowed. To distinguish user-created quotes from top-quote inserted by Delta Chat, a newline is inserted if there is no top-quote and the first line starts with ">". Nested quotes are not supported, e.g. line starting with "> >" will start with ">" on the next line if wrapped.
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
- limit the rate of MDN sending #3402
|
- limit the rate of MDN sending #3402
|
||||||
- ignore ratelimits for bots #3439
|
- ignore ratelimits for bots #3439
|
||||||
- remove `msgs_mdns` references to deleted messages during housekeeping #3387
|
- remove `msgs_mdns` references to deleted messages during housekeeping #3387
|
||||||
|
- format message lines starting with `>` as quotes #3434
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- set a default error if NDN does not provide an error
|
- set a default error if NDN does not provide an error
|
||||||
|
|||||||
@@ -51,14 +51,26 @@ fn format_line_flowed(line: &str, prefix: &str) -> String {
|
|||||||
result + &buffer
|
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();
|
let mut result = String::new();
|
||||||
|
|
||||||
for line in text.split('\n') {
|
for line in text.split('\n') {
|
||||||
if !result.is_empty() {
|
if !result.is_empty() {
|
||||||
result += "\r\n";
|
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 {
|
if prefix.len() + line.len() > 78 {
|
||||||
result += &format_line_flowed(line, prefix);
|
result += &format_line_flowed(line, prefix);
|
||||||
} else {
|
} else {
|
||||||
@@ -70,23 +82,23 @@ fn format_flowed_prefix(text: &str, prefix: &str) -> String {
|
|||||||
result += line;
|
result += line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns text formatted according to RFC 3767 (format=flowed).
|
result
|
||||||
///
|
|
||||||
/// 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, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Same as format_flowed(), but adds "> " prefix to each line.
|
/// Same as format_flowed(), but adds "> " prefix to each line.
|
||||||
pub fn format_flowed_quote(text: &str) -> String {
|
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.
|
/// Joins lines in format=flowed text.
|
||||||
@@ -129,6 +141,7 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::test_utils::TestContext;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_flowed() {
|
fn test_format_flowed() {
|
||||||
@@ -144,18 +157,18 @@ mod tests {
|
|||||||
client and enter the setup code presented on the generating device.";
|
client and enter the setup code presented on the generating device.";
|
||||||
assert_eq!(format_flowed(text), expected);
|
assert_eq!(format_flowed(text), expected);
|
||||||
|
|
||||||
let text = "> Not a quote";
|
let text = "> A quote";
|
||||||
assert_eq!(format_flowed(text), " > Not a quote");
|
assert_eq!(format_flowed(text), "> A quote");
|
||||||
|
|
||||||
// Test space stuffing of wrapped lines
|
// Test space stuffing of wrapped lines
|
||||||
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
|
||||||
> \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.";
|
> 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\
|
let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\
|
||||||
clients.\r\n\
|
> clients.\r\n\
|
||||||
\x20>\r\n\
|
> \r\n\
|
||||||
\x20> To decrypt and use your key, open the message in an Autocrypt-compliant \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.";
|
> client and enter the setup code presented on the generating device.";
|
||||||
assert_eq!(format_flowed(text), expected);
|
assert_eq!(format_flowed(text), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +188,10 @@ mod tests {
|
|||||||
let expected = "> this is a quoted line";
|
let expected = "> this is a quoted line";
|
||||||
assert_eq!(format_flowed_quote(quote), expected);
|
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 quote = "> foo bar baz";
|
||||||
let expected = "> > foo bar baz";
|
let expected = "> > foo bar baz";
|
||||||
assert_eq!(format_flowed_quote(quote), expected);
|
assert_eq!(format_flowed_quote(quote), expected);
|
||||||
@@ -185,4 +202,25 @@ mod tests {
|
|||||||
> unwrapped on the receiver";
|
> unwrapped on the receiver";
|
||||||
assert_eq!(format_flowed_quote(quote), expected);
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1085,10 +1085,15 @@ impl<'a> MimeFactory<'a> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let quoted_text = self
|
let mut quoted_text = self
|
||||||
.msg
|
.msg
|
||||||
.quoted_text()
|
.quoted_text()
|
||||||
.map(|quote| format_flowed_quote("e) + "\r\n\r\n");
|
.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 flowed_text = format_flowed(final_text);
|
||||||
|
|
||||||
let footer = &self.selfstatus;
|
let footer = &self.selfstatus;
|
||||||
|
|||||||
@@ -204,13 +204,11 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
|
|||||||
first_quoted_line = l;
|
first_quoted_line = l;
|
||||||
}
|
}
|
||||||
last_quoted_line = Some(l)
|
last_quoted_line = Some(l)
|
||||||
} else if !is_empty_line(line) {
|
} else if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
|
||||||
if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
|
has_quoted_headline = true
|
||||||
has_quoted_headline = true
|
} else {
|
||||||
} else {
|
/* non-quoting line found */
|
||||||
/* non-quoting line found */
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(last_quoted_line) = last_quoted_line {
|
if let Some(last_quoted_line) = last_quoted_line {
|
||||||
|
|||||||
Reference in New Issue
Block a user