lib/ext

Aleksander Machniak machniak at kolabsys.com
Thu Jan 2 15:54:28 CET 2014


 lib/ext/Roundcube/html.php                     |   15 +--
 lib/ext/Roundcube/rcube.php                    |   23 +++--
 lib/ext/Roundcube/rcube_addressbook.php        |   34 ++++++-
 lib/ext/Roundcube/rcube_browser.php            |   36 +++----
 lib/ext/Roundcube/rcube_charset.php            |   12 ++
 lib/ext/Roundcube/rcube_config.php             |    6 +
 lib/ext/Roundcube/rcube_contacts.php           |   56 +++++++-----
 lib/ext/Roundcube/rcube_csv2vcard.php          |   10 +-
 lib/ext/Roundcube/rcube_db.php                 |   29 +++---
 lib/ext/Roundcube/rcube_html2text.php          |    4 
 lib/ext/Roundcube/rcube_imap.php               |   55 +++++++++++-
 lib/ext/Roundcube/rcube_imap_cache.php         |    6 -
 lib/ext/Roundcube/rcube_ldap.php               |   90 +++++++++++++++----
 lib/ext/Roundcube/rcube_message.php            |   14 +--
 lib/ext/Roundcube/rcube_mime.php               |    2 
 lib/ext/Roundcube/rcube_plugin.php             |    2 
 lib/ext/Roundcube/rcube_plugin_api.php         |   11 +-
 lib/ext/Roundcube/rcube_session.php            |   45 +++++++++
 lib/ext/Roundcube/rcube_spellcheck_atd.php     |   12 ++
 lib/ext/Roundcube/rcube_spellcheck_enchant.php |   18 +++
 lib/ext/Roundcube/rcube_spellcheck_engine.php  |    7 +
 lib/ext/Roundcube/rcube_spellcheck_googie.php  |   36 +++++--
 lib/ext/Roundcube/rcube_spellcheck_pspell.php  |   29 ++++++
 lib/ext/Roundcube/rcube_spellchecker.php       |   58 +++++++++++-
 lib/ext/Roundcube/rcube_storage.php            |   12 ++
 lib/ext/Roundcube/rcube_user.php               |   46 ++++++----
 lib/ext/Roundcube/rcube_utils.php              |  113 +++++++++++++++++--------
 lib/ext/Roundcube/rcube_vcard.php              |   33 ++++---
 lib/ext/Roundcube/rcube_washtml.php            |   92 +++++++++++++++++++-
 29 files changed, 697 insertions(+), 209 deletions(-)

New commits:
commit 764938b9b1525a4645bf4cce009f9a35dd5545ec
Author: Aleksander Machniak <alec at alec.pl>
Date:   Thu Jan 2 15:54:12 2014 +0100

    Update Roundcube Framework

diff --git a/lib/ext/Roundcube/html.php b/lib/ext/Roundcube/html.php
index a367112..f6f744c 100644
--- a/lib/ext/Roundcube/html.php
+++ b/lib/ext/Roundcube/html.php
@@ -3,7 +3,7 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
+ | Copyright (C) 2005-2013, The Roundcube Dev Team                       |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -32,8 +32,8 @@ class html
 
     public static $doctype = 'xhtml';
     public static $lc_tags = true;
-    public static $common_attrib = array('id','class','style','title','align');
-    public static $containers = array('iframe','div','span','p','h1','h2','h3','form','textarea','table','thead','tbody','tr','th','td','style','script');
+    public static $common_attrib = array('id','class','style','title','align','unselectable');
+    public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script');
 
 
     /**
@@ -604,16 +604,17 @@ class html_select extends html
      *
      * @param mixed $names  Option name or array with option names
      * @param mixed $values Option value or array with option values
+     * @param array $attrib Additional attributes for the option entry
      */
-    public function add($names, $values = null)
+    public function add($names, $values = null, $attrib = array())
     {
         if (is_array($names)) {
             foreach ($names as $i => $text) {
-                $this->options[] = array('text' => $text, 'value' => $values[$i]);
+                $this->options[] = array('text' => $text, 'value' => $values[$i]) + $attrib;
             }
         }
         else {
-            $this->options[] = array('text' => $names, 'value' => $values);
+            $this->options[] = array('text' => $names, 'value' => $values) + $attrib;
         }
     }
 
@@ -644,7 +645,7 @@ class html_select extends html
                 $option_content = self::quote($option_content);
             }
 
-            $this->content .= self::tag('option', $attr, $option_content);
+            $this->content .= self::tag('option', $attr + $option, $option_content, array('value','label','class','style','title','disabled','selected'));
         }
 
         return parent::show();
diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php
index 399f84f..503e29d 100644
--- a/lib/ext/Roundcube/rcube.php
+++ b/lib/ext/Roundcube/rcube.php
@@ -642,10 +642,11 @@ class rcube
     /**
      * Load a localization package
      *
-     * @param string Language ID
-     * @param array  Additional text labels/messages
+     * @param string $lang  Language ID
+     * @param array  $add   Additional text labels/messages
+     * @param array  $merge Additional text labels/messages to merge
      */
-    public function load_language($lang = null, $add = array())
+    public function load_language($lang = null, $add = array(), $merge = array())
     {
         $lang = $this->language_prop(($lang ? $lang : $_SESSION['language']));
 
@@ -685,6 +686,11 @@ class rcube
         if (is_array($add) && !empty($add)) {
             $this->texts += $add;
         }
+
+        // merge additional texts (from plugin)
+        if (is_array($merge) && !empty($merge)) {
+            $this->texts = array_merge($this->texts, $merge);
+        }
     }
 
 
@@ -1146,7 +1152,6 @@ class rcube
         // handle PHP exceptions
         if (is_object($arg) && is_a($arg, 'Exception')) {
             $arg = array(
-                'type' => 'php',
                 'code' => $arg->getCode(),
                 'line' => $arg->getLine(),
                 'file' => $arg->getFile(),
@@ -1154,7 +1159,7 @@ class rcube
             );
         }
         else if (is_string($arg)) {
-            $arg = array('message' => $arg, 'type' => 'php');
+            $arg = array('message' => $arg);
         }
 
         if (empty($arg['code'])) {
@@ -1170,7 +1175,7 @@ class rcube
 
         $cli = php_sapi_name() == 'cli';
 
-        if (($log || $terminate) && !$cli && $arg['type'] && $arg['message']) {
+        if (($log || $terminate) && !$cli && $arg['message']) {
             $arg['fatal'] = $terminate;
             self::log_bug($arg);
         }
@@ -1198,7 +1203,7 @@ class rcube
      */
     public static function log_bug($arg_arr)
     {
-        $program = strtoupper($arg_arr['type']);
+        $program = strtoupper(!empty($arg_arr['type']) ? $arg_arr['type'] : 'php');
         $level   = self::get_instance()->config->get('debug_level');
 
         // disable errors for ajax requests, write to log instead (#1487831)
@@ -1537,6 +1542,10 @@ class rcube
                     !empty($response) ? join('; ', $response) : ''));
             }
         }
