Browse Source

Improve auto-wrapping of plain text messages on preview and reply (#6974)

- fix auto-wrapping of some specific cases
- do not auto-wrap non-format=flowed content on preview
- do not auto-wrap content on reply
pull/8233/head
Aleksander Machniak 4 years ago
parent
commit
df7d8f1178
  1. 1
      CHANGELOG.md
  2. 53
      program/actions/mail/compose.php
  3. 10
      program/actions/mail/index.php
  4. 32
      program/lib/Roundcube/rcube_text2html.php
  5. 11
      tests/Actions/Mail/Compose.php
  6. 3
      tests/Framework/Rcube.php
  7. 42
      tests/Framework/Text2Html.php

1
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)

53
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");

10
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);
}
}

32
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 <blockquote>
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;
}

11
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);

3
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');
}
}

42
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<br>aaaa", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>aaaa", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>>_aaaa<", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>>__aaaa<", $options];
$data[] = ["\taaaa", ">____aaaa<", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>_aaaa", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>_ aaaa", $options];
$data[] = ["\taaaa", "_ _ aaaa", $options];
$data[] = ["\naaaa", "<br>aaaa", $options];
$data[] = ["\n aaaa", "<br>aaaa", $options];
$data[] = ["\n aaaa", "<br>>_aaaa<", $options];
$data[] = ["\n aaaa", "<br>>__aaaa<", $options];
$data[] = ["\n aaaa", "<br>_aaaa", $options];
$data[] = ["\n aaaa", "<br>_ aaaa", $options];
$data[] = ["aaaa\n\nbbbb", "aaaa<br><br>bbbb", $options];
$data[] = [">aaaa \n>aaaa", "<blockquote>aaaa aaaa</blockquote>", $options];
$data[] = [">aaaa\n>aaaa", "<blockquote>aaaa<br>aaaa</blockquote>", $options];
$data[] = [">aaaa \n>bbbb\ncccc dddd", "<blockquote>aaaa bbbb</blockquote>>cccc_dddd<", $options];
$data[] = ["\x02\x03", ">\x02\x03<", $options];
$data[] = [">aaaa \n>bbbb\ncccc dddd", "<blockquote>aaaa bbbb</blockquote>cccc 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<br>aaaa", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>aaaa", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>>_aaaa<", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>>__aaaa<", $options];
$data[] = ["\taaaa", ">____aaaa<", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>_aaaa", $options];
$data[] = ["aaaa\n aaaa", "aaaa<br>_ aaaa", $options];
$data[] = ["\taaaa", "_ _ aaaa", $options];
$data[] = ["\naaaa", "<br>aaaa", $options];
$data[] = ["\n aaaa", "<br>aaaa", $options];
$data[] = ["\n aaaa", "<br>>_aaaa<", $options];
$data[] = ["\n aaaa", "<br>>__aaaa<", $options];
$data[] = ["\n aaaa", "<br>_aaaa", $options];
$data[] = ["\n aaaa", "<br>_ aaaa", $options];
$data[] = ["aaaa\n\nbbbb", "aaaa<br><br>bbbb", $options];
$data[] = [">aaaa \n>aaaa", "<blockquote>aaaaaaaa</blockquote>", $options];
$data[] = [">aaaa\n>aaaa", "<blockquote>aaaa<br>aaaa</blockquote>", $options];
$data[] = [">aaaa \n>bbbb\ncccc dddd", "<blockquote>aaaabbbb</blockquote>>cccc_dddd<", $options];
$data[] = [">aaaa \n>bbbb\ncccc dddd", "<blockquote>aaaabbbb</blockquote>cccc dddd", $options];
$options['flowed'] = false;
$options['delsp'] = false;

Loading…
Cancel
Save