Skip to content

Commit 525b04e

Browse files
committed
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.
1 parent 377fa01 commit 525b04e

File tree

4 files changed

+71
-29
lines changed

4 files changed

+71
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- limit the rate of MDN sending #3402
77
- ignore ratelimits for bots #3439
88
- remove `msgs_mdns` references to deleted messages during housekeeping #3387
9+
- format message lines starting with `>` as quotes #3434
910

1011
### Fixes
1112
- set a default error if NDN does not provide an error

src/format_flowed.rs

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,26 @@ fn format_line_flowed(line: &str, prefix: &str) -> String {
5151
result + &buffer
5252
}
5353

54-
fn format_flowed_prefix(text: &str, prefix: &str) -> String {
54+
/// Returns text formatted according to RFC 3767 (format=flowed).
55+
///
56+
/// This function accepts text separated by LF, but returns text
57+
/// separated by CRLF.
58+
///
59+
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
60+
/// SHOULD be set to "no" when sending.
61+
pub(crate) fn format_flowed(text: &str) -> String {
5562
let mut result = String::new();
5663

5764
for line in text.split('\n') {
5865
if !result.is_empty() {
5966
result += "\r\n";
6067
}
61-
let line = line.trim_end();
68+
69+
let line_no_prefix = line.strip_prefix('>');
70+
let is_quote = line_no_prefix.is_some();
71+
let line = line_no_prefix.unwrap_or(line).trim();
72+
let prefix = if is_quote { "> " } else { "" };
73+
6274
if prefix.len() + line.len() > 78 {
6375
result += &format_line_flowed(line, prefix);
6476
} else {
@@ -70,23 +82,23 @@ fn format_flowed_prefix(text: &str, prefix: &str) -> String {
7082
result += line;
7183
}
7284
}
73-
result
74-
}
7585

76-
/// Returns text formatted according to RFC 3767 (format=flowed).
77-
///
78-
/// This function accepts text separated by LF, but returns text
79-
/// separated by CRLF.
80-
///
81-
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
82-
/// SHOULD be set to "no" when sending.
83-
pub fn format_flowed(text: &str) -> String {
84-
format_flowed_prefix(text, "")
86+
result
8587
}
8688

8789
/// Same as format_flowed(), but adds "> " prefix to each line.
8890
pub fn format_flowed_quote(text: &str) -> String {
89-
format_flowed_prefix(text, "> ")
91+
let mut result = String::new();
92+
93+
for line in text.split('\n') {
94+
if !result.is_empty() {
95+
result += "\n";
96+
}
97+
result += "> ";
98+
result += line;
99+
}
100+
101+
format_flowed(&result)
90102
}
91103

92104
/// Joins lines in format=flowed text.
@@ -129,6 +141,7 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String {
129141
#[cfg(test)]
130142
mod tests {
131143
use super::*;
144+
use crate::test_utils::TestContext;
132145

133146
#[test]
134147
fn test_format_flowed() {
@@ -144,18 +157,18 @@ mod tests {
144157
client and enter the setup code presented on the generating device.";
145158
assert_eq!(format_flowed(text), expected);
146159

147-
let text = "> Not a quote";
148-
assert_eq!(format_flowed(text), " > Not a quote");
160+
let text = "> A quote";
161+
assert_eq!(format_flowed(text), "> A quote");
149162

150163
// Test space stuffing of wrapped lines
151164
let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
152165
> \n\
153166
> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
154-
let expected = "\x20> This is the Autocrypt Setup Message used to transfer your key between \r\n\
155-
clients.\r\n\
156-
\x20>\r\n\
157-
\x20> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
158-
client and enter the setup code presented on the generating device.";
167+
let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\
168+
> clients.\r\n\
169+
> \r\n\
170+
> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
171+
> client and enter the setup code presented on the generating device.";
159172
assert_eq!(format_flowed(text), expected);
160173
}
161174

@@ -175,6 +188,10 @@ mod tests {
175188
let expected = "> this is a quoted line";
176189
assert_eq!(format_flowed_quote(quote), expected);
177190

191+
let quote = "first quoted line\nsecond quoted line";
192+
let expected = "> first quoted line\r\n> second quoted line";
193+
assert_eq!(format_flowed_quote(quote), expected);
194+
178195
let quote = "> foo bar baz";
179196
let expected = "> > foo bar baz";
180197
assert_eq!(format_flowed_quote(quote), expected);
@@ -185,4 +202,25 @@ mod tests {
185202
> unwrapped on the receiver";
186203
assert_eq!(format_flowed_quote(quote), expected);
187204
}
205+
206+
#[async_std::test]
207+
async fn test_send_quotes() -> anyhow::Result<()> {
208+
let alice = TestContext::new_alice().await;
209+
let bob = TestContext::new_bob().await;
210+
let chat = alice.create_chat(&bob).await;
211+
212+
let sent = alice.send_text(chat.id, "> First quote").await;
213+
let received = bob.recv_msg(&sent).await;
214+
assert_eq!(received.text.as_deref(), Some("> First quote"));
215+
assert!(received.quoted_text().is_none());
216+
assert!(received.quoted_message(&bob).await?.is_none());
217+
218+
let sent = alice.send_text(chat.id, "> Second quote").await;
219+
let received = bob.recv_msg(&sent).await;
220+
assert_eq!(received.text.as_deref(), Some("> Second quote"));
221+
assert!(received.quoted_text().is_none());
222+
assert!(received.quoted_message(&bob).await?.is_none());
223+
224+
Ok(())
225+
}
188226
}

src/mimefactory.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1085,10 +1085,15 @@ impl<'a> MimeFactory<'a> {
10851085
}
10861086
};
10871087

1088-
let quoted_text = self
1088+
let mut quoted_text = self
10891089
.msg
10901090
.quoted_text()
10911091
.map(|quote| format_flowed_quote(&quote) + "\r\n\r\n");
1092+
if quoted_text.is_none() && final_text.starts_with('>') {
1093+
// Insert empty line to avoid receiver treating user-sent quote as topquote inserted by
1094+
// Delta Chat.
1095+
quoted_text = Some("\r\n".to_string());
1096+
}
10921097
let flowed_text = format_flowed(final_text);
10931098

10941099
let footer = &self.selfstatus;

src/simplify.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,11 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
204204
first_quoted_line = l;
205205
}
206206
last_quoted_line = Some(l)
207-
} else if !is_empty_line(line) {
208-
if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
209-
has_quoted_headline = true
210-
} else {
211-
/* non-quoting line found */
212-
break;
213-
}
207+
} else if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
208+
has_quoted_headline = true
209+
} else {
210+
/* non-quoting line found */
211+
break;
214212
}
215213
}
216214
if let Some(last_quoted_line) = last_quoted_line {

0 commit comments

Comments
 (0)