+        else {
+            // allow plugins to catch sending errors with the same parameters as in 'message_before_send'
+            $this->plugins->exec_hook('message_send_error', $plugin + array('error' => $error));
+        }
 
         if (is_resource($msg_body)) {
             fclose($msg_body);
diff --git a/lib/ext/Roundcube/rcube_addressbook.php b/lib/ext/Roundcube/rcube_addressbook.php
index 6e2b439..4d9fa3d 100644
--- a/lib/ext/Roundcube/rcube_addressbook.php
+++ b/lib/ext/Roundcube/rcube_addressbook.php
@@ -209,6 +209,7 @@ abstract class rcube_addressbook
     public function validate(&$save_data, $autofix = false)
     {
         $rcube = rcube::get_instance();
+        $valid = true;
 
         // check validity of email addresses
         foreach ($this->get_col_values('email', $save_data, true) as $email) {
@@ -216,12 +217,28 @@ abstract class rcube_addressbook
                 if (!rcube_utils::check_email(rcube_utils::idn_to_ascii($email))) {
                     $error = $rcube->gettext(array('name' => 'emailformaterror', 'vars' => array('email' => $email)));
                     $this->set_error(self::ERROR_VALIDATE, $error);
-                    return false;
+                    $valid = false;
+                    break;
                 }
             }
         }
 
-        return true;
+        // allow plugins to do contact validation and auto-fixing
+        $plugin = $rcube->plugins->exec_hook('contact_validate', array(
+            'record'  => $save_data,
+            'autofix' => $autofix,
+            'valid'   => $valid,
+        ));
+
+        if ($valid && !$plugin['valid']) {
+            $this->set_error(self::ERROR_VALIDATE, $plugin['error']);
+        }
+
+        if (is_array($plugin['record'])) {
+            $save_data = $plugin['record'];
+        }
+
+        return $plugin['valid'];
     }
 
     /**
@@ -264,7 +281,8 @@ abstract class rcube_addressbook
      * @param array Assoziative array with save data
      *  Keys:   Field name with optional section in the form FIELD:SECTION
      *  Values: Field value. Can be either a string or an array of strings for multiple values
-     * @return boolean True on success, False on error
+     *
+     * @return mixed On success if ID has been changed returns ID, otherwise True, False on error
      */
     function update($id, $save_cols)
     {
@@ -294,8 +312,10 @@ abstract class rcube_addressbook
 
     /**
      * Mark all records in database as deleted
+     *
+     * @param bool $with_groups Remove also groups
      */
-    function delete_all()
+    function delete_all($with_groups = false)
     {
         /* empty for read-only address books */
     }
@@ -515,8 +535,12 @@ abstract class rcube_addressbook
             $fn = join(' ', array($contact['surname'], $contact['firstname'], $contact['middlename']));
         else if ($compose_mode == 1)
             $fn = join(' ', array($contact['firstname'], $contact['middlename'], $contact['surname']));
-        else
+        else if ($compose_mode == 0)
             $fn = !empty($contact['name']) ? $contact['name'] : join(' ', array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']));
+        else {
+            $plugin = rcube::get_instance()->plugins->exec_hook('contact_listname', array('contact' => $contact));
+            $fn     = $plugin['fn'];
+        }
 
         $fn = trim($fn, ', ');
 
diff --git a/lib/ext/Roundcube/rcube_browser.php b/lib/ext/Roundcube/rcube_browser.php
index 3412829..e53e312 100644
--- a/lib/ext/Roundcube/rcube_browser.php
+++ b/lib/ext/Roundcube/rcube_browser.php
@@ -28,32 +28,24 @@ class rcube_browser
     {
         $HTTP_USER_AGENT = strtolower($_SERVER['HTTP_USER_AGENT']);
 
-        $this->ver = 0;
-        $this->win = strpos($HTTP_USER_AGENT, 'win') != false;
-        $this->mac = strpos($HTTP_USER_AGENT, 'mac') != false;
+        $this->ver   = 0;
+        $this->win   = strpos($HTTP_USER_AGENT, 'win') != false;
+        $this->mac   = strpos($HTTP_USER_AGENT, 'mac') != false;
         $this->linux = strpos($HTTP_USER_AGENT, 'linux') != false;
         $this->unix  = strpos($HTTP_USER_AGENT, 'unix') != false;
 
-        $this->opera = strpos($HTTP_USER_AGENT, 'opera') !== false;
-        $this->ns4 = strpos($HTTP_USER_AGENT, 'mozilla/4') !== false && strpos($HTTP_USER_AGENT, 'msie') === false;
-        $this->ns  = ($this->ns4 || strpos($HTTP_USER_AGENT, 'netscape') !== false);
-        $this->ie  = !$this->opera && strpos($HTTP_USER_AGENT, 'compatible; msie') !== false;
-        $this->khtml = strpos($HTTP_USER_AGENT, 'khtml') !== false;
-        $this->mz  = !$this->ie && !$this->khtml && strpos($HTTP_USER_AGENT, 'mozilla/5') !== false;
+        $this->opera  = strpos($HTTP_USER_AGENT, 'opera') !== false;
+        $this->ns     = strpos($HTTP_USER_AGENT, 'netscape') !== false;
         $this->chrome = strpos($HTTP_USER_AGENT, 'chrome') !== false;
-        $this->safari = !$this->chrome && ($this->khtml || strpos($HTTP_USER_AGENT, 'safari') !== false);
+        $this->ie     = !$this->opera && (strpos($HTTP_USER_AGENT, 'compatible; msie') !== false || strpos($HTTP_USER_AGENT, 'trident/') !== false);
+        $this->safari = !$this->chrome && (strpos($HTTP_USER_AGENT, 'safari') !== false || strpos($HTTP_USER_AGENT, 'applewebkit') !== false);
+        $this->mz     = !$this->ie && !$this->safari && !$this->chrome && !$this->ns && strpos($HTTP_USER_AGENT, 'mozilla') !== false;
 
-        if ($this->ns || $this->chrome) {
-            $test = preg_match('/(mozilla|chrome)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs);
-            $this->ver = $test ? (float)$regs[2] : 0;
+        if (preg_match('/(chrome|msie|opera|version|khtml)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
+            $this->ver = (float) $regs[3];
         }
-        else if ($this->mz) {
-            $test = preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs);
-            $this->ver = $test ? (float)$regs[1] : 0;
-        }
-        else if ($this->ie || $this->opera) {
-            $test = preg_match('/(msie|opera) ([0-9.]+)/', $HTTP_USER_AGENT, $regs);
-            $this->ver = $test ? (float)$regs[2] : 0;
+        else if (preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
+            $this->ver = (float) $regs[1];
         }
 
         if (preg_match('/ ([a-z]{2})-([a-z]{2})/', $HTTP_USER_AGENT, $regs))
@@ -61,10 +53,10 @@ class rcube_browser
         else
             $this->lang =  'en';
 
-        $this->dom = ($this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7));
+        $this->dom      = $this->mz || $this->safari || ($this->ie && $this->ver>=5) || ($this->opera && $this->ver>=7);
         $this->pngalpha = $this->mz || $this->safari || ($this->ie && $this->ver>=5.5) ||
             ($this->ie && $this->ver>=5 && $this->mac) || ($this->opera && $this->ver>=7) ? true : false;
-        $this->imgdata = !$this->ie;
+        $this->imgdata  = !$this->ie;
     }
 }
 
diff --git a/lib/ext/Roundcube/rcube_charset.php b/lib/ext/Roundcube/rcube_charset.php
index 19dbf6c..8214982 100644
--- a/lib/ext/Roundcube/rcube_charset.php
+++ b/lib/ext/Roundcube/rcube_charset.php
@@ -199,10 +199,13 @@ class rcube_charset
                     $iconv_options = '';
                 }
             }
+            else {
+                $iconv_options = false;
+            }
         }
 
         // convert charset using iconv module
-        if ($iconv_options !== null && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
+        if ($iconv_options !== false && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
             // throw an exception if iconv reports an illegal character in input
             // it means that input string has been truncated
             set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE);
@@ -214,6 +217,8 @@ class rcube_charset
             restore_error_handler();
 
             if ($_iconv !== false) {
+
+console(mb_convert_encoding($_iconv, 'UTF-8', 'UTF-8'));
                 return $_iconv;
             }
         }
@@ -224,10 +229,13 @@ class rcube_charset
                 $mbstring_list = mb_list_encodings();
                 $mbstring_list = array_map('strtoupper', $mbstring_list);
             }
+            else {
+                $mbstring_list = false;
+            }
         }
 
         // convert charset using mbstring module
