diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ca2bf23c..9707e9d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Add option to purge deleted mails older than 30, 60 or 90 days (#5493) - Add ability to mark multiple messages as not deleted at once (#5133) - Add possibility to disable line-wrapping of sent mail body (#5101) +- Improve auto-wrapping of plain text messages on preview and reply, don't wrap non-format=flowed content (#6974) - Support displaying RTF content (including encapsulated HTML) from a TNEF attachment - Newmail_notifier: Improved the notification sound (#8155) - Add option to control links handling behavior on html to text conversion (#6485) diff --git a/program/actions/mail/compose.php b/program/actions/mail/compose.php index c4b55c4db..bea3ef6fb 100644 --- a/program/actions/mail/compose.php +++ b/program/actions/mail/compose.php @@ -828,9 +828,10 @@ class rcmail_action_mail_compose extends rcmail_action_mail_index } if (!$bodyIsHtml) { - // soft-wrap and quote message text - $line_length = $rcmail->config->get('line_length', 72); - $body = self::wrap_and_quote($body, $line_length, $reply_indent); + // quote the message text + if ($reply_indent) { + $body = self::quote_text($body); + } if ($reply_mode > 0) { // top-posting $prefix = "\n\n\n" . $prefix; @@ -1715,54 +1716,20 @@ class rcmail_action_mail_compose extends rcmail_action_mail_index } /** - * Wrap text to a given number of characters per line - * but respect the mail quotation of replies messages (>). - * Finally add another quotation level by prepending the lines - * with > + * Add quotation (>) to a replied message text. * - * @param string $text Text to wrap - * @param int $length The line width - * @param bool $quote Enable quote indentation + * @param string $text Text to quote * - * @return string The wrapped text + * @return string The quoted text */ - public static function wrap_and_quote($text, $length = 72, $quote = true) + public static function quote_text($text) { - // Rebuild the message body with a maximum of $max chars, while keeping quoted message. - $max = max(75, $length + 8); $lines = preg_split('/\r?\n/', trim($text)); $out = ''; foreach ($lines as $line) { - // don't wrap already quoted lines - if (isset($line[0]) && $line[0] == '>') { - $line = rtrim($line); - if ($quote) { - $line = '>' . $line; - } - } - // wrap lines above the length limit, but skip these - // special lines with links list created by rcube_html2text - else if (mb_strlen($line) > $max && !preg_match('|^\[[0-9]+\] https?://\S+$|', $line)) { - $newline = ''; - - foreach (explode("\n", rcube_mime::wordwrap($line, $length - 2)) as $l) { - if ($quote) { - $newline .= strlen($l) ? "> $l\n" : ">\n"; - } - else { - $newline .= "$l\n"; - } - } - - $line = rtrim($newline); - } - else if ($quote) { - $line = '> ' . $line; - } - - // Append the line - $out .= $line . "\n"; + $quoted = isset($line[0]) && $line[0] == '>'; + $out .= '>' . ($quoted ? '' : ' ') . $line . "\n"; } return rtrim($out, "\n"); diff --git a/program/actions/mail/index.php b/program/actions/mail/index.php index 805cc56a2..02392e7c6 100644 --- a/program/actions/mail/index.php +++ b/program/actions/mail/index.php @@ -1066,7 +1066,7 @@ class rcmail_action_mail_index extends rcmail_action { $options = [ 'flowed' => $flowed, - 'wrap' => !$flowed, + 'wrap' => $flowed, 'replacer' => 'rcmail_string_replacer', 'delsp' => $delsp ]; @@ -1694,12 +1694,4 @@ class rcmail_action_mail_index extends rcmail_action return array_values($mimetypes); } - - /** - * @deprecated Moved to rcmail_action_mail_compose - */ - public static function wrap_and_quote($text, $length = 72, $quote = true) - { - return rcmail_action_mail_compose::wrap_and_quote($text, $length, $quote); - } } diff --git a/program/lib/Roundcube/rcube_text2html.php b/program/lib/Roundcube/rcube_text2html.php index 2222b1b6b..7d751b6f1 100644 --- a/program/lib/Roundcube/rcube_text2html.php +++ b/program/lib/Roundcube/rcube_text2html.php @@ -145,9 +145,8 @@ class rcube_text2html $replacer = new $this->config['replacer']($attribs); if ($this->config['flowed']) { - $flowed_char = 0x01; - $delsp = $this->config['delsp']; - $text = rcube_mime::unfold_flowed($text, chr($flowed_char), $delsp); + $delsp = $this->config['delsp']; + $text = rcube_mime::unfold_flowed($text, null, $delsp); } // search for patterns like links and e-mail addresses and replace with tokens @@ -163,19 +162,12 @@ class rcube_text2html // wrap quoted lines with
for ($n = 0, $cnt = count($text); $n < $cnt; $n++) { - $flowed = false; $first = $text[$n][0] ?? ''; - if (isset($flowed_char) && ord($first) == $flowed_char) { - $flowed = true; - $text[$n] = substr($text[$n], 1); - $first = $text[$n][0] ?? ''; - } - if ($first == '>' && preg_match('/^(>+ {0,1})+/', $text[$n], $regs)) { $q = substr_count($regs[0], '>'); $text[$n] = substr($text[$n], strlen($regs[0])); - $text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']); + $text[$n] = $this->_convert_line($text[$n]); $_length = strlen(str_replace(' ', '', $text[$n])); if ($q > $quote_level) { @@ -207,7 +199,7 @@ class rcube_text2html } } else { - $text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']); + $text[$n] = $this->_convert_line($text[$n]); $q = 0; $_length = strlen(str_replace(' ', '', $text[$n])); @@ -264,12 +256,11 @@ class rcube_text2html /** * Converts spaces in line of text * - * @param string $text Plain text - * @param bool $is_flowed Is the $text format=flowed? + * @param string $text Plain text * * @return string Converted text */ - protected function _convert_line($text, $is_flowed) + protected function _convert_line($text) { static $table; @@ -290,17 +281,20 @@ class rcube_text2html // replace HTML special and whitespace characters $text = strtr($text, $table); - $nbsp = $this->config['space']; + $nbsp = $this->config['space']; + $wrappable = $this->config['flowed'] || $this->config['wrap']; - // replace spaces with non-breaking spaces - if ($is_flowed) { + // make the line wrappable + if ($wrappable) { $pos = 0; $diff = 0; + $last = -2; $len = strlen($nbsp); $copy = $text; while (($pos = strpos($text, ' ', $pos)) !== false) { - if ($pos == 0 || $text[$pos-1] == ' ') { + if (($pos == 0 || $text[$pos-1] == ' ') && $pos - 1 != $last) { + $last = $pos; $copy = substr_replace($copy, $nbsp, $pos + $diff, 1); $diff += $len - 1; } diff --git a/tests/Actions/Mail/Compose.php b/tests/Actions/Mail/Compose.php index bf500578f..c66f64de2 100644 --- a/tests/Actions/Mail/Compose.php +++ b/tests/Actions/Mail/Compose.php @@ -18,21 +18,20 @@ class Actions_Mail_Compose extends ActionTestCase } /** - * Test wrap_and_quote() method + * Test quote_text() method */ - function test_wrap_and_quote() + function test_quote_text() { $action = new rcmail_action_mail_compose; - $this->assertSame('> ', $action->wrap_and_quote('')); - $this->assertSame('', $action->wrap_and_quote('', 72, false)); + $this->assertSame('> ', $action->quote_text('')); - $result = $action->wrap_and_quote("test1\ntest2"); + $result = $action->quote_text("test1\ntest2"); $expected = "> test1\n> test2"; $this->assertSame($expected, $result); - $result = $action->wrap_and_quote("> test1\n> test2"); + $result = $action->quote_text("> test1\n> test2"); $expected = ">> test1\n>> test2"; $this->assertSame($expected, $result); diff --git a/tests/Framework/Rcube.php b/tests/Framework/Rcube.php index 1d2887ebd..f8f9dc3f5 100644 --- a/tests/Framework/Rcube.php +++ b/tests/Framework/Rcube.php @@ -56,5 +56,8 @@ class Framework_Rcube extends PHPUnit\Framework\TestCase $result = $rcube->decrypt($rcube->encrypt('test')); $this->assertSame('test', $result); + + // Back to the default + $rcube->config->set('cipher_method', 'DES-EDE3-CBC'); } } diff --git a/tests/Framework/Text2Html.php b/tests/Framework/Text2Html.php index 9d5280060..619e7d03a 100644 --- a/tests/Framework/Text2Html.php +++ b/tests/Framework/Text2Html.php @@ -51,46 +51,46 @@ class Framework_Text2Html extends PHPUnit\Framework\TestCase $options['flowed'] = true; $data[] = [" aaaa", "aaaa", $options]; - $data[] = ["aaaa aaaa", ">aaaa_aaaa<", $options]; - $data[] = ["aaaa aaaa", ">aaaa__aaaa<", $options]; - $data[] = ["aaaa aaaa", ">aaaa___aaaa<", $options]; - $data[] = ["aaaa\taaaa", ">aaaa____aaaa<", $options]; + $data[] = ["aaaa aaaa", "aaaa aaaa", $options]; + $data[] = ["aaaa aaaa", "aaaa _aaaa", $options]; + $data[] = ["aaaa aaaa", "aaaa _ aaaa", $options]; + $data[] = ["aaaa\taaaa", "aaaa _ _aaaa", $options]; $data[] = ["aaaa\naaaa", "aaaa
aaaa", $options]; $data[] = ["aaaa\n aaaa", "aaaa
aaaa", $options]; - $data[] = ["aaaa\n aaaa", "aaaa
>_aaaa<", $options]; - $data[] = ["aaaa\n aaaa", "aaaa
>__aaaa<", $options]; - $data[] = ["\taaaa", ">____aaaa<", $options]; + $data[] = ["aaaa\n aaaa", "aaaa
_aaaa", $options]; + $data[] = ["aaaa\n aaaa", "aaaa
_ aaaa", $options]; + $data[] = ["\taaaa", "_ _ aaaa", $options]; $data[] = ["\naaaa", "
aaaa", $options]; $data[] = ["\n aaaa", "
aaaa", $options]; - $data[] = ["\n aaaa", "
>_aaaa<", $options]; - $data[] = ["\n aaaa", "
>__aaaa<", $options]; + $data[] = ["\n aaaa", "
_aaaa", $options]; + $data[] = ["\n aaaa", "
_ aaaa", $options]; $data[] = ["aaaa\n\nbbbb", "aaaa
bbbb", $options]; $data[] = [">aaaa \n>aaaa", "aaaa aaaa", $options]; $data[] = [">aaaa\n>aaaa", "aaaa", $options]; - $data[] = [">aaaa \n>bbbb\ncccc dddd", "
aaaaaaaa bbbb>cccc_dddd<", $options]; - $data[] = ["\x02\x03", ">\x02\x03<", $options]; + $data[] = [">aaaa \n>bbbb\ncccc dddd", "aaaa bbbbcccc dddd", $options]; + $data[] = ["\x02\x03", "\x02\x03", $options]; $options['flowed'] = true; $options['delsp'] = true; $data[] = [" aaaa", "aaaa", $options]; - $data[] = ["aaaa aaaa", ">aaaa_aaaa<", $options]; - $data[] = ["aaaa aaaa", ">aaaa__aaaa<", $options]; - $data[] = ["aaaa aaaa", ">aaaa___aaaa<", $options]; - $data[] = ["aaaa\taaaa", ">aaaa____aaaa<", $options]; + $data[] = ["aaaa aaaa", "aaaa aaaa", $options]; + $data[] = ["aaaa aaaa", "aaaa _aaaa", $options]; + $data[] = ["aaaa aaaa", "aaaa _ aaaa", $options]; + $data[] = ["aaaa\taaaa", "aaaa _ _aaaa", $options]; $data[] = ["aaaa\naaaa", "aaaa
aaaa", $options]; $data[] = ["aaaa\n aaaa", "aaaa
aaaa", $options]; - $data[] = ["aaaa\n aaaa", "aaaa
>_aaaa<", $options]; - $data[] = ["aaaa\n aaaa", "aaaa
>__aaaa<", $options]; - $data[] = ["\taaaa", ">____aaaa<", $options]; + $data[] = ["aaaa\n aaaa", "aaaa
_aaaa", $options]; + $data[] = ["aaaa\n aaaa", "aaaa
_ aaaa", $options]; + $data[] = ["\taaaa", "_ _ aaaa", $options]; $data[] = ["\naaaa", "
aaaa", $options]; $data[] = ["\n aaaa", "
aaaa", $options]; - $data[] = ["\n aaaa", "
>_aaaa<", $options]; - $data[] = ["\n aaaa", "
>__aaaa<", $options]; + $data[] = ["\n aaaa", "
_aaaa", $options]; + $data[] = ["\n aaaa", "
_ aaaa", $options]; $data[] = ["aaaa\n\nbbbb", "aaaa
bbbb", $options]; $data[] = [">aaaa \n>aaaa", "aaaaaaaa", $options]; $data[] = [">aaaa\n>aaaa", "aaaa", $options]; - $data[] = [">aaaa \n>bbbb\ncccc dddd", "
aaaaaaaabbbb>cccc_dddd<", $options]; + $data[] = [">aaaa \n>bbbb\ncccc dddd", "aaaabbbbcccc dddd", $options]; $options['flowed'] = false; $options['delsp'] = false;