From eda92ed4c0d2735144df8fa2136584de69634bdb Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Sun, 11 May 2014 05:03:45 -0400
Subject: [PATCH] Improved display of plain text messages and text to HTML conversion (#1488937)

---
 plugins/hide_blockquote/hide_blockquote.js |    2 
 program/steps/mail/compose.inc             |   12 
 skins/classic/editor_content.css           |   13 
 program/js/editor.js                       |    2 
 program/lib/Roundcube/rcube_mime.php       |   22 +
 program/steps/mail/sendmail.inc            |   25 +
 tests/Framework/Text2Html.php              |   81 +++++++
 program/lib/Roundcube/rcube_text2html.php  |  277 +++++++++++++++++++++++++
 skins/classic/mail.css                     |   11 
 program/steps/utils/text2html.inc          |   28 ++
 program/steps/mail/func.inc                |   83 ------
 tests/phpunit.xml                          |    1 
 program/js/app.js                          |   70 +++--
 skins/larry/editor_content.css             |   13 
 skins/larry/mail.css                       |   11 
 15 files changed, 496 insertions(+), 155 deletions(-)

diff --git a/plugins/hide_blockquote/hide_blockquote.js b/plugins/hide_blockquote/hide_blockquote.js
index 2d28076..964cc07 100644
--- a/plugins/hide_blockquote/hide_blockquote.js
+++ b/plugins/hide_blockquote/hide_blockquote.js
@@ -25,7 +25,7 @@
   if (limit <= 0)
     return;
 
-  $('pre > blockquote', $('#messagebody')).each(function() {
+  $('div.message-part div.pre > blockquote', $('#messagebody')).each(function() {
     var div, link, q = $(this),
       text = $.trim(q.text()),
       res = text.split(/\n/);
diff --git a/program/js/app.js b/program/js/app.js
index 042ebb7..46d7480 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -3439,17 +3439,23 @@
   {
     this.stop_spellchecking();
 
-    if (props.mode == 'html') {
-      this.plain2html($('#'+props.id).val(), props.id);
-      tinyMCE.execCommand('mceAddControl', false, props.id);
+    var input = $('#' + props.id);
 
-      if (this.env.default_font)
-        setTimeout(function() {
-          $(tinyMCE.get(props.id).getBody()).css('font-family', rcmail.env.default_font);
-        }, 500);
-    }
-    else if (this.html2plain(tinyMCE.get(props.id).getContent(), props.id))
-      tinyMCE.execCommand('mceRemoveControl', false, props.id);
+    if (props.mode == 'html')
+      this.plain2html(input.val(), function(data) {
+        input.val(data);
+        tinyMCE.execCommand('mceAddControl', false, props.id);
+
+        if (ref.env.default_font)
+          setTimeout(function() {
+            $(tinyMCE.get(props.id).getBody()).css('font-family', ref.env.default_font);
+          }, 500);
+      });
+    else
+      this.html2plain(tinyMCE.get(props.id).getContent(), function(data) {
+        tinyMCE.execCommand('mceRemoveControl', false, props.id);
+        input.val(data);
+      });
 
     return true;
   };
@@ -6809,39 +6815,49 @@
   /*********  html to text conversion functions   *********/
   /********************************************************/
 
-  this.html2plain = function(htmlText, id)
+  this.html2plain = function(html, func)
+  {
+    return this.format_converter(html, 'html', func);
+  };
+
+  this.plain2html = function(plain, func)
+  {
+    return this.format_converter(plain, 'plain', func);
+  };
+
+  this.format_converter = function(text, format, func)
   {
     // warn the user (if converted content is not empty)
-    if (!htmlText || !(htmlText.replace(/<[^>]+>|&nbsp;|\s/g, '')).length) {
+    if (!text
+      || (format == 'html' && !(text.replace(/<[^>]+>|&nbsp;|\xC2\xA0|\s/g, '')).length)
+      || (format != 'html' && !(text.replace(/\xC2\xA0|\s/g, '')).length)
+    ) {
       // without setTimeout() here, textarea is filled with initial (onload) content
-      setTimeout(function() { $('#'+id).val(''); }, 50);
+      setTimeout(function() { if (func) func(''); }, 50);
       return true;
     }
 
-    if (!confirm(this.get_label('editorwarning')))
+    var confirmed = this.env.editor_warned || confirm(this.get_label('editorwarning'));
+
+    this.env.editor_warned = true;
+
+    if (!confirmed)
       return false;
 
-    var url = '?_task=utils&_action=html2text',
+    var url = '?_task=utils&_action=' + (format == 'html' ? 'html2text' : 'text2html'),
       lock = this.set_busy(true, 'converting');
 
     this.log('HTTP POST: ' + url);
 
-    $.ajax({ type: 'POST', url: url, data: htmlText, contentType: 'application/octet-stream',
+    $.ajax({ type: 'POST', url: url, data: text, contentType: 'application/octet-stream',
       error: function(o, status, err) { ref.http_error(o, status, err, lock); },
-      success: function(data) { ref.set_busy(false, null, lock); $('#'+id).val(data); ref.log(data); }
+      success: function(data) {
+        ref.set_busy(false, null, lock);
+        if (func) func(data);
+      }
     });
 
     return true;
-  };
-
-  this.plain2html = function(plain, id)
-  {
-    var lock = this.set_busy(true, 'converting');
-
-    plain = plain.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
-    $('#'+id).val(plain ? '<pre>'+plain+'</pre>' : '');
-
-    this.set_busy(false, null, lock);
   };
 
 
diff --git a/program/js/editor.js b/program/js/editor.js
index af877d7..d23d2b2 100644
--- a/program/js/editor.js
+++ b/program/js/editor.js
@@ -37,7 +37,7 @@
       apply_source_formatting: true,
       theme: 'advanced',
       language: config.lang,
-      content_css: config.skin_path + '/editor_content.css',
+      content_css: config.skin_path + '/editor_content.css?v2',
       theme_advanced_toolbar_location: 'top',
       theme_advanced_toolbar_align: 'left',
       theme_advanced_buttons3: '',
diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php
index 091b2fa..370d5a8 100644
--- a/program/lib/Roundcube/rcube_mime.php
+++ b/program/lib/Roundcube/rcube_mime.php
@@ -480,15 +480,17 @@
     /**
      * Interpret a format=flowed message body according to RFC 2646
      *
-     * @param string  $text Raw body formatted as flowed text
+     * @param string $text Raw body formatted as flowed text
+     * @param string $mark Mark each flowed line with specified character
      *
      * @return string Interpreted text with unwrapped lines and stuffed space removed
      */
-    public static function unfold_flowed($text)
+    public static function unfold_flowed($text, $mark = null)
     {
         $text = preg_split('/\r?\n/', $text);
         $last = -1;
         $q_level = 0;
+        $marks = array();
 
         foreach ($text as $idx => $line) {
             if (preg_match('/^(>+)/', $line, $m)) {
@@ -508,6 +510,10 @@
                 ) {
                     $text[$last] .= $line;
                     unset($text[$idx]);
+
+                    if ($mark) {
+                        $marks[$last] = true;
+                    }
                 }
                 else {
                     $last = $idx;
@@ -520,7 +526,7 @@
                 }
                 else {
                     // remove space-stuffing
-                    $line = preg_replace('/^\s/', '', $line);
+                    $line = preg_replace('/^ /', '', $line);
 
                     if (isset($text[$last]) && $line
                         && $text[$last] != '-- '
@@ -528,6 +534,10 @@
                     ) {
                         $text[$last] .= $line;
                         unset($text[$idx]);
+
+                        if ($mark) {
+                            $marks[$last] = true;
+                        }
                     }
                     else {
                         $text[$idx] = $line;
@@ -538,6 +548,12 @@
             $q_level = $q;
         }
 
+        if (!empty($marks)) {
+            foreach (array_keys($marks) as $mk) {
+                $text[$mk] = $mark . $text[$mk];
+            }
+        }
+
         return implode("\r\n", $text);
     }
 
diff --git a/program/lib/Roundcube/rcube_text2html.php b/program/lib/Roundcube/rcube_text2html.php
new file mode 100644
index 0000000..5da771a
--- /dev/null
+++ b/program/lib/Roundcube/rcube_text2html.php
@@ -0,0 +1,277 @@
+<?php
+
+/**
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2008-2014, The Roundcube Dev Team                       |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Converts plain text to HTML                                         |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <alec@alec.pl>                            |
+ +-----------------------------------------------------------------------+
+ */
+
+/**
+ * Converts plain text to HTML
+ *
+ * @package    Framework
+ * @subpackage Utils
+ */
+class rcube_text2html
+{
+    /**
+     * Contains the HTML content after conversion.
+     *
+     * @var string $html
+     */
+    protected $html;
+
+    /**
+     * Contains the plain text.
+     *
+     * @var string $text
+     */
+    protected $text;
+
+    /**
+     * Configuration
+     *
+     * @var array $config
+     */
+    protected $config = array(
+        // non-breaking space
+        'space'  => "\xC2\xA0",
+        // enables format=flowed parser
+        'flowed' => false,
+        // enables wrapping for non-flowed text
+        'wrap'   => false,
+        // line-break tag
+        'break'  => "<br>\n",
+        // prefix and suffix (wrapper element)
+        'begin'  => '<div class="pre">',
+        'end'    => '</end>',
+        // enables links replacement
+        'links'  => true,
+    );
+
+
+    /**
+     * Constructor.
+     *
+     * If the plain text source string (or file) is supplied, the class
+     * will instantiate with that source propagated, all that has
+     * to be done it to call get_html().
+     *
+     * @param string  $source    Plain text
+     * @param boolean $from_file Indicates $source is a file to pull content from
+     * @param array   $config    Class configuration
+     */
+    function __construct($source = '', $from_file = false, $config = array())
+    {
+        if (!empty($source)) {
+            $this->set_text($source, $from_file);
+        }
+
+        if (!empty($config) && is_array($config)) {
+            $this->config = array_merge($this->config, $config);
+        }
+    }
+
+    /**
+     * Loads source text into memory, either from $source string or a file.
+     *
+     * @param string  $source    Plain text
+     * @param boolean $from_file Indicates $source is a file to pull content from
+     */
+    function set_text($source, $from_file = false)
+    {
+        if ($from_file && file_exists($source)) {
+            $this->text = file_get_contents($source);
+        }
+        else {
+            $this->text = $source;
+        }
+
+        $this->_converted = false;
+    }
+
+    /**
+     * Returns the HTML content.
+     *
+     * @return string HTML content
+     */
+    function get_html()
+    {
+        if (!$this->_converted) {
+            $this->_convert();
+        }
+
+        return $this->html;
+    }
+
+    /**
+     * Prints the HTML.
+     */
+    function print_html()
+    {
+        print $this->get_html();
+    }
+
+    /**
+     * Workhorse function that does actual conversion (calls _converter() method).
+     */
+    protected function _convert()
+    {
+        $text = stripslashes($this->text);
+
+        // Convert TXT to HTML
+        $this->html       = $this->_converter($text);
+        $this->_converted = true;
+    }
+
+    /**
+     * Workhorse function that does actual conversion.
+     *
+     * @param string Plain text
+     */
+    protected function _converter($text)
+    {
+        // make links and email-addresses clickable
+        $attribs  = array('link_attribs' => array('rel' => 'noreferrer', 'target' => '_blank'));
+        $replacer = new rcmail_string_replacer($attribs);
+
+        if ($this->config['flowed']) {
+            $flowed_char = 0x01;
+            $text        = rcube_mime::unfold_flowed($text, chr($flowed_char));
+        }
+
+        // search for patterns like links and e-mail addresses and replace with tokens
+        if ($this->config['links']) {
+            $text = $replacer->replace($text);
+        }
+
+        // split body into single lines
+        $text        = preg_split('/\r?\n/', $text);
+        $quote_level = 0;
+        $last        = -1;
+
+        // find/mark quoted lines...
+        for ($n=0, $cnt=count($text); $n < $cnt; $n++) {
+            $flowed = false;
+            if ($this->config['flowed'] && ord($text[0]) == $flowed_char) {
+                $flowed   = true;
+                $text[$n] = substr($text[$n], 1);
+            }
+
+            if ($text[$n][0] == '>' && 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']);
+
+                if ($q > $quote_level) {
+                    $text[$n] = $replacer->get_replacement($replacer->add(
+                        str_repeat('<blockquote>', $q - $quote_level))) . $text[$n];
+                    $last = $n;
+                }
+                else if ($q < $quote_level) {
+                    $text[$n] = $replacer->get_replacement($replacer->add(
+                        str_repeat('</blockquote>', $quote_level - $q))) . $text[$n];
+                    $last = $n;
+                }
+            }
+            else {
+                $text[$n] = $this->_convert_line($text[$n], $flowed || $this->config['wrap']);
+                $q = 0;
+
+                if ($quote_level > 0) {
+                    $text[$n] = $replacer->get_replacement($replacer->add(
+                        str_repeat('</blockquote>', $quote_level))) . $text[$n];
+                }
+            }
+
+            $quote_level = $q;
+        }
+
+        if ($quote_level > 0) {
+            $text[$n] = $replacer->get_replacement($replacer->add(
+                str_repeat('</blockquote>', $quote_level))) . $text[$n];
+        }
+
+        $text = join("\n", $text);
+
+        // colorize signature (up to <sig_max_lines> lines)
+        $len = strlen($text);
+        $sig_max_lines = rcube::get_instance()->config->get('sig_max_lines', 15);
+
+        while (($sp = strrpos($text, "-- \n", $sp ? -$len+$sp-1 : 0)) !== false) {
+            if ($sp == 0 || $text[$sp-1] == "\n") {
+                // do not touch blocks with more that X lines
+                if (substr_count($text, "\n", $sp) < $sig_max_lines) {
+                    $text = substr($text, 0, max(0, $sp))
+                        .'<span class="sig">'.substr($text, $sp).'</span>';
+                }
+
+                break;
+            }
+        }
+
+        // insert url/mailto links and citation tags
+        $text = $replacer->resolve($text);
+
+        // replace \n before </blockquote>
+        $text = str_replace("\n</blockquote>", "</blockquote>", $text);
+
+        // replace line breaks
+        $text = str_replace("\n", $this->config['break'], $text);
+
+        return $this->config['begin'] . $text . $this->config['end'];
+    }
+
+    /**
+     * Converts spaces in line of text
+     */
+    protected function _convert_line($text, $is_flowed)
+    {
+        static $table;
+
+        if (empty($table)) {
+            $table = get_html_translation_table(HTML_SPECIALCHARS);
+            unset($table['?']);
+        }
+
+        // skip signature separator
+        if ($text == '-- ') {
+            return $text;
+        }
+
+        // replace HTML special characters
+        $text = strtr($text, $table);
+
+        $nbsp = $this->config['space'];
+
+        // replace some whitespace characters
+        $text = str_replace(array("\r", "\t"), array('', '    '), $text);
+
+        // replace spaces with non-breaking spaces
+        if ($is_flowed) {
+            $text = preg_replace_callback('/(^|[^ ])( +)/', function($matches) {
+                    if (!strlen($matches[2])) {
+                        return str_repeat($nbsp, strlen($matches[2]));
+                    }
+                    else {
+                        return $matches[1] . ' ' . str_repeat($nbsp, strlen($matches[2])-1);
+                    }
+                }, $text);
+        }
+        else {
+            $text = str_replace(' ', $nbsp, $text);
+        }
+
+        return $text;
+    }
+}
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 2b717d6..0ceb85d 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -624,7 +624,8 @@
                 }
 
                 if (!$sql_arr['html_signature']) {
-                    $html = "<pre>" . $html . "</pre>";
+                    $t2h  = new rcube_text2html($sql_arr['signature'], false);
+                    $html = $t2h->get_html();
                 }
 
                 $a_signatures[$identity_id]['text'] = $text;
@@ -826,15 +827,8 @@
                 }
             }
 
-            if ($part->ctype_parameters['format'] == 'flowed') {
-                $body = rcube_mime::unfold_flowed($body);
-            }
-
             // add HTML formatting
-            $body = rcmail_plain_body($body);
-            if ($body) {
-                $body = '<pre>' . $body . '</pre>';
-            }
+            $body = rcmail_plain_body($body, $part->ctype_parameters['format'] == 'flowed');
         }
     }
     else {
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index 7270cf9..ac0d7fc 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -854,95 +854,28 @@
 
     // plaintext postprocessing
     if ($part->ctype_secondary == 'plain') {
-        if ($part->ctype_secondary == 'plain' && $part->ctype_parameters['format'] == 'flowed') {
-            $body = rcube_mime::unfold_flowed($body);
-        }
-
-        $body = rcmail_plain_body($body);
+        $body = rcmail_plain_body($body, $part->ctype_parameters['format'] == 'flowed');
     }
 
     // allow post-processing of the message body
     $data = $RCMAIL->plugins->exec_hook('message_part_after',
         array('type' => $part->ctype_secondary, 'body' => $body, 'id' => $part->mime_id) + $data);
 
-    return $data['type'] == 'html' ? $data['body'] : html::tag('pre', array(), $data['body']);
+    return $data['body'];
 }
 
 /**
  * Handle links and citation marks in plain text message
  *
  * @param string  Plain text string
+ * @param boolean Set to True if the source text is in format=flowed
  *
  * @return string Formatted HTML string
  */
-function rcmail_plain_body($body)
+function rcmail_plain_body($body, $flowed = false)
 {
-    global $RCMAIL;
-
-    // make links and email-addresses clickable
-    $attribs  = array('link_attribs' => array('rel' => 'noreferrer', 'target' => '_blank'));
-    $replacer = new rcmail_string_replacer($attribs);
-
-    // search for patterns like links and e-mail addresses and replace with tokens
-    $body = $replacer->replace($body);
-
-    // split body into single lines
-    $body        = preg_split('/\r?\n/', $body);
-    $quote_level = 0;
-    $last        = -1;
-
-    // find/mark quoted lines...
-    for ($n=0, $cnt=count($body); $n < $cnt; $n++) {
-        if ($body[$n][0] == '>' && preg_match('/^(>+ {0,1})+/', $body[$n], $regs)) {
-            $q        = substr_count($regs[0], '>');
-            $body[$n] = substr($body[$n], strlen($regs[0]));
-
-            if ($q > $quote_level) {
-                $body[$n] = $replacer->get_replacement($replacer->add(
-                    str_repeat('<blockquote>', $q - $quote_level))) . $body[$n];
-                $last = $n;
-            }
-            else if ($q < $quote_level) {
-                $body[$n] = $replacer->get_replacement($replacer->add(
-                    str_repeat('</blockquote>', $quote_level - $q))) . $body[$n];
-                $last = $n;
-            }
-        }
-        else {
-            $q = 0;
-            if ($quote_level > 0)
-                $body[$n] = $replacer->get_replacement($replacer->add(
-                    str_repeat('</blockquote>', $quote_level))) . $body[$n];
-        }
-
-        $quote_level = $q;
-    }
-
-    $body = join("\n", $body);
-
-    // quote plain text (don't use rcube::Q() here, to display entities "as is")
-    $table = get_html_translation_table(HTML_SPECIALCHARS);
-    unset($table['?']);
-    $body = strtr($body, $table);
-
-    // colorize signature (up to <sig_max_lines> lines)
-    $len = strlen($body);
-    $sig_max_lines = $RCMAIL->config->get('sig_max_lines', 15);
-
-    while (($sp = strrpos($body, "-- \n", $sp ? -$len+$sp-1 : 0)) !== false) {
-        if ($sp == 0 || $body[$sp-1] == "\n") {
-            // do not touch blocks with more that X lines
-            if (substr_count($body, "\n", $sp) < $sig_max_lines) {
-                $body = substr($body, 0, max(0, $sp))
-                    . '<span class="sig">'.substr($body, $sp).'</span>';
-            }
-
-            break;
-        }
-    }
-
-    // insert url/mailto links and citation tags
-    $body = $replacer->resolve($body);
+    $text2html = new rcube_text2html($body, false, array('flowed' => $flowed));
+    $body      = $text2html->get_html();
 
     return $body;
 }
@@ -1272,8 +1205,8 @@
             $plugin = $RCMAIL->plugins->exec_hook('message_body_prefix',
                 array('part' => $MESSAGE, 'prefix' => ''));
 
-            $out .= html::div('message-part', $plugin['prefix'] . html::tag('pre', array(),
-                rcmail_plain_body(rcube::Q($MESSAGE->body, 'strict', false))));
+            $out .= html::div('message-part',
+                $plugin['prefix'] . rcmail_plain_body($MESSAGE->body));
         }
     }
 
diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc
index 04ba94f..baecbd1 100644
--- a/program/steps/mail/sendmail.inc
+++ b/program/steps/mail/sendmail.inc
@@ -280,13 +280,23 @@
 
 if (!$savedraft) {
     if ($isHtml) {
-        // remove signature's div ID
-        $message_body = preg_replace('/\s*id="_rc_sig"/', '', $message_body);
+        $b_style   = 'padding: 0 0.4em; border-left: #1010ff 2px solid; margin: 0';
+        $pre_style = 'margin: 0; padding: 0; font-family: monospace';
 
-        // add inline css for blockquotes
-        $bstyle       = 'padding-left:5px; border-left:#1010ff 2px solid; margin-left:5px';
-        $message_body = preg_replace('/<blockquote>/',
-            '<blockquote type="cite" style="'.$bstyle.'">', $message_body);
+        $message_body = preg_replace(
+            array(
+                // remove signature's div ID
+                '/\s*id="_rc_sig"/',
+                // add inline css for blockquotes and container
+                '/<blockquote>/',
+                '/<div class="pre">/'
+            ),
+            array(
+                '',
+                '<blockquote type="cite" style="'.$b_style.'">',
+                '<div class="pre" style="'.$pre_style.'">'
+            ),
+            $message_body);
     }
 
     // Check spelling before send
@@ -912,7 +922,8 @@
         if (!preg_match('/\.(php|ini|conf)$/', $file) && strpos($file, '/etc/') === false) {
             $footer = file_get_contents($file);
             if ($isHtml && !$html_footer) {
-                $footer = '<pre>' . $footer . '</pre>';
+                $t2h    = new rcube_text2html($footer, false);
+                $footer = $t2h->get_html();
             }
             return $footer;
         }
diff --git a/program/steps/utils/text2html.inc b/program/steps/utils/text2html.inc
new file mode 100644
index 0000000..1672436
--- /dev/null
+++ b/program/steps/utils/text2html.inc
@@ -0,0 +1,28 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | program/steps/utils/text2html.inc                                     |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2014, The Roundcube Dev Team                       |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Convert plain text to HTML                                          |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+$text = stream_get_contents(fopen('php://input', 'r'));
+
+$converter = new rcube_text2html($text, false, array('wrap' => true));
+
+header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
+print $converter->get_html();
+exit;
diff --git a/skins/classic/editor_content.css b/skins/classic/editor_content.css
index aabed07..67480ab 100644
--- a/skins/classic/editor_content.css
+++ b/skins/classic/editor_content.css
@@ -12,20 +12,15 @@
 	margin-top: 2px;
 }
 
-pre
-{
+div.pre {
 	margin: 0;
 	padding: 0;
-	white-space: -moz-pre-wrap !important;
-	white-space: pre-wrap !important;
-	white-space: pre;
-	word-wrap: break-word; /* IE (and Safari) */
+	font-family: monospace;
 }
 
 blockquote
 {
-	padding-left: 5px;
 	border-left: #1010ff 2px solid;
-	margin-left: 5px;
-	width: 100%;
+	margin: 0;
+	padding: 0 0.4em;
 }
diff --git a/skins/classic/mail.css b/skins/classic/mail.css
index fc066e0..47faa29 100644
--- a/skins/classic/mail.css
+++ b/skins/classic/mail.css
@@ -1306,17 +1306,12 @@
   color: #0000CC;
 }
 
-div.message-part pre,
-div.message-htmlpart pre,
 div.message-part div.pre
 {
   margin: 0px;
   padding: 0px;
   font-family: monospace;
   font-size: 12px;
-  white-space: -moz-pre-wrap !important;
-  white-space: pre-wrap !important;
-  white-space: pre;
 }
 
 div.message-part span.sig
@@ -1330,8 +1325,10 @@
   border-left: 2px solid blue;
   border-right: 2px solid blue;
   background-color: #F6F6F6;
-  margin: 2px 0px;
-  padding: 1px 8px 1px 10px;
+  margin: 0;
+  padding: 0 0.4em;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 div.message-part blockquote blockquote
diff --git a/skins/larry/editor_content.css b/skins/larry/editor_content.css
index aabed07..67480ab 100644
--- a/skins/larry/editor_content.css
+++ b/skins/larry/editor_content.css
@@ -12,20 +12,15 @@
 	margin-top: 2px;
 }
 
-pre
-{
+div.pre {
 	margin: 0;
 	padding: 0;
-	white-space: -moz-pre-wrap !important;
-	white-space: pre-wrap !important;
-	white-space: pre;
-	word-wrap: break-word; /* IE (and Safari) */
+	font-family: monospace;
 }
 
 blockquote
 {
-	padding-left: 5px;
 	border-left: #1010ff 2px solid;
-	margin-left: 5px;
-	width: 100%;
+	margin: 0;
+	padding: 0 0.4em;
 }
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index 3ec1456..7afb14f 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -1103,16 +1103,11 @@
 	border-top: 0;
 }
 
-div.message-part pre,
-div.message-htmlpart pre,
 div.message-part div.pre {
 	margin: 0;
 	padding: 0;
 	font-family: monospace;
 	font-size: 12px;
-	white-space: -moz-pre-wrap !important;
-	white-space: pre-wrap !important;
-	white-space: pre;
 }
 
 div.message-part span.sig {
@@ -1124,8 +1119,10 @@
 	border-left: 2px solid blue;
 	border-right: 2px solid blue;
 	background-color: #F6F6F6;
-	margin: 2px 0 2px 0;
-	padding: 1px 8px 1px 10px;
+	margin: 0;
+	padding: 0 0.4em;
+	overflow: hidden;
+	text-overflow: ellipsis;
 }
 
 div.message-part blockquote blockquote {
diff --git a/tests/Framework/Text2Html.php b/tests/Framework/Text2Html.php
new file mode 100644
index 0000000..91dabf2
--- /dev/null
+++ b/tests/Framework/Text2Html.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * Test class to test rcube_text2html class
+ *
+ * @package Tests
+ */
+class Framework_Text2Html extends PHPUnit_Framework_TestCase
+{
+
+    /**
+     * Data for test_text2html()
+     */
+    function data_text2html()
+    {
+        $options = array(
+            'begin'  => '',
+            'end'    => '',
+            'break'  => '<br>',
+            'links'  => false,
+            'flowed' => false,
+            'space'  => '_', // replace UTF-8 non-breaking space for simpler testing
+        );
+
+        $data[] = array(" aaaa", "_aaaa", $options);
+        $data[] = array("aaaa aaaa", "aaaa_aaaa", $options);
+        $data[] = array("aaaa  aaaa", "aaaa__aaaa", $options);
+        $data[] = array("aaaa   aaaa", "aaaa___aaaa", $options);
+        $data[] = array("aaaa\taaaa", "aaaa____aaaa", $options);
+        $data[] = array("aaaa\naaaa", "aaaa<br>aaaa", $options);
+        $data[] = array("aaaa\n aaaa", "aaaa<br>_aaaa", $options);
+        $data[] = array("aaaa\n  aaaa", "aaaa<br>__aaaa", $options);
+        $data[] = array("aaaa\n   aaaa", "aaaa<br>___aaaa", $options);
+        $data[] = array("\taaaa", "____aaaa", $options);
+        $data[] = array("\naaaa", "<br>aaaa", $options);
+        $data[] = array("\n aaaa", "<br>_aaaa", $options);
+        $data[] = array("\n  aaaa", "<br>__aaaa", $options);
+        $data[] = array("\n   aaaa", "<br>___aaaa", $options);
+        $data[] = array("aaaa\n\nbbbb", "aaaa<br><br>bbbb", $options);
+        $data[] = array(">aaaa \n>aaaa", "<blockquote>aaaa_<br>aaaa</blockquote>", $options);
+        $data[] = array(">aaaa\n>aaaa", "<blockquote>aaaa<br>aaaa</blockquote>", $options);
+        $data[] = array(">aaaa \n>bbbb\ncccc dddd", "<blockquote>aaaa_<br>bbbb</blockquote>cccc_dddd", $options);
+
+        $options['flowed'] = true;
+
+        $data[] = array(" aaaa", "aaaa", $options);
+        $data[] = array("aaaa aaaa", "aaaa_aaaa", $options);
+        $data[] = array("aaaa  aaaa", "aaaa__aaaa", $options);
+        $data[] = array("aaaa   aaaa", "aaaa___aaaa", $options);
+        $data[] = array("aaaa\taaaa", "aaaa____aaaa", $options);
+        $data[] = array("aaaa\naaaa", "aaaa<br>aaaa", $options);
+        $data[] = array("aaaa\n aaaa", "aaaa<br>aaaa", $options);
+        $data[] = array("aaaa\n  aaaa", "aaaa<br>_aaaa", $options);
+        $data[] = array("aaaa\n   aaaa", "aaaa<br>__aaaa", $options);
+        $data[] = array("\taaaa", "____aaaa", $options);
+        $data[] = array("\naaaa", "<br>aaaa", $options);
+        $data[] = array("\n aaaa", "<br>aaaa", $options);
+        $data[] = array("\n  aaaa", "<br>_aaaa", $options);
+        $data[] = array("\n   aaaa", "<br>__aaaa", $options);
+        $data[] = array("aaaa\n\nbbbb", "aaaa<br><br>bbbb", $options);
+        $data[] = array(">aaaa \n>aaaa", "<blockquote>aaaa aaaa</blockquote>", $options);
+        $data[] = array(">aaaa\n>aaaa", "<blockquote>aaaa<br>aaaa</blockquote>", $options);
+        $data[] = array(">aaaa \n>bbbb\ncccc dddd", "<blockquote>aaaa bbbb</blockquote>cccc_dddd", $options);
+
+        return $data;
+    }
+
+    /**
+     * Test text to html conversion
+     *
+     * @dataProvider data_text2html
+     */
+    function test_text2html($input, $output, $options)
+    {
+        $t2h = new rcube_text2html($input, false, $options);
+
+        $html = $t2h->get_html();
+
+        $this->assertEquals($output, $html);
+    }
+}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index cee3434..f3df535 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -40,6 +40,7 @@
             <file>Framework/SpellcheckPspell.php</file>
             <file>Framework/Spellchecker.php</file>
             <file>Framework/StringReplacer.php</file>
+            <file>Framework/Text2Html.php</file>
             <file>Framework/User.php</file>
             <file>Framework/Utils.php</file>
             <file>Framework/VCard.php</file>

--
Gitblit v1.9.1