-        if ($mbstring_list !== null) {
+        if ($mbstring_list !== false) {
             $aliases['WINDOWS-1257'] = 'ISO-8859-13';
             // it happens that mbstring supports ASCII but not US-ASCII
             if (($from == 'US-ASCII' || $to == 'US-ASCII') && !in_array('US-ASCII', $mbstring_list)) {
diff --git a/lib/ext/Roundcube/rcube_config.php b/lib/ext/Roundcube/rcube_config.php
index 04b914c..0352e47 100644
--- a/lib/ext/Roundcube/rcube_config.php
+++ b/lib/ext/Roundcube/rcube_config.php
@@ -373,7 +373,11 @@ class rcube_config
      */
     public function all()
     {
-        return $this->prop;
+        $rcube  = rcube::get_instance();
+        $plugin = $rcube->plugins->exec_hook('config_get', array(
+            'name' => '*', 'result' => $this->prop));
+
+        return $plugin['result'];
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_contacts.php b/lib/ext/Roundcube/rcube_contacts.php
index 6d01368..d215760 100644
--- a/lib/ext/Roundcube/rcube_contacts.php
+++ b/lib/ext/Roundcube/rcube_contacts.php
@@ -350,7 +350,7 @@ class rcube_contacts extends rcube_addressbook
                 if (in_array($col, $this->table_cols)) {
                     switch ($mode) {
                     case 1: // strict
-                        $where[] = '(' . $this->db->quoteIdentifier($col) . ' = ' . $this->db->quote($val)
+                        $where[] = '(' . $this->db->quote_identifier($col) . ' = ' . $this->db->quote($val)
                             . ' OR ' . $this->db->ilike($col, $val . $AS . '%')
                             . ' OR ' . $this->db->ilike($col, '%' . $AS . $val . $AS . '%')
                             . ' OR ' . $this->db->ilike($col, '%' . $AS . $val) . ')';
@@ -390,7 +390,7 @@ class rcube_contacts extends rcube_addressbook
         }
 
         foreach (array_intersect($required, $this->table_cols) as $col) {
-            $and_where[] = $this->db->quoteIdentifier($col).' <> '.$this->db->quote('');
+            $and_where[] = $this->db->quote_identifier($col).' <> '.$this->db->quote('');
         }
 
         if (!empty($where)) {
@@ -592,8 +592,8 @@ class rcube_contacts extends rcube_addressbook
         // validate e-mail addresses
         $valid = parent::validate($save_data, $autofix);
 
-        // require at least one e-mail address (syntax check is already done)
-        if ($valid && !array_filter($this->get_col_values('email', $save_data, true))) {
+        // require at least one email address or a name
+        if ($valid && !strlen($save_data['firstname'].$save_data['surname'].$save_data['name']) && !array_filter($this->get_col_values('email', $save_data, true))) {
             $this->set_error(self::ERROR_VALIDATE, 'noemailwarning');
             $valid = false;
         }
@@ -626,11 +626,11 @@ class rcube_contacts extends rcube_addressbook
             }
         }
 
-        $save_data = $this->convert_save_data($save_data);
+        $save_data     = $this->convert_save_data($save_data);
         $a_insert_cols = $a_insert_values = array();
 
         foreach ($save_data as $col => $value) {
-            $a_insert_cols[]   = $this->db->quoteIdentifier($col);
+            $a_insert_cols[]   = $this->db->quote_identifier($col);
             $a_insert_values[] = $this->db->quote($value);
         }
 
@@ -655,17 +655,18 @@ class rcube_contacts extends rcube_addressbook
      *
      * @param mixed Record identifier
      * @param array Assoziative array with save data
+     *
      * @return boolean True on success, False on error
      */
     function update($id, $save_cols)
     {
-        $updated = false;
+        $updated   = false;
         $write_sql = array();
-        $record = $this->get_record($id, true);
+        $record    = $this->get_record($id, true);
         $save_cols = $this->convert_save_data($save_cols, $record);
 
         foreach ($save_cols as $col => $value) {
-            $write_sql[] = sprintf("%s=%s", $this->db->quoteIdentifier($col), $this->db->quote($value));
+            $write_sql[] = sprintf("%s=%s", $this->db->quote_identifier($col), $this->db->quote($value));
         }
 
         if (!empty($write_sql)) {
@@ -683,7 +684,7 @@ class rcube_contacts extends rcube_addressbook
             $this->result = null;  // clear current result (from get_record())
         }
 
-        return $updated;
+        return $updated ? true : false;
     }
 
 
@@ -812,16 +813,30 @@ class rcube_contacts extends rcube_addressbook
 
     /**
      * Remove all records from the database
+     *
+     * @param bool $with_groups Remove also groups
+     *
+     * @return int Number of removed records
      */
-    function delete_all()
+    function delete_all($with_groups = false)
     {
         $this->cache = null;
 
-        $this->db->query("UPDATE ".$this->db->table_name($this->db_name).
-            " SET del=1, changed=".$this->db->now().
-            " WHERE user_id = ?", $this->user_id);
+        $this->db->query("UPDATE " . $this->db->table_name($this->db_name)
+            . " SET del = 1, changed = " . $this->db->now()
+            . " WHERE user_id = ?", $this->user_id);
 
-        return $this->db->affected_rows();
+        $count = $this->db->affected_rows();
+
+        if ($with_groups) {
+            $this->db->query("UPDATE " . $this->db->table_name($this->db_groups)
+                . " SET del = 1, changed = " . $this->db->now()
+                . " WHERE user_id = ?", $this->user_id);
+
+            $count += $this->db->affected_rows();
+        }
+
+        return $count;
     }
 
 
@@ -860,11 +875,11 @@ class rcube_contacts extends rcube_addressbook
     function delete_group($gid)
     {
         // flag group record as deleted
-        $sql_result = $this->db->query(
-            "UPDATE ".$this->db->table_name($this->db_groups).
-            " SET del=1, changed=".$this->db->now().
-            " WHERE contactgroup_id=?".
-            " AND user_id=?",
+        $this->db->query(
+            "UPDATE " . $this->db->table_name($this->db_groups)
+            . " SET del = 1, changed = " . $this->db->now()
+            . " WHERE contactgroup_id = ?"
+            . " AND user_id = ?",
             $gid, $this->user_id
         );
 
@@ -873,7 +888,6 @@ class rcube_contacts extends rcube_addressbook
         return $this->db->affected_rows();
     }
 
-
     /**
      * Rename a specific contact group
      *
diff --git a/lib/ext/Roundcube/rcube_csv2vcard.php b/lib/ext/Roundcube/rcube_csv2vcard.php
index 00e6d4e..aa385dc 100644
--- a/lib/ext/Roundcube/rcube_csv2vcard.php
+++ b/lib/ext/Roundcube/rcube_csv2vcard.php
@@ -47,7 +47,7 @@ class rcube_csv2vcard
         //'business_street_2'     => '',
         //'business_street_3'     => '',
         'car_phone'             => 'phone:car',
-        'categories'            => 'categories',
+        'categories'            => 'groups',
         //'children'              => '',
         'company'               => 'organization',
         //'company_main_phone'    => '',
@@ -146,6 +146,9 @@ class rcube_csv2vcard
         'work_title'            => 'jobtitle',
         'work_zip'              => 'zipcode:work',
         'group'                 => 'groups',
+
+        // GMail
+        'groups'                => 'groups',
     );
 
     /**
@@ -427,6 +430,11 @@ class rcube_csv2vcard
             $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
         }
 
+        // categories/groups separator in vCard is ',' not ';'
+        if (!empty($contact['groups'])) {
+            $contact['groups'] = str_replace(';', ',', $contact['groups']);
+        }
+
         // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
         foreach (array('birthday', 'anniversary') as $key) {
             if (!empty($contact[$key])) {
diff --git a/lib/ext/Roundcube/rcube_db.php b/lib/ext/Roundcube/rcube_db.php
index aaba281..2828f26 100644
--- a/lib/ext/Roundcube/rcube_db.php
+++ b/lib/ext/Roundcube/rcube_db.php
@@ -392,7 +392,7 @@ class rcube_db
      */
     protected function _query($query, $offset, $numrows, $params)
     {
-        $query = trim($query);
+        $query = ltrim($query);
 
         $this->db_connect($this->dsn_select($query), true);
 
@@ -405,27 +405,28 @@ class rcube_db
             $query = $this->set_limit($query, $numrows, $offset);
         }
 
-        $params = (array) $params;
-
         // Because in Roundcube we mostly use queries that are
         // executed only once, we will not use prepared queries
         $pos = 0;
         $idx = 0;
 
-        while ($pos = strpos($query, '?', $pos)) {
-            if ($query[$pos+1] == '?') {  // skip escaped ?
-                $pos += 2;
-            }
-            else {
-                $val = $this->quote($params[$idx++]);
-                unset($params[$idx-1]);
-                $query = substr_replace($query, $val, $pos, 1);
-                $pos += strlen($val);
+        if (count($params)) {
+            while ($pos = strpos($query, '?', $pos)) {
+                if ($query[$pos+1] == '?') {  // skip escaped '?'
+                    $pos += 2;
+                }
+                else {
+                    $val = $this->quote($params[$idx++]);
+                    unset($params[$idx-1]);
+                    $query = substr_replace($query, $val, $pos, 1);
+                    $pos += strlen($val);
+                }
             }
         }
 
-        // replace escaped ? back to normal
-        $query = rtrim(strtr($query, array('??' => '?')), ';');
+        // replace escaped '?' back to normal, see self::quote()
+        $query = str_replace('??', '?', $query);
+        $query = rtrim($query, " \t\n\r\0\x0B;");
 
         $this->debug($query);
 
diff --git a/lib/ext/Roundcube/rcube_html2text.php b/lib/ext/Roundcube/rcube_html2text.php
index 6f79e2f..01362e6 100644
--- a/lib/ext/Roundcube/rcube_html2text.php
+++ b/lib/ext/Roundcube/rcube_html2text.php
@@ -608,7 +608,7 @@ class rcube_html2text
                     $this->width = $p_width;
 
                     // Add citation markers and create <pre> block
-                    $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_ballback'), trim($body));
+                    $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_callback'), trim($body));
                     $body = '<pre>' . htmlspecialchars($body) . '</pre>';
 
                     $text = substr_replace($text, $body . "\n", $start, $end + 13 - $start);
@@ -624,7 +624,7 @@ class rcube_html2text
     /**
      * Callback function to correctly add citation markers for blockquote contents
      */
-    public function blockquote_citation_ballback($m)
+    public function blockquote_citation_callback($m)
     {
         $line  = ltrim($m[2]);
         $space = $line[0] == '>' ? '' : ' ';
diff --git a/lib/ext/Roundcube/rcube_imap.php b/lib/ext/Roundcube/rcube_imap.php
index 9faf1bb..4c3bf6f 100644
--- a/lib/ext/Roundcube/rcube_imap.php
+++ b/lib/ext/Roundcube/rcube_imap.php
@@ -680,6 +680,41 @@ class rcube_imap extends rcube_storage
 
 
     /**
+     * Public method for listing message flags
+     *
+     * @param string $folder  Folder name
+     * @param array  $uids    Message UIDs
+     * @param int    $mod_seq Optional MODSEQ value (of last flag update)
+     *
+     * @return array Indexed array with message flags
+     */
+    public function list_flags($folder, $uids, $mod_seq = null)
+    {
+        if (!strlen($folder)) {
+            $folder = $this->folder;
+        }
+
+        if (!$this->check_connection()) {
+            return array();
+        }
+
+        // @TODO: when cache was synchronized in this request
+        // we might already have asked for flag updates, use it.
+
+        $flags  = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq);
+        $result = array();
+
+        if (!empty($flags)) {
+            foreach ($flags as $message) {
+                $result[$message->uid] = $message->flags;
+            }
+        }
+
+        return $result;
+    }
+
+
+    /**
      * Public method for listing headers
      *
      * @param   string   $folder     Folder name
@@ -2121,7 +2156,7 @@ class rcube_imap extends rcube_storage
         // convert charset (if text or message part)
         if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
             // Remove NULL characters if any (#1486189)
-            if (strpos($body, "\x00") !== false) {
+            if ($formatted && strpos($body, "\x00") !== false) {
                 $body = str_replace("\x00", '', $body);
             }
 
@@ -2843,12 +2878,21 @@ class rcube_imap extends rcube_storage
 
     /**
      * Filter the given list of folders according to access rights
+     *
+     * For performance reasons we assume user has full rights
+     * on all personal folders.
      */
     protected function filter_rights($a_folders, $rights)
     {
         $regex = '/('.$rights.')/';
+
         foreach ($a_folders as $idx => $folder) {
+            if ($this->folder_namespace($folder) == 'personal') {
+                continue;
+            }
+
             $myrights = join('', (array)$this->my_rights($folder));
+
             if ($myrights !== null && !preg_match($regex, $myrights)) {
                 unset($a_folders[$idx]);
             }
@@ -3848,9 +3892,12 @@ class rcube_imap extends rcube_storage
     /**
      * Sort folders first by default folders and then in alphabethical order
      *
-     * @param array $a_folders Folders list
+     * @param array $a_folders    Folders list
+     * @param bool  $skip_default Skip default folders handling
+     *
+     * @return array Sorted list
      */
-    protected function sort_folder_list($a_folders)
+    public function sort_folder_list($a_folders, $skip_default = false)
     {
         $a_out = $a_defaults = $folders = array();
 
@@ -3862,7 +3909,7 @@ class rcube_imap extends rcube_storage
                 continue;
             }
 
-            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
+            if (!$skip_default && ($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
                 $a_defaults[$p] = $folder;
             }
             else {
diff --git a/lib/ext/Roundcube/rcube_imap_cache.php b/lib/ext/Roundcube/rcube_imap_cache.php
index a816654..0c3edea 100644
--- a/lib/ext/Roundcube/rcube_imap_cache.php
+++ b/lib/ext/Roundcube/rcube_imap_cache.php
@@ -1250,10 +1250,8 @@ class rcube_imap_cache
 
         unset($msg->replaces);
 
-        if (is_array($msg->structure->parts)) {
-            foreach ($msg->structure->parts as $part) {
-                $this->message_object_prepare($part, $size);
-            }
+        if (is_object($msg->structure)) {
+            $this->message_object_prepare($msg->structure, $size);
         }
 
         if (is_array($msg->parts)) {
diff --git a/lib/ext/Roundcube/rcube_ldap.php b/lib/ext/Roundcube/rcube_ldap.php
index 64288f9..0da3e2c 100644
--- a/lib/ext/Roundcube/rcube_ldap.php
+++ b/lib/ext/Roundcube/rcube_ldap.php
@@ -52,7 +52,7 @@ class rcube_ldap extends rcube_addressbook
      *
      * @var array
      */
-    private static $group_types = array(
+    private $group_types = array(
         'group'                   => 'member',
         'groupofnames'            => 'member',
         'kolabgroupofnames'       => 'member',
@@ -94,6 +94,9 @@ class rcube_ldap extends rcube_addressbook
                 $this->prop['groups']['name_attr'] = 'cn';
             if (empty($this->prop['groups']['scope']))
                 $this->prop['groups']['scope'] = 'sub';
+            // extend group objectclass => member attribute mapping
+            if (!empty($this->prop['groups']['class_member_attr']))
+                $this->group_types = array_merge($this->group_types, $this->prop['groups']['class_member_attr']);
 
             // add group name attrib to the list of attributes to be fetched
             $fetch_attributes[] = $this->prop['groups']['name_attr'];
@@ -292,6 +295,14 @@ class rcube_ldap extends rcube_addressbook
                 if ($this->prop['search_base_dn'] && $this->prop['search_filter']
                     && (strstr($bind_dn, '%dn') || strstr($this->base_dn, '%dn') || strstr($this->groups_base_dn, '%dn'))
                 ) {
+                    $search_attribs = array('uid');
+                     if ($search_bind_attrib = (array)$this->prop['search_bind_attrib']) {
+                         foreach ($search_bind_attrib as $r => $attr) {
+                             $search_attribs[] = $attr;
+                             $replaces[$r] = '';
+                         }
+                     }
+
                     $search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces);
                     $search_base_dn = strtr($this->prop['search_base_dn'], $replaces);
                     $search_filter  = strtr($this->prop['search_filter'], $replaces);
@@ -321,10 +332,18 @@ class rcube_ldap extends rcube_addressbook
                             }
                         }
 
-                        $res = $ldap->search($search_base_dn, $search_filter, 'sub', array('uid'));
+                        $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs);
                         if ($res) {
                             $res->rewind();
                             $replaces['%dn'] = $res->get_dn();
+
+                            // add more replacements from 'search_bind_attrib' config
+                            if ($search_bind_attrib) {
+                                $res = $res->current();
+                                foreach ($search_bind_attrib as $r => $attr) {
+                                    $replaces[$r] = $res[$attr][0];
+                                }
+                            }
                         }
 
                         if ($ldap != $this->ldap) {
@@ -355,6 +374,23 @@ class rcube_ldap extends rcube_addressbook
                 $this->base_dn        = strtr($this->base_dn, $replaces);
                 $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
 
+                // replace placeholders in filter settings
+                if (!empty($this->prop['filter']))
+                    $this->prop['filter'] = strtr($this->prop['filter'], $replaces);
+                if (!empty($this->prop['groups']['filter']))
+                    $this->prop['groups']['filter'] = strtr($this->prop['groups']['filter'], $replaces);
+                if (!empty($this->prop['groups']['member_filter']))
+                    $this->prop['groups']['member_filter'] = strtr($this->prop['groups']['member_filter'], $replaces);
+
+                if (!empty($this->prop['group_filters'])) {
+                    foreach ($this->prop['group_filters'] as $i => $gf) {
+                        if (!empty($gf['base_dn']))
+                            $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces);
+                        if (!empty($gf['filter']))
+                            $this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces);
+                    }
+                }
+
                 if (empty($bind_user)) {
                     $bind_user = $u;
                 }
@@ -559,9 +595,10 @@ class rcube_ldap extends rcube_addressbook
     /**
      * Get all members of the given group
      *
-     * @param string Group DN
-     * @param array  Group entries (if called recursively)
-     * @return array Accumulated group members
+     * @param string  Group DN
+     * @param boolean Count only
+     * @param array   Group entries (if called recursively)
+     * @return array  Accumulated group members
      */
     function list_group_members($dn, $count = false, $entries = null)
     {
@@ -569,7 +606,7 @@ class rcube_ldap extends rcube_addressbook
 
         // fetch group object
         if (empty($entries)) {
-            $attribs = array('dn','objectClass','member','uniqueMember','memberURL');
+            $attribs = array_merge(array('dn','objectClass','memberURL'), array_values($this->group_types));
             $entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs);
             if ($entries === false) {
                 return $group_members;
@@ -581,17 +618,17 @@ class rcube_ldap extends rcube_addressbook
             $attrs = array();
 
             foreach ((array)$entry['objectclass'] as $objectclass) {
-                if (strtolower($objectclass) == 'groupofurls') {
-                    $members       = $this->_list_group_memberurl($dn, $entry, $count);
-                    $group_members = array_merge($group_members, $members);
-                }
-                else if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
+                if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
                     && ($member_attr = strtolower($member_attr)) && !in_array($member_attr, $attrs)
                 ) {
                     $members       = $this->_list_group_members($dn, $entry, $member_attr, $count);
                     $group_members = array_merge($group_members, $members);
                     $attrs[]       = $member_attr;
                 }
+                else if (!empty($entry['memberurl'])) {
+                    $members       = $this->_list_group_memberurl($dn, $entry, $count);
+                    $group_members = array_merge($group_members, $members);
+                }
 
                 if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) {
                     break 2;
@@ -608,6 +645,7 @@ class rcube_ldap extends rcube_addressbook
      * @param string Group DN
      * @param array  Group entry
      * @param string Member attribute to use
+     * @param boolean Count only
      * @return array Accumulated group members
      */
     private function _list_group_members($dn, $entry, $attr, $count)
@@ -621,8 +659,7 @@ class rcube_ldap extends rcube_addressbook
 
         // read these attributes for all members
         $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
-        $attrib[] = 'member';
-        $attrib[] = 'uniqueMember';
+        $attrib = array_merge($attrib, array_values($this->group_types));
         $attrib[] = 'memberURL';
 
         $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
@@ -669,7 +706,7 @@ class rcube_ldap extends rcube_addressbook
             if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
                 $entries = $result->entries();
                 for ($j = 0; $j < $entries['count']; $j++) {
-                    if (self::is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
+                    if ($this->is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
                         $group_members = array_merge($group_members, $nested_group_members);
                     else
                         $group_members[] = $entries[$j];
@@ -1287,8 +1324,10 @@ class rcube_ldap extends rcube_addressbook
 
     /**
      * Remove all contact records
+     *
+     * @param bool $with_groups Delete also groups if enabled
      */
-    function delete_all()
+    function delete_all($with_groups = false)
     {
         // searching for contact entries
         $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
@@ -1299,6 +1338,16 @@ class rcube_ldap extends rcube_addressbook
             }
             $this->delete($dn_list);
         }
+
+        if ($with_groups && $this->groups && ($groups = $this->_fetch_groups()) && count($groups)) {
+            foreach ($groups as $group) {
+                $this->ldap->delete($group['dn']);
+            }
+
+            if ($this->cache) {
+                $this->cache->remove('groups');
+            }
+        }
     }
 
     /**
@@ -1354,7 +1403,7 @@ class rcube_ldap extends rcube_addressbook
             $out[$this->primary_key] = self::dn_encode($rec['dn']);
 
         // determine record type
-        if (self::is_group_entry($rec)) {
+        if ($this->is_group_entry($rec)) {
             $out['_type'] = 'group';
             $out['readonly'] = true;
             $fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr'];
@@ -1479,11 +1528,11 @@ class rcube_ldap extends rcube_addressbook
     /**
      * Determines whether the given LDAP entry is a group record
      */
-    private static function is_group_entry($entry)
+    private function is_group_entry($entry)
     {
         $classes = array_map('strtolower', (array)$entry['objectclass']);
 
-        return count(array_intersect(array_keys(self::$group_types), $classes)) > 0;
+        return count(array_intersect(array_keys($this->group_types), $classes)) > 0;
     }
 
     /**
@@ -1569,11 +1618,12 @@ class rcube_ldap extends rcube_addressbook
         // special case: list groups from 'group_filters' config
         if ($vlv_page === null && !empty($this->prop['group_filters'])) {
             $groups = array();
+            $rcube  = rcube::get_instance();
 
             // list regular groups configuration as special filter
             if (!empty($this->prop['groups']['filter'])) {
                 $id = '__groups__';
-                $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups'), 'virtual' => true) + $this->prop['groups'];
+                $groups[$id] = array('ID' => $id, 'name' => $rcube->gettext('groups'), 'virtual' => true) + $this->prop['groups'];
             }
 
             foreach ($this->prop['group_filters'] as $id => $prop) {
@@ -1914,7 +1964,7 @@ class rcube_ldap extends rcube_addressbook
 
         if (!empty($object_classes)) {
             foreach ((array)$object_classes as $oc) {
-                if ($attr = self::$group_types[strtolower($oc)]) {
+                if ($attr = $this->group_types[strtolower($oc)]) {
                     return $attr;
                 }
             }
diff --git a/lib/ext/Roundcube/rcube_message.php b/lib/ext/Roundcube/rcube_message.php
index 9b662a2..f24ec3e 100644
--- a/lib/ext/Roundcube/rcube_message.php
+++ b/lib/ext/Roundcube/rcube_message.php
@@ -211,16 +211,19 @@ class rcube_message
                 }
 
                 $level = explode('.', $part->mime_id);
+                $depth = count($level);
 
                 // Check if the part belongs to higher-level's multipart part
-                // this can be alternative/related/signed/encrypted, but not mixed
+                // this can be alternative/related/signed/encrypted or mixed
                 while (array_pop($level) !== null) {
-                    if (!count($level)) {
+                    $parent_depth = count($level);
+                    if (!$parent_depth) {
                         return true;
                     }
 
                     $parent = $this->mime_parts[join('.', $level)];
-                    if (!preg_match('/^multipart\/(alternative|related|signed|encrypted)$/', $parent->mimetype)) {
+                    if (!preg_match('/^multipart\/(alternative|related|signed|encrypted|mixed)$/', $parent->mimetype)
+                        || ($parent->mimetype == 'multipart/mixed' && $parent_depth < $depth - 1)) {
                         continue 2;
                     }
                 }
@@ -529,8 +532,9 @@ class rcube_message
                     $part_mimetype = $mail_part->real_mimetype;
                     list($primary_type, $secondary_type) = explode('/', $part_mimetype);
                 }
-                else
-                    $part_mimetype = $mail_part->mimetype;
+                else {
+                    $part_mimetype = $part_orig_mimetype = $mail_part->mimetype;
+                  }
 
                 // multipart/alternative
                 if ($primary_type == 'multipart') {
diff --git a/lib/ext/Roundcube/rcube_mime.php b/lib/ext/Roundcube/rcube_mime.php
index 9c22203..a931c27 100644
--- a/lib/ext/Roundcube/rcube_mime.php
+++ b/lib/ext/Roundcube/rcube_mime.php
@@ -810,7 +810,7 @@ class rcube_mime
         }
 
         $mime_types = $mime_extensions = array();
-        $regex = "/([\w\+\-\.\/]+)\t+([\w\s]+)/i"; 
+        $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
         foreach((array)$lines as $line) {
              // skip comments or mime types w/o any extensions
             if ($line[0] == '#' || !preg_match($regex, $line, $matches))
diff --git a/lib/ext/Roundcube/rcube_plugin.php b/lib/ext/Roundcube/rcube_plugin.php
index 3153a84..aa6d837 100644
--- a/lib/ext/Roundcube/rcube_plugin.php
+++ b/lib/ext/Roundcube/rcube_plugin.php
@@ -109,7 +109,7 @@ abstract class rcube_plugin
      */
     public function require_plugin($plugin_name)
     {
-        return $this->api->load_plugin($plugin_name);
+        return $this->api->load_plugin($plugin_name, true);
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_plugin_api.php b/lib/ext/Roundcube/rcube_plugin_api.php
index 5a25ada..461c3cc 100644
--- a/lib/ext/Roundcube/rcube_plugin_api.php
+++ b/lib/ext/Roundcube/rcube_plugin_api.php
@@ -35,8 +35,9 @@ class rcube_plugin_api
     public $url = 'plugins/';
     public $task = '';
     public $output;
-    public $handlers = array();
-    public $allowed_prefs = array();
+    public $handlers              = array();
+    public $allowed_prefs         = array();
+    public $allowed_session_prefs = array();
 
     protected $plugins = array();
     protected $tasks = array();
@@ -167,10 +168,11 @@ class rcube_plugin_api
      * Load the specified plugin
      *
      * @param string Plugin name
+     * @param boolean Force loading of the plugin even if it doesn't match the filter
      *
      * @return boolean True on success, false if not loaded or failure
      */
-    public function load_plugin($plugin_name)
+    public function load_plugin($plugin_name, $force = false)
     {
         static $plugins_dir;
 
@@ -196,7 +198,7 @@ class rcube_plugin_api
                 // check inheritance...
                 if (is_subclass_of($plugin, 'rcube_plugin')) {
                     // ... task, request type and framed mode
-                    if ((!$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task))
+                    if (($force || !$plugin->task || preg_match('/^('.$plugin->task.')$/i', $this->task))
                         && (!$plugin->noajax || (is_object($this->output) && $this->output->type == 'html'))
                         && (!$plugin->noframe || empty($_REQUEST['_framed']))
                     ) {
@@ -282,6 +284,7 @@ class rcube_plugin_api
         $composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
         if (file_exists($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
           list($info['vendor'], $info['name']) = explode('/', $json['name']);
+          $info['version'] = $json['version'];
           $info['license'] = $json['license'];
           if ($license_uri = $license_uris[$info['license']])
             $info['license_uri'] = $license_uri;
diff --git a/lib/ext/Roundcube/rcube_session.php b/lib/ext/Roundcube/rcube_session.php
index 67072df..caca262 100644
--- a/lib/ext/Roundcube/rcube_session.php
+++ b/lib/ext/Roundcube/rcube_session.php
@@ -34,6 +34,7 @@ class rcube_session
     private $changed;
     private $time_diff = 0;
     private $reloaded = false;
+    private $appends = array();
     private $unsets = array();
     private $gc_handlers = array();
     private $cookiename = 'roundcube_sessauth';
@@ -441,8 +442,19 @@ class rcube_session
 
         $node = &$this->get_node(explode('.', $path), $_SESSION);
 
-        if ($key !== null) $node[$key] = $value;
-        else               $node[] = $value;
+        if ($key !== null) {
+            $node[$key] = $value;
+            $path .= '.' . $key;
+        }
+        else {
+            $node[] = $value;
+        }
+
+        $this->appends[] = $path;
+
+        // when overwriting a previously unset variable
+        if ($this->unsets[$path])
+            unset($this->unsets[$path]);
     }
 
 
@@ -491,13 +503,40 @@ class rcube_session
      */
     public function reload()
     {
+        // collect updated data from previous appends
+        $merge_data = array();
+        foreach ((array)$this->appends as $var) {
+            $path = explode('.', $var);
+            $value = $this->get_node($path, $_SESSION);
+            $k = array_pop($path);
+            $node = &$this->get_node($path, $merge_data);
+            $node[$k] = $value;
+        }
+
         if ($this->key && $this->memcache)
             $data = $this->mc_read($this->key);
         else if ($this->key)
             $data = $this->db_read($this->key);
 
-        if ($data)
+        if ($data) {
             session_decode($data);
+
+            // apply appends and unsets to reloaded data
+            $_SESSION = array_merge_recursive($_SESSION, $merge_data);
+
+            foreach ((array)$this->unsets as $var) {
+                if (isset($_SESSION[$var])) {
+                    unset($_SESSION[$var]);
+                }
+                else {
+                    $path = explode('.', $var);
+                    $k = array_pop($path);
+                    $node = &$this->get_node($path, $_SESSION);
+                    unset($node[$k]);
+                }
+            }
+        }
+
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_spellcheck_atd.php b/lib/ext/Roundcube/rcube_spellcheck_atd.php
index 68e8b7c..9f073f5 100644
--- a/lib/ext/Roundcube/rcube_spellcheck_atd.php
+++ b/lib/ext/Roundcube/rcube_spellcheck_atd.php
@@ -39,6 +39,18 @@ class rcube_spellcheck_atd extends rcube_spellcheck_engine
     );
 
     /**
+     * Return a list of languages supported by this backend
+     *
+     * @see rcube_spellcheck_engine::languages()
+     */
+    function languages()
+    {
+        $langs = array_values($this->langhosts);
+        $langs[] = 'en';
+        return $langs;
+    }
+
+    /**
      * Set content and check spelling
      *
      * @see rcube_spellcheck_engine::check()
diff --git a/lib/ext/Roundcube/rcube_spellcheck_enchant.php b/lib/ext/Roundcube/rcube_spellcheck_enchant.php
index a22251e..14d6fff 100644
--- a/lib/ext/Roundcube/rcube_spellcheck_enchant.php
+++ b/lib/ext/Roundcube/rcube_spellcheck_enchant.php
@@ -31,6 +31,24 @@ class rcube_spellcheck_enchant extends rcube_spellcheck_engine
     private $matches = array();
 
     /**
+     * Return a list of languages supported by this backend
+     *
+     * @see rcube_spellcheck_engine::languages()
+     */
+    function languages()
+    {
+        $this->init();
+
+        $langs = array();
+        $dicts = enchant_broker_list_dicts($this->enchant_broker);
+        foreach ($dicts as $dict) {
+            $langs[] = preg_replace('/-.*$/', '', $dict['lang_tag']);
+        }
+
+        return array_unique($langs);
+    }
+
+    /**
      * Initializes Enchant dictionary
      */
     private function init()
diff --git a/lib/ext/Roundcube/rcube_spellcheck_engine.php b/lib/ext/Roundcube/rcube_spellcheck_engine.php
index 88e10ac..3cb4ca3 100644
--- a/lib/ext/Roundcube/rcube_spellcheck_engine.php
+++ b/lib/ext/Roundcube/rcube_spellcheck_engine.php
@@ -43,6 +43,13 @@ abstract class rcube_spellcheck_engine
     }
 
     /**
+     * Return a list of languages supported by this backend
+     *
+     * @return array Indexed list of language codes
+     */
+    abstract function languages();
+
+    /**
      * Set content and check spelling
      *
      * @param string $text    Text content for spellchecking
diff --git a/lib/ext/Roundcube/rcube_spellcheck_googie.php b/lib/ext/Roundcube/rcube_spellcheck_googie.php
index 70507dc..3777942 100644
--- a/lib/ext/Roundcube/rcube_spellcheck_googie.php
+++ b/lib/ext/Roundcube/rcube_spellcheck_googie.php
@@ -26,13 +26,28 @@
  */
 class rcube_spellcheck_googie extends rcube_spellcheck_engine
 {
-    const GOOGLE_HOST = 'ssl://www.google.com';
-    const GOOGLE_PORT = 443;
+    const GOOGIE_HOST = 'ssl://spell.roundcube.net';
+    const GOOGIE_PORT = 443;
 
     private $matches = array();
     private $content;
 
     /**
+     * Return a list of languages supported by this backend
+     *
+     * @see rcube_spellcheck_engine::languages()
+     */
+    function languages()
+    {
+        return array('am','ar','ar','bg','br','ca','cs','cy','da',
+            'de_CH','de_DE','el','en_GB','en_US',
+            'eo','es','et','eu','fa','fi','fr_FR','ga','gl','gl',
+            'he','hr','hu','hy','is','it','ku','lt','lv','nl',
+            'pl','pt_BR','pt_PT','ro','ru',
+            'sk','sl','sv','uk');
+    }
+
+    /**
      * Set content and check spelling
      *
      * @see rcube_spellcheck_engine::check()
@@ -52,25 +67,25 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine
             $path  = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang;
         }
         else {
-            $host = self::GOOGLE_HOST;
-            $port = self::GOOGLE_PORT;
+            $host = self::GOOGIE_HOST;
+            $port = self::GOOGIE_PORT;
             $path = '/tbproxy/spell?lang=' . $this->lang;
         }
 
-        // Google has some problem with spaces, use \n instead
-        $gtext = str_replace(' ', "\n", $text);
+        $path .= sprintf('&key=%06d', $_SESSION['user_id']);
 
         $gtext = '<?xml version="1.0" encoding="utf-8" ?>'
             .'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
-            .'<text>' . $gtext . '</text>'
+            .'<text>' . htmlspecialchars($text, ENT_QUOTES, RCUBE_CHARSET) . '</text>'
             .'</spellrequest>';
 
         $store = '';
         if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
             $out = "POST $path HTTP/1.0\r\n";
             $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
+            $out .= "User-Agent: Roundcube Webmail/" . RCMAIL_VERSION . " (Googiespell Wrapper)\r\n";
             $out .= "Content-Length: " . strlen($gtext) . "\r\n";
-            $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
+            $out .= "Content-Type: text/xml\r\n";
             $out .= "Connection: Close\r\n\r\n";
             $out .= $gtext;
             fwrite($fp, $out);
@@ -83,8 +98,10 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine
         // parse HTTP response
         if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) {
             $http_status = $m[1];
-            if ($http_status != '200')
+            if ($http_status != '200') {
                 $this->error = 'HTTP ' . $m[1] . $m[2];
+                $this->error .= "\n" . $store;
+            }
         }
 
         if (!$store) {
@@ -92,6 +109,7 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine
         }
         else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) {
             $this->error = "Error code $m[1] returned";
+            $this->error .= preg_match('/<errortext>([^<]+)/', $store, $m) ? ": " . html_entity_decode($m[1]) : '';
         }
 
         preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER);
diff --git a/lib/ext/Roundcube/rcube_spellcheck_pspell.php b/lib/ext/Roundcube/rcube_spellcheck_pspell.php
index ce089ed..b12684e 100644
--- a/lib/ext/Roundcube/rcube_spellcheck_pspell.php
+++ b/lib/ext/Roundcube/rcube_spellcheck_pspell.php
@@ -30,6 +30,35 @@ class rcube_spellcheck_pspell extends rcube_spellcheck_engine
     private $matches = array();
 
     /**
+     * Return a list of languages supported by this backend
+     *
+     * @see rcube_spellcheck_engine::languages()
+     */
+    function languages()
+    {
+        $defaults = array('en');
+        $langs = array();
+
+        // get aspell dictionaries
+        exec('aspell dump dicts', $dicts);
+        if (!empty($dicts)) {
+            $seen = array();
+            foreach ($dicts as $lang) {
+                $lang = preg_replace('/-.*$/', '', $lang);
+                $langc = strlen($lang) == 2 ? $lang.'_'.strtoupper($lang) : $lang;
+                if (!$seen[$langc]++)
+                    $langs[] = $lang;
+            }
+            $langs = array_unique($langs);
+        }
+        else {
+            $langs = $defaults;
+        }
+
+        return $langs;
+    }
+
+    /**
      * Initializes PSpell dictionary
      */
     private function init()
diff --git a/lib/ext/Roundcube/rcube_spellchecker.php b/lib/ext/Roundcube/rcube_spellchecker.php
index 31835db..5b77bda 100644
--- a/lib/ext/Roundcube/rcube_spellchecker.php
+++ b/lib/ext/Roundcube/rcube_spellchecker.php
@@ -65,6 +65,52 @@ class rcube_spellchecker
         }
     }
 
+    /**
+     * Return a list of supported languages
+     */
+    function languages()
+    {
+        // trust configuration
+        $configured = $this->rc->config->get('spellcheck_languages');
+        if (!empty($configured) && is_array($configured) && !$configured[0]) {
+            return $configured;
+        }
+        else if (!empty($configured)) {
+            $langs = (array)$configured;
+        }
+        else if ($this->backend) {
+            $langs = $this->backend->languages();
+        }
+
+        // load index
+        @include(RCUBE_LOCALIZATION_DIR . 'index.inc');
+
+        // add correct labels
+        $languages = array();
+        foreach ($langs as $lang) {
+            $langc = strtolower(substr($lang, 0, 2));
+            $alias = $rcube_language_aliases[$langc];
+            if (!$alias) {
+                $alias = $langc.'_'.strtoupper($langc);
+            }
+            if ($rcube_languages[$lang]) {
+                $languages[$lang] = $rcube_languages[$lang];
+            }
+            else if ($rcube_languages[$alias]) {
+                $languages[$lang] = $rcube_languages[$alias];
+            }
+            else {
+                $languages[$lang] = ucfirst($lang);
+            }
+        }
+
+        // remove possible duplicates (#1489395)
+        $languages = array_unique($languages);
+
+        asort($languages);
+
+        return $languages;
+    }
 
     /**
      * Set content and check spelling
@@ -152,7 +198,7 @@ class rcube_spellchecker
         // send output
         $out = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="'.mb_strlen($this->content).'">';
 
-        foreach ($this->matches as $item) {
+        foreach ((array)$this->matches as $item) {
             $out .= '<c o="'.$item[1].'" l="'.$item[2].'">';
             $out .= is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
             $out .= '</c>';
@@ -173,7 +219,7 @@ class rcube_spellchecker
     {
         $result = array();
 
-        foreach ($this->matches as $item) {
+        foreach ((array)$this->matches as $item) {
             if ($this->engine == 'pspell') {
                 $word = $item[0];
             }
@@ -306,7 +352,7 @@ class rcube_spellchecker
                     "UPDATE ".$this->rc->db->table_name('dictionary')
                     ." SET data = ?"
                     ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
-                        ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?",
+                        ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
                     implode(' ', $plugin['dictionary']), $plugin['language']);
             }
             // don't store empty dict
@@ -314,14 +360,14 @@ class rcube_spellchecker
                 $this->rc->db->query(
                     "DELETE FROM " . $this->rc->db->table_name('dictionary')
                     ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
-                        ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?",
+                        ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
                     $plugin['language']);
             }
         }
         else if (!empty($this->dict)) {
             $this->rc->db->query(
                 "INSERT INTO " .$this->rc->db->table_name('dictionary')
-                ." (user_id, " . $this->rc->db->quoteIdentifier('language') . ", data) VALUES (?, ?, ?)",
+                ." (user_id, " . $this->rc->db->quote_identifier('language') . ", data) VALUES (?, ?, ?)",
                 $plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary']));
         }
     }
@@ -348,7 +394,7 @@ class rcube_spellchecker
             $sql_result = $this->rc->db->query(
                 "SELECT data FROM ".$this->rc->db->table_name('dictionary')
                 ." WHERE user_id ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
-                    ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?",
+                    ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
                 $plugin['language']);
 
             if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) {
diff --git a/lib/ext/Roundcube/rcube_storage.php b/lib/ext/Roundcube/rcube_storage.php
index e697b2c..ca65af1 100644
--- a/lib/ext/Roundcube/rcube_storage.php
+++ b/lib/ext/Roundcube/rcube_storage.php
@@ -360,6 +360,18 @@ abstract class rcube_storage
 
 
     /**
+     * Public method for listing message flags
+     *
+     * @param string $folder  Folder name
+     * @param array  $uids    Message UIDs
+     * @param int    $mod_seq Optional MODSEQ value
+     *
+     * @return array Indexed array with message flags
+     */
+    abstract function list_flags($folder, $uids, $mod_seq = null);
+
+
+    /**
      * Public method for listing headers.
      *
      * @param   string   $folder     Folder name
diff --git a/lib/ext/Roundcube/rcube_user.php b/lib/ext/Roundcube/rcube_user.php
index 5e9c9af..1d5a905 100644
--- a/lib/ext/Roundcube/rcube_user.php
+++ b/lib/ext/Roundcube/rcube_user.php
@@ -163,8 +163,16 @@ class rcube_user
         if (!$this->ID)
             return false;
 
-        $config    = $this->rc->config;
-        $old_prefs = (array)$this->get_prefs();
+        $plugin = $this->rc->plugins->exec_hook('preferences_update', array(
+            'userid' => $this->ID, 'prefs' => $a_user_prefs, 'old' => (array)$this->get_prefs()));
+
+        if (!empty($plugin['abort'])) {
+            return;
+        }
+
+        $a_user_prefs = $plugin['prefs'];
+        $old_prefs    = $plugin['old'];
+        $config       = $this->rc->config;
 
         // merge (partial) prefs array with existing settings
         $save_prefs = $a_user_prefs + $old_prefs;
@@ -213,6 +221,14 @@ class rcube_user
         return false;
     }
 
+    /**
+     * Generate a unique hash to identify this user which
+     */
+    function get_hash()
+    {
+        $key = substr($this->rc->config->get('des_key'), 1, 4);
+        return md5($this->data['user_id'] . $key . $this->data['username'] . '@' . $this->data['mail_host']);
+    }
 
     /**
      * Get default identity of this user
@@ -249,7 +265,7 @@ class rcube_user
             "SELECT * FROM ".$this->db->table_name('identities').
             " WHERE del <> 1 AND user_id = ?".
             ($sql_add ? " ".$sql_add : "").
-            " ORDER BY ".$this->db->quoteIdentifier('standard')." DESC, name ASC, identity_id ASC",
+            " ORDER BY ".$this->db->quote_identifier('standard')." DESC, name ASC, identity_id ASC",
             $this->ID);
 
         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -284,7 +300,7 @@ class rcube_user
         $query_cols = $query_params = array();
 
         foreach ((array)$data as $col => $value) {
-            $query_cols[]   = $this->db->quoteIdentifier($col) . ' = ?';
+            $query_cols[]   = $this->db->quote_identifier($col) . ' = ?';
             $query_params[] = $value;
         }
         $query_params[] = $iid;
@@ -320,7 +336,7 @@ class rcube_user
 
         $insert_cols = $insert_values = array();
         foreach ((array)$data as $col => $value) {
-            $insert_cols[]   = $this->db->quoteIdentifier($col);
+            $insert_cols[]   = $this->db->quote_identifier($col);
             $insert_values[] = $value;
         }
         $insert_cols[]   = 'user_id';
@@ -385,7 +401,7 @@ class rcube_user
         if ($this->ID && $iid) {
             $this->db->query(
                 "UPDATE ".$this->db->table_name('identities').
-                " SET ".$this->db->quoteIdentifier('standard')." = '0'".
+                " SET ".$this->db->quote_identifier('standard')." = '0'".
                 " WHERE user_id = ?".
                     " AND identity_id <> ?".
                     " AND del <> 1",
@@ -625,11 +641,11 @@ class rcube_user
         $result = array();
 
         $sql_result = $this->db->query(
-            "SELECT search_id AS id, ".$this->db->quoteIdentifier('name')
+            "SELECT search_id AS id, ".$this->db->quote_identifier('name')
             ." FROM ".$this->db->table_name('searches')
             ." WHERE user_id = ?"
-                ." AND ".$this->db->quoteIdentifier('type')." = ?"
-            ." ORDER BY ".$this->db->quoteIdentifier('name'),
+                ." AND ".$this->db->quote_identifier('type')." = ?"
+            ." ORDER BY ".$this->db->quote_identifier('name'),
             (int) $this->ID, (int) $type);
 
         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -657,9 +673,9 @@ class rcube_user
         }
 
         $sql_result = $this->db->query(
-            "SELECT ".$this->db->quoteIdentifier('name')
-                .", ".$this->db->quoteIdentifier('data')
-                .", ".$this->db->quoteIdentifier('type')
+            "SELECT ".$this->db->quote_identifier('name')
+                .", ".$this->db->quote_identifier('data')
+                .", ".$this->db->quote_identifier('type')
             ." FROM ".$this->db->table_name('searches')
             ." WHERE user_id = ?"
                 ." AND search_id = ?",
@@ -714,11 +730,11 @@ class rcube_user
 
         $insert_cols[]   = 'user_id';
         $insert_values[] = (int) $this->ID;
-        $insert_cols[]   = $this->db->quoteIdentifier('type');
+        $insert_cols[]   = $this->db->quote_identifier('type');
         $insert_values[] = (int) $data['type'];
-        $insert_cols[]   = $this->db->quoteIdentifier('name');
+        $insert_cols[]   = $this->db->quote_identifier('name');
         $insert_values[] = $data['name'];
-        $insert_cols[]   = $this->db->quoteIdentifier('data');
+        $insert_cols[]   = $this->db->quote_identifier('data');
         $insert_values[] = serialize($data['data']);
 
         $sql = "INSERT INTO ".$this->db->table_name('searches')
diff --git a/lib/ext/Roundcube/rcube_utils.php b/lib/ext/Roundcube/rcube_utils.php
index b73bc08..c48cd80 100644
--- a/lib/ext/Roundcube/rcube_utils.php
+++ b/lib/ext/Roundcube/rcube_utils.php
@@ -454,6 +454,9 @@ class rcube_utils
 
         // cut out all contents between { and }
         while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
+            $nested = strpos($source, '{', $pos+1);
+            if ($nested && $nested < $pos2)  // when dealing with nested blocks (e.g. @media), take the inner one
+                $pos = $nested;
             $length = $pos2 - $pos - 1;
             $styles = substr($source, $pos+1, $length);
 
@@ -619,6 +622,10 @@ class rcube_utils
      */
     public static function parse_host($name, $host = '')
     {
+        if (!is_string($name)) {
+            return $name;
+        }
+
         // %n - host
         $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
         // %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld
@@ -639,8 +646,7 @@ class rcube_utils
             }
         }
 
-        $name = str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name);
-        return $name;
+        return str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name);
     }
 
 
@@ -677,9 +683,17 @@ class rcube_utils
      */
     public static function remote_addr()
     {
-        foreach (array('HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP','REMOTE_ADDR') as $prop) {
-            if (!empty($_SERVER[$prop]))
-                return $_SERVER[$prop];
+        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+            $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
+            return $hosts[0];
+        }
+
+        if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
+            return $_SERVER['HTTP_X_REAL_IP'];
+        }
+
+        if (!empty($_SERVER['REMOTE_ADDR'])) {
+            return $_SERVER['REMOTE_ADDR'];
         }
 
         return '';
@@ -744,40 +758,13 @@ class rcube_utils
      */
     public static function strtotime($date)
     {
-        $date = trim($date);
-
-        // check for MS Outlook vCard date format YYYYMMDD
-        if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
-            return mktime(0,0,0, intval($m[2]), intval($m[3]), intval($m[1]));
-        }
-
-        // common little-endian formats, e.g. dd/mm/yyyy (not all are supported by strtotime)
-        if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})$/', $date, $m)
-            && $m[1] > 0 && $m[1] <= 31 && $m[2] > 0 && $m[2] <= 12 && $m[3] >= 1970
-        ) {
-            return mktime(0,0,0, intval($m[2]), intval($m[1]), intval($m[3]));
-        }
+        $date = self::clean_datestr($date);
 
         // unix timestamp
         if (is_numeric($date)) {
             return (int) $date;
         }
 
-        // Clean malformed data
-        $date = preg_replace(
-            array(
-                '/GMT\s*([+-][0-9]+)/',                     // support non-standard "GMTXXXX" literal
-                '/[^a-z0-9\x20\x09:+-]/i',                  // remove any invalid characters
-                '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i',   // remove weekday names
-            ),
-            array(
-                '\\1',
-                '',
-                '',
-            ), $date);
-
-        $date = trim($date);
-
         // if date parsing fails, we have a date in non-rfc format.
         // remove token from the end and try again
         while ((($ts = @strtotime($date)) === false) || ($ts < 0)) {
@@ -805,8 +792,8 @@ class rcube_utils
             return $date;
         }
 
-        $dt = false;
-        $date = trim($date);
+        $dt   = false;
+        $date = self::clean_datestr($date);
 
         // try to parse string with DateTime first
         if (!empty($date)) {
@@ -831,6 +818,52 @@ class rcube_utils
         return $dt;
     }
 
+    /**
+     * Clean up date string for strtotime() input
+     *
+     * @param string $date Date string
+     *
+     * @return string Date string
+     */
+    public static function clean_datestr($date)
+    {
+        $date = trim($date);
+
+        // check for MS Outlook vCard date format YYYYMMDD
+        if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
+            return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3]));
+        }
+
+        // Clean malformed data
+        $date = preg_replace(
+            array(
+                '/GMT\s*([+-][0-9]+)/',                     // support non-standard "GMTXXXX" literal
+                '/[^a-z0-9\x20\x09:+-\/]/i',                  // remove any invalid characters
+                '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i',   // remove weekday names
+            ),
+            array(
+                '\\1',
+                '',
+                '',
+            ), $date);
+
+        $date = trim($date);
+
+        // try to fix dd/mm vs. mm/dd discrepancy, we can't do more here
+        if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})$/', $date, $m)) {
+            $mdy   = $m[2] > 12 && $m[1] <= 12;
+            $day   = $mdy ? $m[2] : $m[1];
+            $month = $mdy ? $m[1] : $m[2];
+            $date  = sprintf('%04d-%02d-%02d 00:00:00', intval($m[3]), $month, $day);
+        }
+        // I've found that YYYY.MM.DD is recognized wrong, so here's a fix
+        else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})$/', $date)) {
+            $date = str_replace('.', '-', $date) . ' 00:00:00';
+        }
+
+        return $date;
+    }
+
     /*
      * Idn_to_ascii wrapper.
      * Intl/Idn modules version of this function doesn't work with e-mail address
@@ -890,10 +923,20 @@ class rcube_utils
      *
      * @param string  Input string (UTF-8)
      * @param boolean True to return list of words as array
+     *
      * @return mixed  Normalized string or a list of normalized tokens
      */
     public static function normalize_string($str, $as_array = false)
     {
+        // replace 4-byte unicode characters with '?' character,
+        // these are not supported in default utf-8 charset on mysql,
+        // the chance we'd need them in searching is very low
+        $str = preg_replace('/('
+            . '\xF0[\x90-\xBF][\x80-\xBF]{2}'
+            . '|[\xF1-\xF3][\x80-\xBF]{3}'
+            . '|\xF4[\x80-\x8F][\x80-\xBF]{2}'
+            . ')/', '?', $str);
+
         // split by words
         $arr = self::tokenize_string($str);
 
diff --git a/lib/ext/Roundcube/rcube_vcard.php b/lib/ext/Roundcube/rcube_vcard.php
index d54dc56..a54ee7e 100644
--- a/lib/ext/Roundcube/rcube_vcard.php
+++ b/lib/ext/Roundcube/rcube_vcard.php
@@ -378,7 +378,7 @@ class rcube_vcard
         default:
             if ($field == 'phone' && $this->phonetypemap[$type_uc]) {
                 $type = $this->phonetypemap[$type_uc];
-             }
+            }
 
             if (($tag = self::$fieldmap[$field]) && (is_array($value) || strlen($value))) {
                 $index = count($this->raw[$tag]);
@@ -518,20 +518,28 @@ class rcube_vcard
      */
     public static function cleanup($vcard)
     {
-        // Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
-        $vcard = preg_replace(
-            '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
-            '\2;type=\5\3:\4',
-            $vcard);
-
         // convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility
         $vcard = preg_replace_callback(
             '/item(\d+)\.(X-ABRELATEDNAMES)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
             array('self', 'x_abrelatednames_callback'),
             $vcard);
 
-        // Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
-        $vcard = preg_replace(array('/^item\d*\.X-AB.*$/m', '/^item\d*\./m', "/\n+/"), array('', '', "\n"), $vcard);
+        // Cleanup
+        $vcard = preg_replace(array(
+                // convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
+                '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
+                '/^item\d*\.X-AB.*$/m',  // remove cruft like item1.X-AB*
+                '/^item\d*\./m',         // remove item1.ADR instead of ADR
+                '/\n+/',                 // remove empty lines
+                '/^(N:[^;\R]*)$/m',      // if N doesn't have any semicolons, add some
+            ),
+            array(
+                '\2;type=\5\3:\4',
+                '',
+                '',
+                "\n",
+                '\1;;;;',
+            ), $vcard);
 
         // convert X-WAB-GENDER to X-GENDER
         if (preg_match('/X-WAB-GENDER:(\d)/', $vcard, $matches)) {
@@ -539,9 +547,6 @@ class rcube_vcard
             $vcard = preg_replace('/X-WAB-GENDER:\d/', 'X-GENDER:' . $value, $vcard);
         }
 
-        // if N doesn't have any semicolons, add some 
-        $vcard = preg_replace('/^(N:[^;\R]*)$/m', '\1;;;;', $vcard);
-
         return $vcard;
     }
 
@@ -612,8 +617,8 @@ class rcube_vcard
                 $enc   = null;
 
                 foreach($regs2[1] as $attrid => $attr) {
+                    $attr = preg_replace('/[\s\t\n\r\0\x0B]/', '', $attr);
                     if ((list($key, $value) = explode('=', $attr)) && $value) {
-                        $value = trim($value);
                         if ($key == 'ENCODING') {
                             $value = strtoupper($value);
                             // add next line(s) to value string if QP line end detected
@@ -792,7 +797,7 @@ class rcube_vcard
                 return $result;
             }
 
-            $s = strtr($s, $rep2);
+            $s = trim(strtr($s, $rep2));
         }
 
         // some implementations (GMail) use non-standard backslash before colon (#1489085)
diff --git a/lib/ext/Roundcube/rcube_washtml.php b/lib/ext/Roundcube/rcube_washtml.php
index e746754..5a5b3dc 100644
--- a/lib/ext/Roundcube/rcube_washtml.php
+++ b/lib/ext/Roundcube/rcube_washtml.php
@@ -418,7 +418,7 @@ class rcube_washtml
         $html = preg_replace($html_search, $html_replace, trim($html));
 
         //-> Replace all of those weird MS Word quotes and other high characters
-        $badwordchars=array(
+        $badwordchars = array(
             "\xe2\x80\x98", // left single quote
             "\xe2\x80\x99", // right single quote
             "\xe2\x80\x9c", // left double quote
@@ -426,7 +426,7 @@ class rcube_washtml
             "\xe2\x80\x94", // em dash
             "\xe2\x80\xa6" // elipses
         );
-        $fixedwordchars=array(
+        $fixedwordchars = array(
             "'",
             "'",
             '"',
@@ -434,7 +434,7 @@ class rcube_washtml
             '—',
             '...'
         );
-        $html = str_replace($badwordchars,$fixedwordchars, $html);
+        $html = str_replace($badwordchars, $fixedwordchars, $html);
 
         // PCRE errors handling (#1486856), should we use something like for every preg_* use?
         if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
@@ -455,13 +455,16 @@ class rcube_washtml
         }
 
         // fix (unknown/malformed) HTML tags before "wash"
-        $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)/', array($this, 'html_tag_callback'), $html);
+        $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)([^>]*)/', array($this, 'html_tag_callback'), $html);
 
         // Remove invalid HTML comments (#1487759)
         // Don't remove valid conditional comments
         // Don't remove MSOutlook (<!-->) conditional comments (#1489004)
         $html = preg_replace('/<!--[^->\[\n]+>/', '', $html);
 
+        // fix broken nested lists
+        self::fix_broken_lists($html);
+
         // turn relative into absolute urls
         $html = self::resolve_base($html);
 
@@ -479,7 +482,12 @@ class rcube_washtml
             '/[^a-z0-9_\[\]\!-]/i', // forbidden characters
         ), '', $tagname);
 
-        return $matches[1] . $tagname;
+        // fix invalid closing tags - remove any attributes (#1489446)
+        if ($matches[1] == '</') {
+            $matches[3] = '';
+        }
+
+        return $matches[1] . $tagname . $matches[3];
     }
 
     /**
@@ -495,5 +503,77 @@ class rcube_washtml
 
         return $body;
     }
-}
 
+    /**
+     * Fix broken nested lists, they are not handled properly by DOMDocument (#1488768)
+     */
+    public static function fix_broken_lists(&$html)
+    {
+        // do two rounds, one for <ol>, one for <ul>
+        foreach (array('ol', 'ul') as $tag) {
+            $pos = 0;
+            while (($pos = stripos($html, '<' . $tag, $pos)) !== false) {
+                $pos++;
+
+                // make sure this is an ol/ul tag
+                if (!in_array($html[$pos+2], array(' ', '>'))) {
+                    continue;
+                }
+
+                $p      = $pos;
+                $in_li  = false;
+                $li_pos = 0;
+
+                while (($p = strpos($html, '<', $p)) !== false) {
+                    $tt = strtolower(substr($html, $p, 4));
+
+                    // li open tag
+                    if ($tt == '<li>' || $tt == '<li ') {
+                        $in_li = true;
+                        $p += 4;
+                    }
+                    // li close tag
+                    else if ($tt == '</li' && in_array($html[$p+4], array(' ', '>'))) {
+                        $li_pos = $p;
+                        $p += 4;
+                        $in_li = false;
+                    }
+                    // ul/ol closing tag
+                    else if ($tt == '</' . $tag && in_array($html[$p+4], array(' ', '>'))) {
+                        break;
+                    }
+                    // nested ol/ul element out of li
+                    else if (!$in_li && $li_pos && ($tt == '<ol>' || $tt == '<ol ' || $tt == '<ul>' || $tt == '<ul ')) {
+                        // find closing tag of this ul/ol element
+                        $element = substr($tt, 1, 2);
+                        $cpos    = $p;
+                        do {
+                            $tpos = stripos($html, '<' . $element, $cpos+1);
+                            $cpos = stripos($html, '</' . $element, $cpos+1);
+                        }
+                        while ($tpos !== false && $cpos !== false && $cpos > $tpos);
+
+                        // not found, this is invalid HTML, skip it
+                        if ($cpos === false) {
+                            break;
+                        }
+
+                        // get element content
+                        $end     = strpos($html, '>', $cpos);
+                        $len     = $end - $p + 1;
+                        $element = substr($html, $p, $len);
+
+                        // move element to the end of the last li
+                        $html    = substr_replace($html, '', $p, $len);
+                        $html    = substr_replace($html, $element, $li_pos, 0);
+
+                        $p = $end;
+                    }
+                    else {
+                        $p++;
+                    }
+                }
+            }
+        }
+    }
+}




More information about the commits mailing list