lib/ext lib/plugins

Aleksander Machniak machniak at kolabsys.com
Mon Jul 1 15:13:49 CEST 2013


 lib/ext/Roundcube/bootstrap.php                            |    3 
 lib/ext/Roundcube/rcube.php                                |    2 
 lib/ext/Roundcube/rcube_addressbook.php                    |    6 
 lib/ext/Roundcube/rcube_imap.php                           |  117 -
 lib/ext/Roundcube/rcube_imap_generic.php                   |  154 -
 lib/ext/Roundcube/rcube_ldap.php                           | 1265 ++++---------
 lib/ext/Roundcube/rcube_ldap_generic.php                   | 1049 ++++++++++
 lib/ext/Roundcube/rcube_ldap_result.php                    |  130 +
 lib/ext/Roundcube/rcube_spellchecker.php                   |    2 
 lib/ext/Roundcube/rcube_storage.php                        |   13 
 lib/ext/Roundcube/rcube_utils.php                          |    6 
 lib/ext/Roundcube/rcube_vcard.php                          |   10 
 lib/plugins/kolab_auth/config.inc.php.dist                 |   11 
 lib/plugins/kolab_auth/kolab_auth.php                      |  119 -
 lib/plugins/kolab_auth/kolab_auth_ldap.php                 |  489 +++++
 lib/plugins/kolab_auth/package.xml                         |   10 
 lib/plugins/libkolab/SQL/mysql.initial.sql                 |    6 
 lib/plugins/libkolab/SQL/mysql/2013041900.sql              |    3 
 lib/plugins/libkolab/lib/kolab_format_contact.php          |    2 
 lib/plugins/libkolab/lib/kolab_format_distributionlist.php |    8 
 lib/plugins/libkolab/lib/kolab_format_file.php             |   18 
 lib/plugins/libkolab/lib/kolab_storage.php                 |   21 
 lib/plugins/libkolab/lib/kolab_storage_cache.php           |   34 
 lib/plugins/libkolab/lib/kolab_storage_folder.php          |   95 
 lib/plugins/libkolab/package.xml                           |    7 
 25 files changed, 2423 insertions(+), 1157 deletions(-)

New commits:
commit ba2936bf74b80a76456fd33b9f4ea120ed50e348
Author: Aleksander Machniak <alec at alec.pl>
Date:   Mon Jul 1 15:13:21 2013 +0200

    Update Roundcube Framework and plugins

diff --git a/lib/ext/Roundcube/bootstrap.php b/lib/ext/Roundcube/bootstrap.php
index 68d3142..182ea12 100644
--- a/lib/ext/Roundcube/bootstrap.php
+++ b/lib/ext/Roundcube/bootstrap.php
@@ -44,7 +44,8 @@ if (php_sapi_name() != 'cli') {
 }
 
 foreach ($config as $optname => $optval) {
-    if ($optval != ini_get($optname) && @ini_set($optname, $optval) === false) {
+    $ini_optval = filter_var(ini_get($optname), FILTER_VALIDATE_BOOLEAN);
+    if ($optval != $ini_optval && @ini_set($optname, $optval) === false) {
         $error = "ERROR: Wrong '$optname' option value and it wasn't possible to set it to required value ($optval).\n"
             . "Check your PHP configuration (including php_admin_flag).";
         if (defined('STDERR')) fwrite(STDERR, $error); else echo $error;
diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php
index 21b49f4..6543a39 100644
--- a/lib/ext/Roundcube/rcube.php
+++ b/lib/ext/Roundcube/rcube.php
@@ -1487,7 +1487,7 @@ class rcube
                     $subject    = str_replace("\r\n", $delim, $subject);
                 }
 
-                if (ini_get('safe_mode'))
+                if (filter_var(ini_get('safe_mode'), FILTER_VALIDATE_BOOLEAN))
                     $sent = mail($to, $subject, $msg_body, $header_str);
                 else
                     $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
diff --git a/lib/ext/Roundcube/rcube_addressbook.php b/lib/ext/Roundcube/rcube_addressbook.php
index d23ad36..4ed139c 100644
--- a/lib/ext/Roundcube/rcube_addressbook.php
+++ b/lib/ext/Roundcube/rcube_addressbook.php
@@ -539,10 +539,10 @@ abstract class rcube_addressbook
 
         // add email to a key to not skip contacts with the same name (#1488375)
         if (!empty($contact['email'])) {
-             $key .= ':' . implode(':', (array)$contact['email']);
-         }
+            $key .= ':' . implode(':', (array)$contact['email']);
+        }
 
-         return $key;
+        return $key;
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_imap.php b/lib/ext/Roundcube/rcube_imap.php
index 3ca8a07..7ef8d62 100644
--- a/lib/ext/Roundcube/rcube_imap.php
+++ b/lib/ext/Roundcube/rcube_imap.php
@@ -1189,11 +1189,13 @@ class rcube_imap extends rcube_storage
      * @param string $sort_field Sort column
      * @param string $sort_order Sort order [ASC, DESC]
      * @param bool   $no_threads Get not threaded index
+     * @param bool   $no_search  Get index not limited to search result (optionally)
      *
      * @return rcube_result_index|rcube_result_thread List of messages (UIDs)
      */
-    public function index($folder = '', $sort_field = NULL, $sort_order = NULL, $no_threads = false)
-    {
+    public function index($folder = '', $sort_field = NULL, $sort_order = NULL,
+        $no_threads = false, $no_search = false
+    ) {
         if (!$no_threads && $this->threading) {
             return $this->thread_index($folder, $sort_field, $sort_order);
         }
@@ -1206,43 +1208,50 @@ class rcube_imap extends rcube_storage
 
         // we have a saved search result, get index from there
         if ($this->search_string) {
-            if ($this->search_threads) {
-                $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field);
+            if ($this->search_set->is_empty()) {
+                return new rcube_result_index($folder, '* SORT');
             }
 
-            // use message index sort as default sorting
-            if (!$this->sort_field || $this->search_sorted) {
-                if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
-                    $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field);
-                }
+            // search result is an index with the same sorting?
+            if (($this->search_set instanceof rcube_result_index)
+                && ((!$this->sort_field && !$this->search_sorted) ||
+                    ($this->search_sorted && $this->search_sort_field == $this->sort_field))
+            ) {
                 $index = $this->search_set;
             }
-            else if (!$this->check_connection()) {
-                return new rcube_result_index();
-            }
-            else {
-                $index = $this->conn->index($folder, $this->search_set->get(),
-                    $this->sort_field, $this->options['skip_deleted'], true, true);
+            // $no_search is enabled when we are not interested in
+            // fetching index for search result, e.g. to sort
+            // threaded search result we can use full mailbox index.
+            // This makes possible to use index from cache
+            else if (!$no_search) {
+                if (!$this->sort_field) {
+                    // No sorting needed, just build index from the search result
+                    // @TODO: do we need to sort by UID here?
+                    $search = $this->search_set->get_compressed();
+                    $index  = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
+                }
+                else {
+                    $index = $this->index_direct($folder, $this->search_charset,
+                        $this->sort_field, $this->search_set);
+                }
             }
 
-            if ($this->sort_order != $index->get_parameters('ORDER')) {
-                $index->revert();
-            }
+            if (isset($index)) {
+                if ($this->sort_order != $index->get_parameters('ORDER')) {
+                    $index->revert();
+                }
 
-            return $index;
+                return $index;
+            }
         }
 
         // check local cache
         if ($mcache = $this->get_mcache_engine()) {
-            $index = $mcache->get_index($folder, $this->sort_field, $this->sort_order);
-        }
-        // fetch from IMAP server
-        else {
-            $index = $this->index_direct(
-                $folder, $this->sort_field, $this->sort_order);
+            return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
         }
 
-        return $index;
+        // fetch from IMAP server
+        return $this->index_direct($folder, $this->sort_field, $this->sort_order);
     }
 
 
@@ -1250,18 +1259,24 @@ class rcube_imap extends rcube_storage
      * Return sorted list of message UIDs ignoring current search settings.
      * Doesn't uses cache by default.
      *
-     * @param string $folder     Folder to get index from
-     * @param string $sort_field Sort column
-     * @param string $sort_order Sort order [ASC, DESC]
+     * @param string         $folder     Folder to get index from
+     * @param string         $sort_field Sort column
+     * @param string         $sort_order Sort order [ASC, DESC]
+     * @param rcube_result_* $search     Optional messages set to limit the result
      *
      * @return rcube_result_index Sorted list of message UIDs
      */
-    public function index_direct($folder, $sort_field = null, $sort_order = null)
+    public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
     {
+        if (!empty($search)) {
+            $search = $this->search_set->get_compressed();
+        }
+
         // use message index sort as default sorting
         if (!$sort_field) {
             // use search result from count() if possible
-            if ($this->options['skip_deleted'] && !empty($this->icache['undeleted_idx'])
+            if (empty($search) && $this->options['skip_deleted']
+                && !empty($this->icache['undeleted_idx'])
                 && $this->icache['undeleted_idx']->get_parameters('ALL') !== null
                 && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
             ) {
@@ -1271,8 +1286,12 @@ class rcube_imap extends rcube_storage
                 return new rcube_result_index();
             }
             else {
-                $index = $this->conn->search($folder,
-                    'ALL' .($this->options['skip_deleted'] ? ' UNDELETED' : ''), true);
+                $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
+                if ($search) {
+                    $query = trim($query . ' UID ' . $search);
+                }
+
+                $index = $this->conn->search($folder, $query, true);
             }
         }
         else if (!$this->check_connection()) {
@@ -1281,13 +1300,18 @@ class rcube_imap extends rcube_storage
         // fetch complete message index
         else {
             if ($this->get_capability('SORT')) {
-                $index = $this->conn->sort($folder, $sort_field,
-                    $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
+                $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
+                if ($search) {
+                    $query = trim($query . ' UID ' . $search);
+                }
+
+                $index = $this->conn->sort($folder, $sort_field, $query, true);
             }
 
             if (empty($index) || $index->is_error()) {
-                $index = $this->conn->index($folder, "1:*", $sort_field,
-                    $this->options['skip_deleted'], false, true);
+                $index = $this->conn->index($folder, $search ? $search : "1:*",
+                    $sort_field, $this->options['skip_deleted'],
+                    $search ? true : false, true);
             }
         }
 
@@ -1346,9 +1370,9 @@ class rcube_imap extends rcube_storage
         // THREAD=REFERENCES:     sorting by sent date of root message
         // THREAD=REFS:           sorting by the most recent date in each thread
 
-        if ($this->get_capability('THREAD') != 'REFS') {
+        if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
             $sortby = $this->sort_field ? $this->sort_field : 'date';
-            $index  = $this->index($this->folder, $sortby, $this->sort_order, true);
+            $index  = $this->index($this->folder, $sortby, $this->sort_order, true, true);
 
             if (!$index->is_empty()) {
                 $threads->sort($index);
@@ -2243,13 +2267,14 @@ class rcube_imap extends rcube_storage
     /**
      * Append a mail message (source) to a specific folder
      *
-     * @param string  $folder  Target folder
-     * @param string  $message The message source string or filename
-     * @param string  $headers Headers string if $message contains only the body
-     * @param boolean $is_file True if $message is a filename
-     * @param array   $flags   Message flags
-     * @param mixed   $date    Message internal date
-     * @param bool    $binary  Enables BINARY append
+     * @param string       $folder  Target folder
+     * @param string|array $message The message source string or filename
+     *                              or array (of strings and file pointers)
+     * @param string       $headers Headers string if $message contains only the body
+     * @param boolean      $is_file True if $message is a filename
+     * @param array        $flags   Message flags
+     * @param mixed        $date    Message internal date
+     * @param bool         $binary  Enables BINARY append
      *
      * @return int|bool Appended message UID or True on success, False on error
      */
diff --git a/lib/ext/Roundcube/rcube_imap_generic.php b/lib/ext/Roundcube/rcube_imap_generic.php
index 292b932..1928c70 100644
--- a/lib/ext/Roundcube/rcube_imap_generic.php
+++ b/lib/ext/Roundcube/rcube_imap_generic.php
@@ -1563,11 +1563,12 @@ class rcube_imap_generic
         }
 
         // message IDs
-        if (!empty($add))
+        if (!empty($add)) {
             $add = $this->compressMessageSet($add);
+        }
 
         list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
-            array("($field)", $encoding, 'ALL' . (!empty($add) ? ' '.$add : '')));
+            array("($field)", $encoding, !empty($add) ? $add : 'ALL'));
 
         if ($code != self::ERROR_OK) {
             $response = null;
@@ -2505,11 +2506,13 @@ class rcube_imap_generic
             // handle one line response
             if ($line[0] == '(' && substr($line, -1) == ')') {
                 // tokenize content inside brackets
+                // the content can be e.g.: (UID 9844 BODY[2.4] NIL)
                 $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line));
 
                 for ($i=0; $i<count($tokens); $i+=2) {
                     if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) {
-                        $result = $tokens[$i+1];
+                        $i += 2; // skip BODY|BINARY and part number
+                        $result = $tokens[$i];
                         $found  = true;
                         break;
                     }
@@ -2615,11 +2618,11 @@ class rcube_imap_generic
     /**
      * Handler for IMAP APPEND command
      *
-     * @param string $mailbox Mailbox name
-     * @param string $message Message content
-     * @param array  $flags   Message flags
-     * @param string $date    Message internal date
-     * @param bool   $binary  Enable BINARY append (RFC3516)
+     * @param string       $mailbox Mailbox name
+     * @param string|array $message The message source string or array (of strings and file pointers)
+     * @param array        $flags   Message flags
+     * @param string       $date    Message internal date
+     * @param bool         $binary  Enable BINARY append (RFC3516)
      *
      * @return string|bool On success APPENDUID response (if available) or True, False on failure
      */
@@ -2633,13 +2636,28 @@ class rcube_imap_generic
 
         $binary       = $binary && $this->getCapability('BINARY');
         $literal_plus = !$binary && $this->prefs['literal+'];
+        $len          = 0;
+        $msg          = is_array($message) ? $message : array(&$message);
+        $chunk_size   = 512000;
+
+        for ($i=0, $cnt=count($msg); $i<$cnt; $i++) {
+            if (is_resource($msg[$i])) {
+                $stat = fstat($msg[$i]);
+                if ($stat === false) {
+                    return false;
+                }
+                $len += $stat['size'];
+            }
+            else {
+                if (!$binary) {
+                    $msg[$i] = str_replace("\r", '', $msg[$i]);
+                    $msg[$i] = str_replace("\n", "\r\n", $msg[$i]);
+                }
 
-        if (!$binary) {
-            $message = str_replace("\r", '', $message);
-            $message = str_replace("\n", "\r\n", $message);
+                $len += strlen($msg[$i]);
+            }
         }
 
-        $len = strlen($message);
         if (!$len) {
             return false;
         }
@@ -2664,7 +2682,32 @@ class rcube_imap_generic
                 }
             }
 
-            if (!$this->putLine($message)) {
+            foreach ($msg as $msg_part) {
+                // file pointer
+                if (is_resource($msg_part)) {
+                    rewind($msg_part);
+                    while (!feof($msg_part) && $this->fp) {
+                        $buffer = fread($msg_part, $chunk_size);
+                        $this->putLine($buffer, false);
+                    }
+                    fclose($msg_part);
+                }
+                // string
+                else {
+                    $size = strlen($msg_part);
+
+                    // Break up the data by sending one chunk (up to 512k) at a time.
+                    // This approach reduces our peak memory usage
+                    for ($offset = 0; $offset < $size; $offset += $chunk_size) {
+                        $chunk = substr($msg_part, $offset, $chunk_size);
+                        if (!$this->putLine($chunk, false)) {
+                            return false;
+                        }
+                    }
+                }
+            }
+
+            if (!$this->putLine('')) { // \r\n
                 return false;
             }
 
@@ -2703,94 +2746,23 @@ class rcube_imap_generic
      */
     function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false)
     {
-        unset($this->data['APPENDUID']);
-
-        if ($mailbox === null || $mailbox === '') {
-            return false;
-        }
-
         // open message file
-        $in_fp = false;
         if (file_exists(realpath($path))) {
-            $in_fp = fopen($path, 'r');
+            $fp = fopen($path, 'r');
         }
 
-        if (!$in_fp) {
+        if (!$fp) {
             $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
             return false;
         }
 
-        $body_separator = "\r\n\r\n";
-        $len = filesize($path);
-
-        if (!$len) {
-            return false;
-        }
-
+        $message = array();
         if ($headers) {
-            $headers = preg_replace('/[\r\n]+$/', '', $headers);
-            $len += strlen($headers) + strlen($body_separator);
-        }
-
-        $binary       = $binary && $this->getCapability('BINARY');
-        $literal_plus = !$binary && $this->prefs['literal+'];
-
-        // build APPEND command
-        $key = $this->nextTag();
-        $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')';
-        if (!empty($date)) {
-            $request .= ' ' . $this->escape($date);
+            $message[] = trim($headers, "\r\n") . "\r\n\r\n";
         }
-        $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
+        $message[] = $fp;
 
-        // send APPEND command
-        if ($this->putLine($request)) {
-            // Don't wait when LITERAL+ is supported
-            if (!$literal_plus) {
-                $line = $this->readReply();
-
-                if ($line[0] != '+') {
-                    $this->parseResult($line, 'APPEND: ');
-                    return false;
-                }
-            }
-
-            // send headers with body separator
-            if ($headers) {
-                $this->putLine($headers . $body_separator, false);
-            }
-
-            // send file
-            while (!feof($in_fp) && $this->fp) {
-                $buffer = fgets($in_fp, 4096);
-                $this->putLine($buffer, false);
-            }
-            fclose($in_fp);
-
-            if (!$this->putLine('')) { // \r\n
-                return false;
-            }
-
-            // read response
-            do {
-                $line = $this->readLine();
-            } while (!$this->startsWith($line, $key, true, true));
-
-            // Clear internal status cache
-            unset($this->data['STATUS:'.$mailbox]);
-
-            if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
-                return false;
-            else if (!empty($this->data['APPENDUID']))
-                return $this->data['APPENDUID'];
-            else
-                return true;
-        }
-        else {
-            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
-        }
-
-        return false;
+        return $this->append($mailbox, $message, $flags, $date, $binary);
     }
 
     /**
@@ -3522,7 +3494,7 @@ class rcube_imap_generic
                     break 2;
                 }
 
-                // excluded chars: SP, CTL, ), [, ]
+                // excluded chars: SP, CTL, ), [, ], DEL
                 if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) {
                     $result[] = $m[1] == 'NIL' ? NULL : $m[1];
                     $str = substr($str, strlen($m[1]));
diff --git a/lib/ext/Roundcube/rcube_ldap.php b/lib/ext/Roundcube/rcube_ldap.php
index 39a48b4..522c81d 100644
--- a/lib/ext/Roundcube/rcube_ldap.php
+++ b/lib/ext/Roundcube/rcube_ldap.php
@@ -3,8 +3,8 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2006-2012, The Roundcube Dev Team                       |
- | Copyright (C) 2011-2012, Kolab Systems AG                             |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
+ | Copyright (C) 2011-2013, Kolab Systems AG                             |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -29,36 +29,33 @@ class rcube_ldap extends rcube_addressbook
 {
     /** public properties */
     public $primary_key = 'ID';
-    public $groups = false;
-    public $readonly = true;
-    public $ready = false;
-    public $group_id = 0;
-    public $coltypes = array();
+    public $groups      = false;
+    public $readonly    = true;
+    public $ready       = false;
+    public $group_id    = 0;
+    public $coltypes    = array();
 
     /** private properties */
-    protected $conn;
-    protected $prop = array();
+    protected $ldap;
+    protected $prop     = array();
     protected $fieldmap = array();
+    protected $filter   = '';
     protected $sub_filter;
-    protected $filter = '';
-    protected $result = null;
-    protected $ldap_result = null;
+    protected $result;
+    protected $ldap_result;
     protected $mail_domain = '';
     protected $debug = false;
 
-    private $base_dn = '';
+    private $base_dn        = '';
     private $groups_base_dn = '';
-    private $group_url = null;
+    private $group_url;
     private $cache;
 
-    private $vlv_active = false;
-    private $vlv_count = 0;
-
 
     /**
     * Object constructor
     *
-    * @param array 	 $p            LDAP connection properties
+    * @param array   $p            LDAP connection properties
     * @param boolean $debug        Enables debug mode
     * @param string  $mail_domain  Current user mail domain name
     */
@@ -66,8 +63,7 @@ class rcube_ldap extends rcube_addressbook
     {
         $this->prop = $p;
 
-        if (isset($p['searchonly']))
-            $this->searchonly = $p['searchonly'];
+        $fetch_attributes = array('objectClass');
 
         // check if groups are configured
         if (is_array($p['groups']) && count($p['groups'])) {
@@ -82,6 +78,21 @@ class rcube_ldap extends rcube_addressbook
                 $this->prop['groups']['name_attr'] = 'cn';
             if (empty($this->prop['groups']['scope']))
                 $this->prop['groups']['scope'] = 'sub';
+
+            // add group name attrib to the list of attributes to be fetched
+            $fetch_attributes[] = $this->prop['groups']['name_attr'];
+        }
+        if (is_array($p['group_filters']) && count($p['group_filters'])) {
+            $this->groups = true;
+
+            foreach ($p['group_filters'] as $k => $group_filter) {
+                // set default name attribute to cn
+                if (empty($group_filter['name_attr']) && empty($this->prop['groups']['name_attr']))
+                    $this->prop['group_filters'][$k]['name_attr'] = $group_filter['name_attr'] = 'cn';
+
+                if ($group_filter['name_attr'])
+                    $fetch_attributes[] = $group_filter['name_attr'];
+            }
         }
 
         // fieldmap property is given
@@ -185,12 +196,25 @@ class rcube_ldap extends rcube_addressbook
         $this->mail_domain = $mail_domain;
 
         // initialize cache
-        $rcube      = rcube::get_instance();
-        $cache_type = $rcube->config->get('ldap_cache', 'db');
-        $cache_ttl  = $rcube->config->get('ldap_cache_ttl', '10m');
-        $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
+        $rcube = rcube::get_instance();
+        if ($cache_type = $rcube->config->get('ldap_cache', 'db')) {
+            $cache_ttl  = $rcube->config->get('ldap_cache_ttl', '10m');
+            $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
+
+            $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
+        }
 
-        $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
+        // determine which attributes to fetch
+        $this->prop['list_attributes'] = array_unique($fetch_attributes);
+        $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes);
+        foreach ($rcube->config->get('contactlist_fields') as $col) {
+            $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col));
+        }
+
+        // initialize ldap wrapper object
+        $this->ldap = new rcube_ldap_generic($this->prop);
+        $this->ldap->set_cache($this->cache);
+        $this->ldap->set_debug($this->debug);
 
         $this->_connect();
     }
@@ -203,49 +227,18 @@ class rcube_ldap extends rcube_addressbook
     {
         $rcube = rcube::get_instance();
 
-        if (!function_exists('ldap_connect'))
-            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
-                'file' => __FILE__, 'line' => __LINE__,
-                'message' => "No ldap support in this installation of PHP"),
-                true, true);
-
-        if (is_resource($this->conn))
+        if ($this->ready)
             return true;
 
         if (!is_array($this->prop['hosts']))
             $this->prop['hosts'] = array($this->prop['hosts']);
 
-        if (empty($this->prop['ldap_version']))
-            $this->prop['ldap_version'] = 3;
-
         // try to connect + bind for every host configured
         // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
         // see http://www.php.net/manual/en/function.ldap-connect.php
         foreach ($this->prop['hosts'] as $host) {
-            $host     = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
-            $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
-
-            $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
-
-            if ($lc = @ldap_connect($host, $this->prop['port'])) {
-                if ($this->prop['use_tls'] === true)
-                    if (!ldap_start_tls($lc))
-                        continue;
-
-                $this->_debug("S: OK");
-
-                ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
-                $this->prop['host'] = $host;
-                $this->conn = $lc;
-
-                if (!empty($this->prop['network_timeout']))
-                  ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->prop['network_timeout']);
-
-                if (isset($this->prop['referrals']))
-                    ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
-            }
-            else {
-                $this->_debug("S: NOT OK");
+            // skip host if connection failed
+            if (!$this->ldap->connect($host)) {
                 continue;
             }
 
@@ -260,7 +253,7 @@ class rcube_ldap extends rcube_addressbook
 
             $this->base_dn        = $this->prop['base_dn'];
             $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
-            $this->prop['groups']['base_dn'] : $this->base_dn;
+                $this->prop['groups']['base_dn'] : $this->base_dn;
 
             // User specific access, generate the proper values to use.
             if ($this->prop['user_specific']) {
@@ -279,29 +272,46 @@ class rcube_ldap extends rcube_addressbook
 
                 $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
 
+                // Search for the dn to use to authenticate
                 if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
-                    if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
-                        $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
-                    }
-
-                    // Search for the dn to use to authenticate
-                    $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
-                    $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
+                    $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);
 
-                    $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
+                    $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:"
+                        .$this->prop['search_bind_pw']);
 
-                    $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
-                    if ($res) {
-                        if (($entry = ldap_first_entry($this->conn, $res))
-                            && ($bind_dn = ldap_get_dn($this->conn, $entry))
-                        ) {
-                            $this->_debug("S: search returned dn: $bind_dn");
-                            $dn = ldap_explode_dn($bind_dn, 1);
-                            $replaces['%dn'] = $dn[0];
-                        }
+                    if ($this->cache && ($dn = $this->cache->get($cache_key))) {
+                        $replaces['%dn'] = $dn;
                     }
                     else {
-                        $this->_debug("S: ".ldap_error($this->conn));
+                        $ldap = $this->ldap;
+                        if (!empty($search_bind_dn) && !empty($this->prop['search_bind_pw'])) {
+                            // To protect from "Critical extension is unavailable" error
+                            // we need to use a separate LDAP connection
+                            if (!empty($this->prop['vlv'])) {
+                                $ldap = new rcube_ldap_generic($this->prop);
+                                $ldap->set_debug($this->debug);
+                                $ldap->set_cache($this->cache);
+                                if (!$ldap->connect($host)) {
+                                    continue;
+                                }
+                            }
+
+                            if (!$ldap->bind($search_bind_dn, $this->prop['search_bind_pw'])) {
+                                continue;  // bind failed, try next host
+                            }
+                        }
+
+                        $res = $ldap->search($search_base_dn, $search_filter, 'sub', array('uid'));
+                        if ($res) {
+                            $res->rewind();
+                            $replaces['%dn'] = $res->get_dn();
+                        }
+
+                        if ($ldap != $this->ldap) {
+                            $ldap->close();
+                        }
                     }
 
                     // DN not found
@@ -313,9 +323,13 @@ class rcube_ldap extends rcube_addressbook
                                 'code' => 100, 'type' => 'ldap',
                                 'file' => __FILE__, 'line' => __LINE__,
                                 'message' => "DN not found using LDAP search."), true);
-                            return false;
+                            continue;
                         }
                     }
+
+                    if ($this->cache && !empty($replaces['%dn'])) {
+                        $this->cache->set($cache_key, $replaces['%dn']);
+                    }
                 }
 
                 // Replace the bind_dn and base_dn variables.
@@ -333,13 +347,13 @@ class rcube_ldap extends rcube_addressbook
             }
             else {
                 if (!empty($bind_dn)) {
-                    $this->ready = $this->bind($bind_dn, $bind_pass);
+                    $this->ready = $this->ldap->bind($bind_dn, $bind_pass);
                 }
                 else if (!empty($this->prop['auth_cid'])) {
-                    $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
+                    $this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
                 }
                 else {
-                    $this->ready = $this->sasl_bind($bind_user, $bind_pass);
+                    $this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
                 }
             }
 
@@ -350,10 +364,10 @@ class rcube_ldap extends rcube_addressbook
 
         }  // end foreach hosts
 
-        if (!is_resource($this->conn)) {
+        if (!is_resource($this->ldap->conn)) {
             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
                 'file' => __FILE__, 'line' => __LINE__,
-                'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
+                'message' => "Could not connect to any LDAP server, last tried $host"), true);
 
             return false;
         }
@@ -363,112 +377,47 @@ class rcube_ldap extends rcube_addressbook
 
 
     /**
-     * Bind connection with (SASL-) user and password
-     *
-     * @param string $authc Authentication user
-     * @param string $pass  Bind password
-     * @param string $authz Autorization user
-     *
-     * @return boolean True on success, False on error
+     * Close connection to LDAP server
      */
-    public function sasl_bind($authc, $pass, $authz=null)
+    function close()
     {
-        if (!$this->conn) {
-            return false;
-        }
-
-        if (!function_exists('ldap_sasl_bind')) {
-            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
-                'file' => __FILE__, 'line' => __LINE__,
-                'message' => "Unable to bind: ldap_sasl_bind() not exists"),
-                true, true);
-        }
-
-        if (!empty($authz)) {
-            $authz = 'u:' . $authz;
+        if ($this->ldap) {
+            $this->ldap->close();
         }
-
-        if (!empty($this->prop['auth_method'])) {
-            $method = $this->prop['auth_method'];
-        }
-        else {
-            $method = 'DIGEST-MD5';
-        }
-
-        $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
-
-        if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
-            $this->_debug("S: OK");
-            return true;
-        }
-
-        $this->_debug("S: ".ldap_error($this->conn));
-
-        rcube::raise_error(array(
-            'code' => ldap_errno($this->conn), 'type' => 'ldap',
-            'file' => __FILE__, 'line' => __LINE__,
-            'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
-            true);
-
-        return false;
     }
 
 
     /**
-     * Bind connection with DN and password
-     *
-     * @param string Bind DN
-     * @param string Bind password
+     * Returns address book name
      *
-     * @return boolean True on success, False on error
+     * @return string Address book name
      */
-    public function bind($dn, $pass)
+    function get_name()
     {
-        if (!$this->conn) {
-            return false;
-        }
-
-        $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
-
-        if (@ldap_bind($this->conn, $dn, $pass)) {
-            $this->_debug("S: OK");
-            return true;
-        }
-
-        $this->_debug("S: ".ldap_error($this->conn));
-
-        rcube::raise_error(array(
-            'code' => ldap_errno($this->conn), 'type' => 'ldap',
-            'file' => __FILE__, 'line' => __LINE__,
-            'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
-            true);
-
-        return false;
+        return $this->prop['name'];
     }
 
 
     /**
-     * Close connection to LDAP server
+     * Set internal list page
+     *
+     * @param  number  Page number to list
      */
-    function close()
+    function set_page($page)
     {
-        if ($this->conn)
-        {
-            $this->_debug("C: Close");
-            ldap_unbind($this->conn);
-            $this->conn = null;
-        }
+        $this->list_page = (int)$page;
+        $this->ldap->set_vlv_page($this->list_page, $this->page_size);
     }
 
-
     /**
-     * Returns address book name
+     * Set internal page size
      *
-     * @return string Address book name
+     * @param  number  Number of records to display on one page
      */
-    function get_name()
+    function set_pagesize($size)
     {
-        return $this->prop['name'];
+        $this->page_size = (int)$size;
+        $this->ldap->set_vlv_page($this->list_page, $this->page_size);
     }
 
 
@@ -528,16 +477,14 @@ class rcube_ldap extends rcube_addressbook
      */
     function list_records($cols=null, $subset=0)
     {
-        if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
-        {
+        if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) {
             $this->result = new rcube_result_set(0);
             $this->result->searchonly = true;
             return $this->result;
         }
 
         // fetch group members recursively
-        if ($this->group_id && $this->group_data['dn'])
-        {
+        if ($this->group_id && $this->group_data['dn']) {
             $entries = $this->list_group_members($this->group_data['dn']);
 
             // make list of entries unique and sort it
@@ -551,34 +498,34 @@ class rcube_ldap extends rcube_addressbook
             $entries['count'] = count($entries);
             $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
         }
-        else
-        {
-            // add general filter to query
-            if (!empty($this->prop['filter']) && empty($this->filter))
-                $this->set_search_set($this->prop['filter']);
+        else {
+            $prop = $this->group_id ? $this->group_data : $this->prop;
+
+            // use global search filter
+            if (!empty($this->filter))
+                $prop['filter'] = $this->filter;
 
             // exec LDAP search if no result resource is stored
-            if ($this->conn && !$this->ldap_result)
-                $this->_exec_search();
+            if ($this->ready && !$this->ldap_result)
+                $this->ldap_result = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);
 
             // count contacts for this user
             $this->result = $this->count();
 
             // we have a search result resource
-            if ($this->ldap_result && $this->result->count > 0)
-            {
+            if ($this->ldap_result && $this->result->count > 0) {
                 // sorting still on the ldap server
-                if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
-                    ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
+                if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active)
+                    $this->ldap_result->sort($this->sort_col);
 
                 // get all entries from the ldap server
-                $entries = ldap_get_entries($this->conn, $this->ldap_result);
+                $entries = $this->ldap_result->entries();
             }
 
         }  // end else
 
         // start and end of the page
-        $start_row = $this->vlv_active ? 0 : $this->result->first;
+        $start_row = $this->ldap->vlv_active ? 0 : $this->result->first;
         $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
         $last_row = $this->result->first + $this->page_size;
         $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
@@ -603,43 +550,34 @@ class rcube_ldap extends rcube_addressbook
 
         // fetch group object
         if (empty($entries)) {
-            $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
-            if ($result === false)
-            {
-                $this->_debug("S: ".ldap_error($this->conn));
+            $attribs = array('dn','objectClass','member','uniqueMember','memberURL');
+            $entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs);
+            if ($entries === false) {
                 return $group_members;
             }
-
-            $entries = @ldap_get_entries($this->conn, $result);
         }
 
-        for ($i=0; $i < $entries['count']; $i++)
-        {
+        for ($i=0; $i < $entries['count']; $i++) {
             $entry = $entries[$i];
+            $attrs = array();
 
-            if (empty($entry['objectclass']))
-                continue;
+            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), ''))
+                    && ($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;
+                }
 
-            foreach ((array)$entry['objectclass'] as $objectclass)
-            {
-                switch (strtolower($objectclass)) {
-                    case "group":
-                    case "groupofnames":
-                    case "kolabgroupofnames":
-                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
-                        break;
-                    case "groupofuniquenames":
-                    case "kolabgroupofuniquenames":
-                        $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
-                        break;
-                    case "groupofurls":
-                        $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
-                        break;
+                if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) {
+                    break 2;
                 }
             }
-
-            if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
-              break;
         }
 
         return array_filter($group_members);
@@ -658,28 +596,24 @@ class rcube_ldap extends rcube_addressbook
         // Use the member attributes to return an array of member ldap objects
         // NOTE that the member attribute is supposed to contain a DN
         $group_members = array();
-        if (empty($entry[$attr]))
+        if (empty($entry[$attr])) {
             return $group_members;
+        }
 
         // read these attributes for all members
-        $attrib = $count ? array('dn') : array_values($this->fieldmap);
-        $attrib[] = 'objectClass';
+        $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
         $attrib[] = 'member';
         $attrib[] = 'uniqueMember';
         $attrib[] = 'memberURL';
 
-        for ($i=0; $i < $entry[$attr]['count']; $i++)
-        {
+        $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
+
+        for ($i=0; $i < $entry[$attr]['count']; $i++) {
             if (empty($entry[$attr][$i]))
                 continue;
 
-            $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
-                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
-
-            $members = @ldap_get_entries($this->conn, $result);
-            if ($members == false)
-            {
-                $this->_debug("S: ".ldap_error($this->conn));
+            $members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
+            if ($members == false) {
                 $members = array();
             }
 
@@ -705,34 +639,22 @@ class rcube_ldap extends rcube_addressbook
     {
         $group_members = array();
 
-        for ($i=0; $i < $entry['memberurl']['count']; $i++)
-        {
+        for ($i=0; $i < $entry['memberurl']['count']; $i++) {
             // extract components from url
             if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
                 continue;
 
             // add search filter if any
             $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
-            $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
-
-            $attrib = $count ? array('dn') : array_values($this->fieldmap);
-            if ($result = @$func($this->conn, $m[1], $filter,
-                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
-            ) {
-                $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
-            }
-            else {
-                $this->_debug("S: ".ldap_error($this->conn));
-                return $group_members;
-            }
-
-            $entries = @ldap_get_entries($this->conn, $result);
-            for ($j = 0; $j < $entries['count']; $j++)
-            {
-                if ($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];
+            $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
+            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)))
+                        $group_members = array_merge($group_members, $nested_group_members);
+                    else
+                        $group_members[] = $entries[$j];
+                }
             }
         }
 
@@ -768,14 +690,11 @@ class rcube_ldap extends rcube_addressbook
         $mode = intval($mode);
 
         // special treatment for ID-based search
-        if ($fields == 'ID' || $fields == $this->primary_key)
-        {
+        if ($fields == 'ID' || $fields == $this->primary_key) {
             $ids = !is_array($value) ? explode(',', $value) : $value;
             $result = new rcube_result_set();
-            foreach ($ids as $id)
-            {
-                if ($rec = $this->get_record($id, true))
-                {
+            foreach ($ids as $id) {
+                if ($rec = $this->get_record($id, true)) {
                     $result->add($rec);
                     $result->count++;
                 }
@@ -787,34 +706,20 @@ class rcube_ldap extends rcube_addressbook
         $rcube = rcube::get_instance();
         $list_fields = $rcube->config->get('contactlist_fields');
 
-        if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $list_fields))
-        {
-            // add general filter to query
-            if (!empty($this->prop['filter']) && empty($this->filter))
-                $this->set_search_set($this->prop['filter']);
-
-            // set VLV controls with encoded search string
-            $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value);
-
-            $function = $this->_scope2func($this->prop['scope']);
-            $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
-                array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
-
+        if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
             $this->result = new rcube_result_set(0);
 
-            if (!$this->ldap_result) {
-                $this->_debug("S: ".ldap_error($this->conn));
+            $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : '';
+            $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
+                array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */));
+            if ($ldap_data === false) {
                 return $this->result;
             }
 
-            $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
-
             // get all entries of this page and post-filter those that really match the query
-            $search  = mb_strtolower($value);
-            $entries = ldap_get_entries($this->conn, $this->ldap_result);
-
-            for ($i = 0; $i < $entries['count']; $i++) {
-                $rec = $this->_ldap2result($entries[$i]);
+            $search = mb_strtolower($value);
+            foreach ($ldap_data as $i => $entry) {
+                $rec = $this->_ldap2result($entry);
                 foreach ($fields as $f) {
                     foreach ((array)$rec[$f] as $val) {
                         if ($this->compare_search_value($f, $val, $search, $mode)) {
@@ -840,31 +745,27 @@ class rcube_ldap extends rcube_addressbook
             }
         }
 
-        if ($fields == '*')
-        {
+        if ($fields == '*') {
             // search_fields are required for fulltext search
-            if (empty($this->prop['search_fields']))
-            {
+            if (empty($this->prop['search_fields'])) {
                 $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
                 $this->result = new rcube_result_set();
                 return $this->result;
             }
-            if (is_array($this->prop['search_fields']))
-            {
+            if (is_array($this->prop['search_fields'])) {
                 foreach ($this->prop['search_fields'] as $field) {
-                    $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
+                    $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)";
                 }
             }
         }
-        else
-        {
+        else {
             foreach ((array)$fields as $idx => $field) {
                 $val = is_array($value) ? $value[$idx] : $value;
                 if ($attrs = $this->_map_field($field)) {
                     if (count($attrs) > 1)
                         $filter .= '(|';
                     foreach ($attrs as $f)
-                        $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
+                        $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
                     if (count($attrs) > 1)
                         $filter .= ')';
                 }
@@ -875,6 +776,8 @@ class rcube_ldap extends rcube_addressbook
         // add required (non empty) fields filter
         $req_filter = '';
         foreach ((array)$required as $field) {
+            if (in_array($field, (array)$fields))  // required field is already in search filter
+                continue;
             if ($attrs = $this->_map_field($field)) {
                 if (count($attrs) > 1)
                     $req_filter .= '(|';
@@ -897,7 +800,6 @@ class rcube_ldap extends rcube_addressbook
 
         // set filter string and execute search
         $this->set_search_set($filter);
-        $this->_exec_search();
 
         if ($select)
             $this->list_records();
@@ -916,20 +818,21 @@ class rcube_ldap extends rcube_addressbook
     function count()
     {
         $count = 0;
-        if ($this->conn && $this->ldap_result) {
-            $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
+        if ($this->ldap_result) {
+            $count = $this->ldap_result->count();
         }
         else if ($this->group_id && $this->group_data['dn']) {
             $count = count($this->list_group_members($this->group_data['dn'], true));
         }
-        else if ($this->conn) {
-            // We have a connection but no result set, attempt to get one.
-            if (empty($this->filter)) {
-                // The filter is not set, set it.
-                $this->filter = $this->prop['filter'];
+        // We have a connection but no result set, attempt to get one.
+        else if ($this->ready) {
+            $prop = $this->group_id ? $this->group_data : $this->prop;
+
+            if (!empty($this->filter)) {  // Use global search filter
+                $prop['filter'] = $this->filter;
             }
 
-            $count = (int) $this->_exec_search(true);
+            $count = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], array('dn'), $prop, true);
         }
 
         return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
@@ -959,28 +862,16 @@ class rcube_ldap extends rcube_addressbook
     {
         $res = $this->result = null;
 
-        if ($this->conn && $dn)
-        {
+        if ($this->ready && $dn) {
             $dn = self::dn_decode($dn);
 
-            $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
-
-            if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) {
-                $this->_debug("S: OK");
-
-                $entry = ldap_first_entry($this->conn, $ldap_result);
-
-                if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) {
-                    $rec = array_change_key_case($rec, CASE_LOWER);
-                }
-            }
-            else {
-                $this->_debug("S: ".ldap_error($this->conn));
+            if ($rec = $this->ldap->get_entry($dn)) {
+                $rec = array_change_key_case($rec, CASE_LOWER);
             }
 
             // Use ldap_list to get subentries like country (c) attribute (#1488123)
             if (!empty($rec) && $this->sub_filter) {
-                if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
+                if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
                     foreach ($entries as $entry) {
                         $lrec = array_change_key_case($entry, CASE_LOWER);
                         $rec  = array_merge($lrec, $rec);
@@ -992,7 +883,7 @@ class rcube_ldap extends rcube_addressbook
                 // Add in the dn for the entry.
                 $rec['dn'] = $dn;
                 $res = $this->_ldap2result($rec);
-                $this->result = new rcube_result_set();
+                $this->result = new rcube_result_set(1);
                 $this->result->add($res);
             }
         }
@@ -1105,7 +996,7 @@ class rcube_ldap extends rcube_addressbook
         }
 
         // Build the new entries DN.
-        $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
+        $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
 
         // Remove attributes that need to be added separately (child objects)
         $xfields = array();
@@ -1118,19 +1009,19 @@ class rcube_ldap extends rcube_addressbook
             }
         }
 
-        if (!$this->ldap_add($dn, $newentry)) {
+        if (!$this->ldap->add($dn, $newentry)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return false;
         }
 
         foreach ($xfields as $xidx => $xf) {
-            $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn;
+            $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
             $xf = array(
                 $xidx => $xf,
                 'objectClass' => (array) $this->prop['sub_fields'][$xidx],
             );
 
-            $this->ldap_add($xdn, $xf);
+            $this->ldap->add($xdn, $xf);
         }
 
         $dn = self::dn_encode($dn);
@@ -1236,7 +1127,7 @@ class rcube_ldap extends rcube_addressbook
         // Update the entry as required.
         if (!empty($deletedata)) {
             // Delete the fields.
-            if (!$this->ldap_mod_del($dn, $deletedata)) {
+            if (!$this->ldap->mod_del($dn, $deletedata)) {
                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
                 return false;
             }
@@ -1246,17 +1137,17 @@ class rcube_ldap extends rcube_addressbook
             // Handle RDN change
             if ($replacedata[$this->prop['LDAP_rdn']]) {
                 $newdn = $this->prop['LDAP_rdn'].'='
-                    .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
+                    .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
                     .','.$this->base_dn;
                 if ($dn != $newdn) {
                     $newrdn = $this->prop['LDAP_rdn'].'='
-                    .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
+                    .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
                     unset($replacedata[$this->prop['LDAP_rdn']]);
                 }
             }
             // Replace the fields.
             if (!empty($replacedata)) {
-                if (!$this->ldap_mod_replace($dn, $replacedata)) {
+                if (!$this->ldap->mod_replace($dn, $replacedata)) {
                     $this->set_error(self::ERROR_SAVING, 'errorsaving');
                     return false;
                 }
@@ -1272,8 +1163,8 @@ class rcube_ldap extends rcube_addressbook
         // remove sub-entries
         if (!empty($subdeldata)) {
             foreach ($subdeldata as $fld => $val) {
-                $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
-                if (!$this->ldap_delete($subdn)) {
+                $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
+                if (!$this->ldap->delete($subdn)) {
                     return false;
                 }
             }
@@ -1281,7 +1172,7 @@ class rcube_ldap extends rcube_addressbook
 
         if (!empty($newdata)) {
             // Add the fields.
-            if (!$this->ldap_mod_add($dn, $newdata)) {
+            if (!$this->ldap->mod_add($dn, $newdata)) {
                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
                 return false;
             }
@@ -1289,7 +1180,7 @@ class rcube_ldap extends rcube_addressbook
 
         // Handle RDN change
         if (!empty($newrdn)) {
-            if (!$this->ldap_rename($dn, $newrdn, null, true)) {
+            if (!$this->ldap->rename($dn, $newrdn, null, true)) {
                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
                 return false;
             }
@@ -1300,7 +1191,7 @@ class rcube_ldap extends rcube_addressbook
             // change the group membership of the contact
             if ($this->groups) {
                 $group_ids = $this->get_record_groups($dn);
-                foreach ($group_ids as $group_id)
+                foreach ($group_ids as $group_id => $group_prop)
                 {
                     $this->remove_from_group($group_id, $dn);
                     $this->add_to_group($group_id, $newdn);
@@ -1313,12 +1204,12 @@ class rcube_ldap extends rcube_addressbook
         // add sub-entries
         if (!empty($subnewdata)) {
             foreach ($subnewdata as $fld => $val) {
-                $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
+                $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
                 $xf = array(
                     $fld => $val,
                     'objectClass' => (array) $this->prop['sub_fields'][$fld],
                 );
-                $this->ldap_add($subdn, $xf);
+                $this->ldap->add($subdn, $xf);
             }
         }
 
@@ -1346,9 +1237,9 @@ class rcube_ldap extends rcube_addressbook
 
             // Need to delete all sub-entries first
             if ($this->sub_filter) {
-                if ($entries = $this->ldap_list($dn, $this->sub_filter)) {
+                if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
                     foreach ($entries as $entry) {
-                        if (!$this->ldap_delete($entry['dn'])) {
+                        if (!$this->ldap->delete($entry['dn'])) {
                             $this->set_error(self::ERROR_SAVING, 'errorsaving');
                             return false;
                         }
@@ -1357,7 +1248,7 @@ class rcube_ldap extends rcube_addressbook
             }
 
             // Delete the record.
-            if (!$this->ldap_delete($dn)) {
+            if (!$this->ldap->delete($dn)) {
                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
                 return false;
             }
@@ -1366,7 +1257,7 @@ class rcube_ldap extends rcube_addressbook
             if ($this->groups) {
                 $dn = self::dn_encode($dn);
                 $group_ids = $this->get_record_groups($dn);
-                foreach ($group_ids as $group_id) {
+                foreach ($group_ids as $group_id => $group_prop) {
                     $this->remove_from_group($group_id, $dn);
                 }
             }
@@ -1381,8 +1272,8 @@ class rcube_ldap extends rcube_addressbook
      */
     function delete_all()
     {
-        //searching for contact entries
-        $dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
+        // searching for contact entries
+        $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
 
         if (!empty($dn_list)) {
             foreach ($dn_list as $idx => $entry) {
@@ -1432,120 +1323,26 @@ class rcube_ldap extends rcube_addressbook
         }
     }
 
-    /**
-     * Execute the LDAP search based on the stored credentials
-     */
-    private function _exec_search($count = false)
-    {
-        if ($this->ready)
-        {
-            $filter = $this->filter ? $this->filter : '(objectclass=*)';
-            $function = $this->_scope2func($this->prop['scope'], $ns_function);
-
-            $this->_debug("C: Search [$filter][dn: $this->base_dn]");
-
-            // when using VLV, we get the total count by...
-            if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
-                // ...either reading numSubOrdinates attribute
-                if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
-                    $counts = ldap_get_entries($this->conn, $result_count);
-                    for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
-                        $this->vlv_count += $counts[$j]['numsubordinates'][0];
-                    $this->_debug("D: total numsubordinates = " . $this->vlv_count);
-                }
-                else if (!function_exists('ldap_parse_virtuallist_control'))  // ...or by fetching all records dn and count them
-                    $this->vlv_count = $this->_exec_search(true);
-
-                $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
-            }
-
-            // only fetch dn for count (should keep the payload low)
-            $attrs = $count ? array('dn') : array_values($this->fieldmap);
-            if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
-                $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
-            ) {
-                // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
-                if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
-                    if (ldap_parse_result($this->conn, $this->ldap_result,
-                        $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
-                        && $serverctrls // can be null e.g. in case of adm. limit error
-                    ) {
-                        ldap_parse_virtuallist_control($this->conn, $serverctrls,
-                            $last_offset, $this->vlv_count, $vresult);
-                        $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
-                    }
-                    else {
-                        $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
-                    }
-                }
-
-                $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
-                $this->_debug("S: $entries_count record(s)");
-
-                return $count ? $entries_count : true;
-            }
-            else {
-                $this->_debug("S: ".ldap_error($this->conn));
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Choose the right PHP function according to scope property
-     */
-    private function _scope2func($scope, &$ns_function = null)
-    {
-        switch ($scope) {
-          case 'sub':
-            $function = $ns_function  = 'ldap_search';
-            break;
-          case 'base':
-            $function = $ns_function = 'ldap_read';
-            break;
-          default:
-            $function = 'ldap_list';
-            $ns_function = 'ldap_read';
-            break;
-        }
-
-        return $function;
-    }
-
-    /**
-     * Set server controls for Virtual List View (paginated listing)
-     */
-    private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
-    {
-        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$prop['sort']));
-        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
-
-        $sort = (array)$prop['sort'];
-        $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
-            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
-
-        if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
-            return false;
-        }
-
-        return true;
-    }
-
 
     /**
      * Converts LDAP entry into an array
      */
     private function _ldap2result($rec)
     {
-        $out = array();
+        $out = array('_type' => 'person');
+        $fieldmap = $this->fieldmap;
 
         if ($rec['dn'])
             $out[$this->primary_key] = self::dn_encode($rec['dn']);
 
-        foreach ($this->fieldmap as $rf => $lf)
+        // determine record type
+        if (self::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'];
+        }
+
+        foreach ($fieldmap as $rf => $lf)
         {
             for ($i=0; $i < $rec[$lf]['count']; $i++) {
                 if (!($value = $rec[$lf][$i]))
@@ -1607,8 +1404,10 @@ class rcube_ldap extends rcube_addressbook
             if (is_array($colprop['serialized'])) {
                foreach ($colprop['serialized'] as $subtype => $delim) {
                   $key = $col.':'.$subtype;
-                  foreach ((array)$save_cols[$key] as $i => $val)
-                     $save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country']));
+                  foreach ((array)$save_cols[$key] as $i => $val) {
+                     $values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']);
+                     $save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null;
+                 }
                }
             }
         }
@@ -1659,6 +1458,17 @@ class rcube_ldap extends rcube_addressbook
         return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
     }
 
+    /**
+     * Determines whether the given LDAP entry is a group record
+     */
+    private static function is_group_entry($entry)
+    {
+        return array_intersect(
+            array('group', 'groupofnames', 'kolabgroupofnames', 'groupofuniquenames',
+                'kolabgroupofuniquenames', 'groupofurls', 'univentiongroup'),
+            array_map('strtolower', (array)$entry['objectclass'])
+        );
+    }
 
     /**
      * Prints debug info to the log
@@ -1675,55 +1485,27 @@ class rcube_ldap extends rcube_addressbook
      * Activate/deactivate debug mode
      *
      * @param boolean $dbg True if LDAP commands should be logged
-     * @access public
      */
     function set_debug($dbg = true)
     {
         $this->debug = $dbg;
-    }
-
-
-    /**
-     * Quotes attribute value string
-     *
-     * @param string $str Attribute value
-     * @param bool   $dn  True if the attribute is a DN
-     *
-     * @return string Quoted string
-     */
-    private static function _quote_string($str, $dn=false)
-    {
-        // take firt entry if array given
-        if (is_array($str))
-            $str = reset($str);
-
-        if ($dn)
-            $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
-                '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
-        else
-            $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
-                '/'=>'\2f');
 
-        return strtr($str, $replace);
+        if ($this->ldap) {
+            $this->ldap->set_debug($dbg);
+        }
     }
 
 
     /**
      * Setter for the current group
-     * (empty, has to be re-implemented by extending class)
      */
     function set_group($group_id)
     {
-        if ($group_id)
-        {
-            if (($group_cache = $this->cache->get('groups')) === null)
-                $group_cache = $this->_fetch_groups();
-
+        if ($group_id) {
             $this->group_id = $group_id;
-            $this->group_data = $group_cache[$group_id];
+            $this->group_data = $this->get_group_entry($group_id);
         }
-        else
-        {
+        else {
             $this->group_id = 0;
             $this->group_data = null;
         }
@@ -1742,15 +1524,13 @@ class rcube_ldap extends rcube_addressbook
      */
     function list_groups($search = null, $mode = 0)
     {
-        if (!$this->groups)
+        if (!$this->groups) {
             return array();
+        }
 
-        // use cached list for searching
-        $this->cache->expunge();
-        if (!$search || ($group_cache = $this->cache->get('groups')) === null)
-            $group_cache = $this->_fetch_groups();
+        $group_cache = $this->_fetch_groups();
+        $groups      = array();
 
-        $groups = array();
         if ($search) {
             foreach ($group_cache as $group) {
                 if ($this->compare_search_value('name', $group['name'], $search, $mode)) {
@@ -1758,8 +1538,9 @@ class rcube_ldap extends rcube_addressbook
                 }
             }
         }
-        else
+        else {
             $groups = $group_cache;
+        }
 
         return array_values($groups);
     }
@@ -1767,80 +1548,139 @@ class rcube_ldap extends rcube_addressbook
     /**
      * Fetch groups from server
      */
-    private function _fetch_groups($vlv_page = 0)
+    private function _fetch_groups($vlv_page = null)
     {
-        $base_dn = $this->groups_base_dn;
-        $filter = $this->prop['groups']['filter'];
-        $name_attr = $this->prop['groups']['name_attr'];
+        // special case: list groups from 'group_filters' config
+        if ($vlv_page === null && !empty($this->prop['group_filters'])) {
+            $groups = array();
+
+            // 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'];
+            }
+
+            foreach ($this->prop['group_filters'] as $id => $prop) {
+                $groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id), 'virtual' => true, 'base_dn' => $this->base_dn);
+            }
+
+            return $groups;
+        }
+
+        if ($this->cache && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) {
+            return $groups;
+        }
+
+        $base_dn    = $this->groups_base_dn;
+        $filter     = $this->prop['groups']['filter'];
+        $name_attr  = $this->prop['groups']['name_attr'];
         $email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
         $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
-        $sort_attr = $sort_attrs[0];
+        $sort_attr  = $sort_attrs[0];
 
-        $this->_debug("C: Search [$filter][dn: $base_dn]");
+        $ldap = $this->ldap;
 
         // use vlv to list groups
         if ($this->prop['groups']['vlv']) {
             $page_size = 200;
-            if (!$this->prop['groups']['sort'])
+            if (!$this->prop['groups']['sort']) {
                 $this->prop['groups']['sort'] = $sort_attrs;
-            $vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size);
+            }
+
+            $ldap = clone $this->ldap;
+            $ldap->set_config($this->prop['groups']);
+            $ldap->set_vlv_page($vlv_page+1, $page_size);
         }
 
-        $function = $this->_scope2func($this->prop['groups']['scope']);
-        $res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)));
-        if ($res === false)
-        {
-            $this->_debug("S: ".ldap_error($this->conn));
+        $attrs     = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
+        $ldap_data = $ldap->search($base_dn, $filter, $this->prop['groups']['scope'], $attrs, $this->prop['groups']);
+
+        if ($ldap_data === false) {
             return array();
         }
 
-        $ldap_data = ldap_get_entries($this->conn, $res);
-        $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
-
-        $groups = array();
+        $groups          = array();
         $group_sortnames = array();
-        $group_count = $ldap_data["count"];
-        for ($i=0; $i < $group_count; $i++)
-        {
-            $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
-            $group_id = self::dn_encode($group_name);
+        $group_count     = $ldap_data->count();
+
+        foreach ($ldap_data as $entry) {
+            if (!$entry['dn'])  // DN is mandatory
+                $entry['dn'] = $ldap_data->get_dn();
+
+            $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+            $group_id = self::dn_encode($entry['dn']);
             $groups[$group_id]['ID'] = $group_id;
-            $groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
+            $groups[$group_id]['dn'] = $entry['dn'];
             $groups[$group_id]['name'] = $group_name;
-            $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']);
+            $groups[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
 
             // list email attributes of a group
-            for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) {
-                if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0)
-                    $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
+            for ($j=0; $entry[$email_attr] && $j < $entry[$email_attr]['count']; $j++) {
+                if (strpos($entry[$email_attr][$j], '@') > 0)
+                    $groups[$group_id]['email'][] = $entry[$email_attr][$j];
             }
 
-            $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
+            $group_sortnames[] = mb_strtolower($entry[$sort_attr][0]);
         }
 
         // recursive call can exit here
-        if ($vlv_page > 0)
+        if ($vlv_page > 0) {
             return $groups;
+        }
 
         // call recursively until we have fetched all groups
-        while ($vlv_active && $group_count == $page_size)
-        {
-            $next_page = $this->_fetch_groups(++$vlv_page);
-            $groups = array_merge($groups, $next_page);
+        while ($this->prop['groups']['vlv'] && $group_count == $page_size) {
+            $next_page   = $this->_fetch_groups(++$vlv_page);
+            $groups      = array_merge($groups, $next_page);
             $group_count = count($next_page);
         }
 
         // when using VLV the list of groups is already sorted
-        if (!$this->prop['groups']['vlv'])
+        if (!$this->prop['groups']['vlv']) {
             array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
+        }
 
         // cache this
-        $this->cache->set('groups', $groups);
+        if ($this->cache) {
+            $this->cache->set('groups', $groups);
+        }
 
         return $groups;
     }
 
     /**
+     * Fetch a group entry from LDAP and save in local cache
+     */
+    private function get_group_entry($group_id)
+    {
+        $group_cache = $this->_fetch_groups();
+
+        // add group record to cache if it isn't yet there
+        if (!isset($group_cache[$group_id])) {
+            $name_attr = $this->prop['groups']['name_attr'];
+            $dn = self::dn_decode($group_id);
+
+            if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) {
+                $entry = $list[0];
+                $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+                $group_cache[$group_id]['ID'] = $group_id;
+                $group_cache[$group_id]['dn'] = $dn;
+                $group_cache[$group_id]['name'] = $group_name;
+                $group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
+            }
+            else {
+                $group_cache[$group_id] = false;
+            }
+
+            if ($this->cache) {
+                $this->cache->set('groups', $group_cache);
+            }
+        }
+
+        return $group_cache[$group_id];
+    }
+
+    /**
      * Get group properties such as name and email address(es)
      *
      * @param string Group identifier
@@ -1848,10 +1688,7 @@ class rcube_ldap extends rcube_addressbook
      */
     function get_group($group_id)
     {
-        if (($group_cache = $this->cache->get('groups')) === null)
-            $group_cache = $this->_fetch_groups();
-
-        $group_data = $group_cache[$group_id];
+        $group_data = $this->get_group_entry($group_id);
         unset($group_data['dn'], $group_data['member_attr']);
 
         return $group_data;
@@ -1865,24 +1702,24 @@ class rcube_ldap extends rcube_addressbook
      */
     function create_group($group_name)
     {
-        $base_dn = $this->groups_base_dn;
-        $new_dn = "cn=$group_name,$base_dn";
-        $new_gid = self::dn_encode($group_name);
+        $new_dn      = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn;
+        $new_gid     = self::dn_encode($new_dn);
         $member_attr = $this->get_group_member_attr();
-        $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
-
-        $new_entry = array(
+        $name_attr   = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
+        $new_entry   = array(
             'objectClass' => $this->prop['groups']['object_classes'],
             $name_attr => $group_name,
             $member_attr => '',
         );
 
-        if (!$this->ldap_add($new_dn, $new_entry)) {
+        if (!$this->ldap->add($new_dn, $new_entry)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return false;
         }
 
-        $this->cache->remove('groups');
+        if ($this->cache) {
+            $this->cache->remove('groups');
+        }
 
         return array('id' => $new_gid, 'name' => $group_name);
     }
@@ -1895,19 +1732,18 @@ class rcube_ldap extends rcube_addressbook
      */
     function delete_group($group_id)
     {
-        if (($group_cache = $this->cache->get('groups')) === null)
-            $group_cache = $this->_fetch_groups();
-
-        $base_dn = $this->groups_base_dn;
-        $group_name = $group_cache[$group_id]['name'];
-        $del_dn = "cn=$group_name,$base_dn";
+        $group_cache = $this->_fetch_groups();
+        $del_dn      = $group_cache[$group_id]['dn'];
 
-        if (!$this->ldap_delete($del_dn)) {
+        if (!$this->ldap->delete($del_dn)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return false;
         }
 
-        $this->cache->remove('groups');
+        if ($this->cache) {
+            unset($group_cache[$group_id]);
+            $this->cache->set('groups', $group_cache);
+        }
 
         return true;
     }
@@ -1922,21 +1758,19 @@ class rcube_ldap extends rcube_addressbook
      */
     function rename_group($group_id, $new_name, &$new_gid)
     {
-        if (($group_cache = $this->cache->get('groups')) === null)
-            $group_cache = $this->_fetch_groups();
-
-        $base_dn = $this->groups_base_dn;
-        $group_name = $group_cache[$group_id]['name'];
-        $old_dn = "cn=$group_name,$base_dn";
-        $new_rdn = "cn=$new_name";
-        $new_gid = self::dn_encode($new_name);
+        $group_cache = $this->_fetch_groups();
+        $old_dn      = $group_cache[$group_id]['dn'];
+        $new_rdn     = "cn=" . rcube_ldap_generic::quote_string($new_name, true);
+        $new_gid     = self::dn_encode($new_rdn . ',' . $this->groups_base_dn);
 
-        if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) {
+        if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return false;
         }
 
-        $this->cache->remove('groups');
+        if ($this->cache) {
+            $this->cache->remove('groups');
+        }
 
         return $new_name;
     }
@@ -1951,27 +1785,27 @@ class rcube_ldap extends rcube_addressbook
      */
     function add_to_group($group_id, $contact_ids)
     {
-        if (($group_cache = $this->cache->get('groups')) === null)
-            $group_cache = $this->_fetch_groups();
-
-        if (!is_array($contact_ids))
-            $contact_ids = explode(',', $contact_ids);
-
-        $base_dn     = $this->groups_base_dn;
-        $group_name  = $group_cache[$group_id]['name'];
+        $group_cache = $this->_fetch_groups();
         $member_attr = $group_cache[$group_id]['member_attr'];
-        $group_dn    = "cn=$group_name,$base_dn";
+        $group_dn    = $group_cache[$group_id]['dn'];
         $new_attrs   = array();
 
-        foreach ($contact_ids as $id)
+        if (!is_array($contact_ids)) {
+            $contact_ids = explode(',', $contact_ids);
+        }
+
+        foreach ($contact_ids as $id) {
             $new_attrs[$member_attr][] = self::dn_decode($id);
+        }
 
-        if (!$this->ldap_mod_add($group_dn, $new_attrs)) {
+        if (!$this->ldap->mod_add($group_dn, $new_attrs)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return 0;
         }
 
-        $this->cache->remove('groups');
+        if ($this->cache) {
+            $this->cache->remove('groups');
+        }
 
         return count($new_attrs[$member_attr]);
     }
@@ -1986,27 +1820,27 @@ class rcube_ldap extends rcube_addressbook
      */
     function remove_from_group($group_id, $contact_ids)
     {
-        if (($group_cache = $this->cache->get('groups')) === null)
-            $group_cache = $this->_fetch_groups();
-
-        if (!is_array($contact_ids))
-            $contact_ids = explode(',', $contact_ids);
-
-        $base_dn     = $this->groups_base_dn;
-        $group_name  = $group_cache[$group_id]['name'];
+        $group_cache = $this->_fetch_groups();
         $member_attr = $group_cache[$group_id]['member_attr'];
-        $group_dn    = "cn=$group_name,$base_dn";
+        $group_dn    = $group_cache[$group_id]['dn'];
         $del_attrs   = array();
 
-        foreach ($contact_ids as $id)
+        if (!is_array($contact_ids)) {
+            $contact_ids = explode(',', $contact_ids);
+        }
+
+        foreach ($contact_ids as $id) {
             $del_attrs[$member_attr][] = self::dn_decode($id);
+        }
 
-        if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
+        if (!$this->ldap->mod_del($group_dn, $del_attrs)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return 0;
         }
 
-        $this->cache->remove('groups');
+        if ($this->cache) {
+            $this->cache->remove('groups');
+        }
 
         return count($del_attrs[$member_attr]);
     }
@@ -2021,8 +1855,9 @@ class rcube_ldap extends rcube_addressbook
      */
     function get_record_groups($contact_id)
     {
-        if (!$this->groups)
+        if (!$this->groups) {
             return array();
+        }
 
         $base_dn     = $this->groups_base_dn;
         $contact_dn  = self::dn_decode($contact_id);
@@ -2033,23 +1868,18 @@ class rcube_ldap extends rcube_addressbook
             $add_filter = "($member_attr=$contact_dn)";
         $filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
 
-        $this->_debug("C: Search [$filter][dn: $base_dn]");
-
-        $res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr));
-        if ($res === false)
-        {
-            $this->_debug("S: ".ldap_error($this->conn));
+        $ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr));
+        if ($res === false) {
             return array();
         }
-        $ldap_data = ldap_get_entries($this->conn, $res);
-        $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
 
         $groups = array();
-        for ($i=0; $i<$ldap_data["count"]; $i++)
-        {
-            $group_name = $ldap_data[$i][$name_attr][0];
-            $group_id = self::dn_encode($group_name);
-            $groups[$group_id] = $group_id;
+        foreach ($ldap_data as $entry) {
+            if (!$entry['dn'])
+                $entry['dn'] = $ldap_data->get_dn();
+            $group_name = $entry[$name_attr][0];
+            $group_id = self::dn_encode($entry['dn']);
+            $groups[$group_id] = array('ID' => $group_id, 'name' => $group_name, 'dn' => $entry['dn']);
         }
         return $groups;
     }
@@ -2057,170 +1887,37 @@ class rcube_ldap extends rcube_addressbook
     /**
      * Detects group member attribute name
      */
-    private function get_group_member_attr($object_classes = array())
+    private function get_group_member_attr($object_classes = array(), $default = 'member')
     {
         if (empty($object_classes)) {
             $object_classes = $this->prop['groups']['object_classes'];
         }
+
         if (!empty($object_classes)) {
             foreach ((array)$object_classes as $oc) {
                 switch (strtolower($oc)) {
                     case 'group':
                     case 'groupofnames':
                     case 'kolabgroupofnames':
-                        $member_attr = 'member';
-                        break;
+                        return 'member';
 
                     case 'groupofuniquenames':
                     case 'kolabgroupofuniquenames':
-                        $member_attr = 'uniqueMember';
-                        break;
+                    case 'univentiongroup':
+                        return 'uniqueMember';
                 }
             }
         }
 
-        if (!empty($member_attr)) {
-            return $member_attr;
-        }
-
         if (!empty($this->prop['groups']['member_attr'])) {
             return $this->prop['groups']['member_attr'];
         }
 
-        return 'member';
+        return $default;
     }
 
 
     /**
-     * Generate BER encoded string for Virtual List View option
-     *
-     * @param integer List offset (first record)
-     * @param integer Records per page
-     * @return string BER encoded option value
-     */
-    private function _vlv_ber_encode($offset, $rpp, $search = '')
-    {
-        # this string is ber-encoded, php will prefix this value with:
-        # 04 (octet string) and 10 (length of 16 bytes)
-        # the code behind this string is broken down as follows:
-        # 30 = ber sequence with a length of 0e (14) bytes following
-        # 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
-        # 02 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
-        # a0 = type context-specific/constructed with a length of 06 (6) bytes following
-        # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
-        # 02 = type integer with 2 bytes following (contentCount):  01 00
-
-        # whith a search string present:
-        # 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
-        # 81 indicates a user string is present where as a a0 indicates just a offset search
-        # 81 = type context-specific/constructed with a length of 06 (6) bytes following
-
-        # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
-        # encoding of integer values (note: these values are in
-        # two-complement form so since offset will never be negative bit 8 of the
-        # leftmost octet should never by set to 1):
-        # 8.3.2: If the contents octets of an integer value encoding consist
-        # of more than one octet, then the bits of the first octet (rightmost) and bit 8
-        # of the second (to the left of first octet) octet:
-        # a) shall not all be ones; and
-        # b) shall not all be zero
-
-        if ($search)
-        {
-            $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
-            $ber_val = self::_string2hex($search);
-            $str = self::_ber_addseq($ber_val, '81');
-        }
-        else
-        {
-            # construct the string from right to left
-            $str = "020100"; # contentCount
-
-            $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
-
-            // calculate octet length of $ber_val
-            $str = self::_ber_addseq($ber_val, '02') . $str;
-
-            // now compute length over $str
-            $str = self::_ber_addseq($str, 'a0');
-        }
-
-        // now tack on records per page
-        $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
-
-        // now tack on sequence identifier and length
-        $str = self::_ber_addseq($str, '30');
-
-        return pack('H'.strlen($str), $str);
-    }
-
-
-    /**
-     * create ber encoding for sort control
-     *
-     * @param array List of cols to sort by
-     * @return string BER encoded option value
-     */
-    private function _sort_ber_encode($sortcols)
-    {
-        $str = '';
-        foreach (array_reverse((array)$sortcols) as $col) {
-            $ber_val = self::_string2hex($col);
-
-            # 30 = ber sequence with a length of octet value
-            # 04 = octet string with a length of the ascii value
-            $oct = self::_ber_addseq($ber_val, '04');
-            $str = self::_ber_addseq($oct, '30') . $str;
-        }
-
-        // now tack on sequence identifier and length
-        $str = self::_ber_addseq($str, '30');
-
-        return pack('H'.strlen($str), $str);
-    }
-
-    /**
-     * Add BER sequence with correct length and the given identifier
-     */
-    private static function _ber_addseq($str, $identifier)
-    {
-        $len = dechex(strlen($str)/2);
-        if (strlen($len) % 2 != 0)
-            $len = '0'.$len;
-
-        return $identifier . $len . $str;
-    }
-
-    /**
-     * Returns BER encoded integer value in hex format
-     */
-    private static function _ber_encode_int($offset)
-    {
-        $val = dechex($offset);
-        $prefix = '';
-
-        // check if bit 8 of high byte is 1
-        if (preg_match('/^[89abcdef]/', $val))
-            $prefix = '00';
-
-        if (strlen($val)%2 != 0)
-            $prefix .= '0';
-
-        return $prefix . $val;
-    }
-
-    /**
-     * Returns ascii string encoded in hex
-     */
-    private static function _string2hex($str)
-    {
-        $hex = '';
-        for ($i=0; $i < strlen($str); $i++)
-            $hex .= dechex(ord($str[$i]));
-        return $hex;
-    }
-
-    /**
      * HTML-safe DN string encoding
      *
      * @param string $str DN string
@@ -2247,130 +1944,4 @@ class rcube_ldap extends rcube_addressbook
         return base64_decode($str);
     }
 
-    /**
-     * Wrapper for ldap_add()
-     */
-    protected function ldap_add($dn, $entry)
-    {
-        $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
-
-        $res = ldap_add($this->conn, $dn, $entry);
-        if ($res === false) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            return false;
-        }
-
-        $this->_debug("S: OK");
-        return true;
-    }
-
-    /**
-     * Wrapper for ldap_delete()
-     */
-    protected function ldap_delete($dn)
-    {
-        $this->_debug("C: Delete [dn: $dn]");
-
-        $res = ldap_delete($this->conn, $dn);
-        if ($res === false) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            return false;
-        }
-
-        $this->_debug("S: OK");
-        return true;
-    }
-
-    /**
-     * Wrapper for ldap_mod_replace()
-     */
-    protected function ldap_mod_replace($dn, $entry)
-    {
-        $this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true));
-
-        if (!ldap_mod_replace($this->conn, $dn, $entry)) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            return false;
-        }
-
-        $this->_debug("S: OK");
-        return true;
-    }
-
-    /**
-     * Wrapper for ldap_mod_add()
-     */
-    protected function ldap_mod_add($dn, $entry)
-    {
-        $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
-
-        if (!ldap_mod_add($this->conn, $dn, $entry)) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            return false;
-        }
-
-        $this->_debug("S: OK");
-        return true;
-    }
-
-    /**
-     * Wrapper for ldap_mod_del()
-     */
-    protected function ldap_mod_del($dn, $entry)
-    {
-        $this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true));
-
-        if (!ldap_mod_del($this->conn, $dn, $entry)) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            return false;
-        }
-
-        $this->_debug("S: OK");
-        return true;
-    }
-
-    /**
-     * Wrapper for ldap_rename()
-     */
-    protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
-    {
-        $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
-
-        if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            return false;
-        }
-
-        $this->_debug("S: OK");
-        return true;
-    }
-
-    /**
-     * Wrapper for ldap_list()
-     */
-    protected function ldap_list($dn, $filter, $attrs = array(''))
-    {
-        $list = array();
-        $this->_debug("C: List [dn: $dn] [{$filter}]");
-
-        if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) {
-            $list = ldap_get_entries($this->conn, $result);
-
-            if ($list === false) {
-                $this->_debug("S: ".ldap_error($this->conn));
-                return array();
-            }
-
-            $count = $list['count'];
-            unset($list['count']);
-
-            $this->_debug("S: $count record(s)");
-        }
-        else {
-            $this->_debug("S: ".ldap_error($this->conn));
-        }
-
-        return $list;
-    }
-
 }
diff --git a/lib/ext/Roundcube/rcube_ldap_generic.php b/lib/ext/Roundcube/rcube_ldap_generic.php
new file mode 100644
index 0000000..88378dc
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_ldap_generic.php
@@ -0,0 +1,1049 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | Roundcube/rcube_ldap_generic.php                                      |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
+ | Copyright (C) 2012-2013, Kolab Systems AG                             |
+ |                                                                       |
+ | 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:                                                              |
+ |   Provide basic functionality for accessing LDAP directories          |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube at gmail.com>                        |
+ |         Aleksander Machniak <machniak at kolabsys.com>                   |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/*
+  LDAP connection properties
+  --------------------------
+
+  $prop = array(
+      'host'            => '<ldap-server-address>',
+      // or
+      'hosts'           => array('directory.verisign.com'),
+      'port'            => 389,
+      'use_tls'         => true|false,
+      'ldap_version'    => 3,             // using LDAPv3
+      'auth_method'     => '',            // SASL authentication method (for proxy auth), e.g. DIGEST-MD5
+      'attributes'      => array('dn'),   // List of attributes to read from the server
+      'vlv'             => false,         // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
+      'config_root_dn'  => 'cn=config',   // Root DN to read config (e.g. vlv indexes) from
+      'numsub_filter'   => '(objectClass=organizationalUnit)',   // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
+      'sizelimit'       => '0',           // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
+      'timelimit'       => '0',           // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
+      'network_timeout' => 10,            // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
+      'referrals'       => true|false,    // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
+  );
+*/
+
+/**
+ * Model class to access an LDAP directories
+ *
+ * @package    Framework
+ * @subpackage LDAP
+ */
+class rcube_ldap_generic
+{
+    const UPDATE_MOD_ADD = 1;
+    const UPDATE_MOD_DELETE = 2;
+    const UPDATE_MOD_REPLACE = 4;
+    const UPDATE_MOD_FULL = 7;
+
+    public $conn;
+    public $vlv_active = false;
+
+    /** private properties */
+    protected $cache = null;
+    protected $config = array();
+    protected $attributes = array('dn');
+    protected $entries = null;
+    protected $result = null;
+    protected $debug = false;
+    protected $list_page = 1;
+    protected $page_size = 10;
+    protected $vlv_config = null;
+
+
+    /**
+    * Object constructor
+    *
+    * @param array $p LDAP connection properties
+    */
+    function __construct($p)
+    {
+        $this->config = $p;
+
+        if (is_array($p['attributes']))
+            $this->attributes = $p['attributes'];
+
+        if (!is_array($p['hosts']) && !empty($p['host']))
+            $this->config['hosts'] = array($p['host']);
+    }
+
+    /**
+     * Activate/deactivate debug mode
+     *
+     * @param boolean $dbg True if LDAP commands should be logged
+     */
+    public function set_debug($dbg = true)
+    {
+        $this->debug = $dbg;
+    }
+
+    /**
+     * Set connection options
+     *
+     * @param mixed $opt Option name as string or hash array with multiple options
+     * @param mixed $val Option value
+     */
+    public function set_config($opt, $val = null)
+    {
+        if (is_array($opt))
+            $this->config = array_merge($this->config, $opt);
+        else
+            $this->config[$opt] = $value;
+    }
+
+    /**
+     * Enable caching by passing an instance of rcube_cache to be used by this object
+     *
+     * @param object rcube_cache Instance or False to disable caching
+     */
+    public function set_cache($cache_engine)
+    {
+        $this->cache = $cache_engine;
+    }
+
+    /**
+     * Set properties for VLV-based paging
+     *
+     * @param  number $page  Page number to list (starting at 1)
+     * @param  number $size  Number of entries to display on one page
+     */
+    public function set_vlv_page($page, $size = 10)
+    {
+        $this->list_page = $page;
+        $this->page_size = $size;
+    }
+
+    /**
+    * Establish a connection to the LDAP server
+    */
+    public function connect($host = null)
+    {
+        if (!function_exists('ldap_connect')) {
+            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "No ldap support in this installation of PHP"),
+                true);
+            return false;
+        }
+
+        if (is_resource($this->conn) && $this->config['host'] == $host)
+            return true;
+
+        if (empty($this->config['ldap_version']))
+            $this->config['ldap_version'] = 3;
+
+        // iterate over hosts if none specified
+        if (!$host) {
+            if (!is_array($this->config['hosts']))
+                $this->config['hosts'] = array($this->config['hosts']);
+
+            foreach ($this->config['hosts'] as $host) {
+                if ($this->connect($host)) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        // open connection to the given $host
+        $host     = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
+        $hostname = $host . ($this->config['port'] ? ':'.$this->config['port'] : '');
+
+        $this->_debug("C: Connect to $hostname [{$this->config['name']}]");
+
+        if ($lc = @ldap_connect($host, $this->config['port'])) {
+            if ($this->config['use_tls'] === true)
+                if (!ldap_start_tls($lc))
+                    continue;
+
+            $this->_debug("S: OK");
+
+            ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->config['ldap_version']);
+            $this->config['host'] = $host;
+            $this->conn = $lc;
+
+            if (!empty($this->config['network_timeout']))
+              ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->config['network_timeout']);
+
+            if (isset($this->config['referrals']))
+                ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->config['referrals']);
+        }
+        else {
+            $this->_debug("S: NOT OK");
+        }
+
+        if (!is_resource($this->conn)) {
+            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Could not connect to any LDAP server, last tried $hostname"),
+                true);
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Bind connection with (SASL-) user and password
+     *
+     * @param string $authc Authentication user
+     * @param string $pass  Bind password
+     * @param string $authz Autorization user
+     *
+     * @return boolean True on success, False on error
+     */
+    public function sasl_bind($authc, $pass, $authz=null)
+    {
+        if (!$this->conn) {
+            return false;
+        }
+
+        if (!function_exists('ldap_sasl_bind')) {
+            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Unable to bind: ldap_sasl_bind() not exists"),
+                true);
+            return false;
+        }
+
+        if (!empty($authz)) {
+            $authz = 'u:' . $authz;
+        }
+
+        if (!empty($this->config['auth_method'])) {
+            $method = $this->config['auth_method'];
+        }
+        else {
+            $method = 'DIGEST-MD5';
+        }
+
+        $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: $pass]");
+
+        if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
+            $this->_debug("S: OK");
+            return true;
+        }
+
+        $this->_debug("S: ".ldap_error($this->conn));
+
+        rcube::raise_error(array(
+            'code' => ldap_errno($this->conn), 'type' => 'ldap',
+            'file' => __FILE__, 'line' => __LINE__,
+            'message' => "SASL Bind failed for authcid=$authc ".ldap_error($this->conn)),
+            true);
+        return false;
+    }
+
+    /**
+     * Bind connection with DN and password
+     *
+     * @param string $dn   Bind DN
+     * @param string $pass Bind password
+     *
+     * @return boolean True on success, False on error
+     */
+    public function bind($dn, $pass)
+    {
+        if (!$this->conn) {
+            return false;
+        }
+
+        $this->_debug("C: Bind $dn [pass: $pass]");
+
+        if (@ldap_bind($this->conn, $dn, $pass)) {
+            $this->_debug("S: OK");
+            return true;
+        }
+
+        $this->_debug("S: ".ldap_error($this->conn));
+
+        rcube::raise_error(array(
+            'code' => ldap_errno($this->conn), 'type' => 'ldap',
+            'file' => __FILE__, 'line' => __LINE__,
+            'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
+            true);
+
+        return false;
+    }
+
+    /**
+     * Close connection to LDAP server
+     */
+    public function close()
+    {
+        if ($this->conn) {
+            $this->_debug("C: Close");
+            ldap_unbind($this->conn);
+            $this->conn = null;
+        }
+    }
+
+    /**
+     * Return the last result set
+     *
+     * @return object rcube_ldap_result Result object
+     */
+    function get_result()
+    {
+        return $this->result;
+    }
+
+    /**
+     * Get a specific LDAP entry, identified by its DN
+     *
+     * @param string $dn Record identifier
+     * @return array     Hash array
+     */
+    function get_entry($dn)
+    {
+        $rec = null;
+
+        if ($this->conn && $dn) {
+            $this->_debug("C: Read $dn [(objectclass=*)]");
+
+            if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $this->attributes)) {
+                $this->_debug("S: OK");
+
+                if ($entry = ldap_first_entry($this->conn, $ldap_result)) {
+                    $rec = ldap_get_attributes($this->conn, $entry);
+                }
+            }
+            else {
+                $this->_debug("S: ".ldap_error($this->conn));
+            }
+
+            if (!empty($rec)) {
+                $rec['dn'] = $dn; // Add in the dn for the entry.
+            }
+        }
+
+        return $rec;
+    }
+
+    /**
+     * Execute the LDAP search based on the stored credentials
+     *
+     * @param string $base_dn  The base DN to query
+     * @param string $filter   The LDAP filter for search
+     * @param string $scope    The LDAP scope (list|sub|base)
+     * @param array  $attrs    List of entry attributes to read
+     * @param array  $prop     Hash array with query configuration properties:
+     *   - sort: array of sort attributes (has to be in sync with the VLV index)
+     *   - search: search string used for VLV controls
+     * @param boolean $count_only Set to true if only entry count is requested
+     *
+     * @return mixed  rcube_ldap_result object or number of entries (if count_only=true) or false on error
+     */
+    public function search($base_dn, $filter = '', $scope = 'sub', $attrs = array('dn'), $prop = array(), $count_only = false)
+    {
+        if (!$this->conn) {
+            return false;
+        }
+
+        if (empty($filter)) {
+            $filter = '(objectclass=*)';
+        }
+
+        $this->_debug("C: Search $base_dn for $filter");
+
+        $function = self::scope2func($scope, $ns_function);
+
+        // find available VLV index for this query
+        if (!$count_only && ($vlv_sort = $this->_find_vlv($base_dn, $filter, $scope, $prop['sort']))) {
+            // when using VLV, we get the total count by...
+            // ...either reading numSubOrdinates attribute
+            if (($sub_filter = $this->config['numsub_filter']) &&
+                ($result_count = @$ns_function($this->conn, $base_dn, $sub_filter, array('numSubOrdinates'), 0, 0, 0))
+            ) {
+                $counts = ldap_get_entries($this->conn, $result_count);
+                for ($vlv_count = $j = 0; $j < $counts['count']; $j++)
+                    $vlv_count += $counts[$j]['numsubordinates'][0];
+                $this->_debug("D: total numsubordinates = " . $vlv_count);
+            }
+            // ...or by fetching all records dn and count them
+            else if (!function_exists('ldap_parse_virtuallist_control')) {
+                $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $prop, true);
+            }
+
+            $this->vlv_active = $this->_vlv_set_controls($vlv_sort, $this->list_page, $this->page_size, $prop['search']);
+        }
+        else {
+            $this->vlv_active = false;
+        }
+
+        // only fetch dn for count (should keep the payload low)
+        if ($ldap_result = @$function($this->conn, $base_dn, $filter,
+            $attrs, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit'])
+        ) {
+            // when running on a patched PHP we can use the extended functions
+            // to retrieve the total count from the LDAP search result
+            if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
+                if (ldap_parse_result($this->conn, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) {
+                    ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult);
+                    $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count");
+                }
+                else {
+                    $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
+                }
+            }
+            else if ($this->debug) {
+                $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found");
+            }
+
+            $this->result = new rcube_ldap_result($this->conn, $ldap_result, $base_dn, $filter, $vlv_count);
+
+            return $count_only ? $this->result->count() : $this->result;
+        }
+        else {
+            $this->_debug("S: ".ldap_error($this->conn));
+        }
+
+        return false;
+    }
+
+    /**
+     * Modify an LDAP entry on the server
+     *
+     * @param string $dn      Entry DN
+     * @param array  $params  Hash array of entry attributes
+     * @param int    $mode    Update mode (UPDATE_MOD_ADD | UPDATE_MOD_DELETE | UPDATE_MOD_REPLACE)
+     */
+    public function modify($dn, $parms, $mode = 255)
+    {
+        // TODO: implement this
+
+        return false;
+    }
+
+    /**
+     * Wrapper for ldap_add()
+     *
+     * @see ldap_add()
+     */
+    public function add($dn, $entry)
+    {
+        $this->_debug("C: Add $dn: ".print_r($entry, true));
+
+        $res = ldap_add($this->conn, $dn, $entry);
+        if ($res === false) {
+            $this->_debug("S: ".ldap_error($this->conn));
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        return true;
+    }
+
+    /**
+     * Wrapper for ldap_delete()
+     *
+     * @see ldap_delete()
+     */
+    public function delete($dn)
+    {
+        $this->_debug("C: Delete $dn");
+
+        $res = ldap_delete($this->conn, $dn);
+        if ($res === false) {
+            $this->_debug("S: ".ldap_error($this->conn));
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        return true;
+    }
+
+    /**
+     * Wrapper for ldap_mod_replace()
+     *
+     * @see ldap_mod_replace()
+     */
+    public function mod_replace($dn, $entry)
+    {
+        $this->_debug("C: Replace $dn: ".print_r($entry, true));
+
+        if (!ldap_mod_replace($this->conn, $dn, $entry)) {
+            $this->_debug("S: ".ldap_error($this->conn));
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        return true;
+    }
+
+    /**
+     * Wrapper for ldap_mod_add()
+     *
+     * @see ldap_mod_add()
+     */
+    public function mod_add($dn, $entry)
+    {
+        $this->_debug("C: Add $dn: ".print_r($entry, true));
+
+        if (!ldap_mod_add($this->conn, $dn, $entry)) {
+            $this->_debug("S: ".ldap_error($this->conn));
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        return true;
+    }
+
+    /**
+     * Wrapper for ldap_mod_del()
+     *
+     * @see ldap_mod_del()
+     */
+    public function mod_del($dn, $entry)
+    {
+        $this->_debug("C: Delete $dn: ".print_r($entry, true));
+
+        if (!ldap_mod_del($this->conn, $dn, $entry)) {
+            $this->_debug("S: ".ldap_error($this->conn));
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        return true;
+    }
+
+    /**
+     * Wrapper for ldap_rename()
+     *
+     * @see ldap_rename()
+     */
+    public function rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
+    {
+        $this->_debug("C: Rename $dn to $newrdn");
+
+        if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
+            $this->_debug("S: ".ldap_error($this->conn));
+            return false;
+        }
+
+        $this->_debug("S: OK");
+        return true;
+    }
+
+    /**
+     * Wrapper for ldap_list() + ldap_get_entries()
+     *
+     * @see ldap_list()
+     * @see ldap_get_entries()
+     */
+    public function list_entries($dn, $filter, $attributes = array('dn'))
+    {
+        $list = array();
+        $this->_debug("C: List $dn [{$filter}]");
+
+        if ($result = ldap_list($this->conn, $dn, $filter, $attributes)) {
+            $list = ldap_get_entries($this->conn, $result);
+
+            if ($list === false) {
+                $this->_debug("S: ".ldap_error($this->conn));
+                return array();
+            }
+
+            $count = $list['count'];
+            unset($list['count']);
+
+            $this->_debug("S: $count record(s)");
+        }
+        else {
+            $this->_debug("S: ".ldap_error($this->conn));
+        }
+
+        return $list;
+    }
+
+    /**
+     * Wrapper for ldap_read() + ldap_get_entries()
+     *
+     * @see ldap_read()
+     * @see ldap_get_entries()
+     */
+    public function read_entries($dn, $filter, $attributes = null)
+    {
+        $this->_debug("C: Read $dn [{$filter}]");
+
+        if ($this->conn && $dn) {
+            if (!$attributes)
+                $attributes = $this->attributes;
+
+            $result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']);
+            if ($result === false) {
+                $this->_debug("S: ".ldap_error($this->conn));
+                return false;
+            }
+
+            $this->_debug("S: OK");
+            return ldap_get_entries($this->conn, $result);
+        }
+
+        return false;
+    }
+
+    /**
+     * Choose the right PHP function according to scope property
+     *
+     * @param string $scope         The LDAP scope (sub|base|list)
+     * @param string $ns_function   Function to be used for numSubOrdinates queries
+     * @return string  PHP function to be used to query directory
+     */
+    public static function scope2func($scope, &$ns_function = null)
+    {
+        switch ($scope) {
+          case 'sub':
+            $function = $ns_function  = 'ldap_search';
+            break;
+          case 'base':
+            $function = $ns_function = 'ldap_read';
+            break;
+          default:
+            $function = 'ldap_list';
+            $ns_function = 'ldap_read';
+            break;
+        }
+
+        return $function;
+    }
+
+    /**
+     * Convert the given scope integer value to a string representation
+     */
+    public static function scopeint2str($scope)
+    {
+        switch ($scope) {
+            case 2:  return 'sub';
+            case 1:  return 'one';
+            case 0:  return 'base';
+            default: $this->_debug("Scope $scope is not a valid scope integer");
+        }
+
+        return '';
+    }
+
+    /**
+     * Escapes the given value according to RFC 2254 so that it can be safely used in LDAP filters.
+     *
+     * @param string $val Value to quote
+     * @return string The escaped value
+     */
+    public static function escape_value($val)
+    {
+        return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29',
+            '\\'=>'\5c', '/'=>'\2f'));
+    }
+
+    /**
+     * Escapes a DN value according to RFC 2253
+     *
+     * @param string $dn DN value o quote
+     * @return string The escaped value
+     */
+    public static function escape_dn($dn)
+    {
+        return strtr($str, array(','=>'\2c', '='=>'\3d', '+'=>'\2b',
+            '<'=>'\3c', '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c',
+            '"'=>'\22', '#'=>'\23'));
+    }
+
+    /**
+     * Normalize a LDAP result by converting entry attributes arrays into single values
+     *
+     * @param array $result LDAP result set fetched with ldap_get_entries()
+     * @return array        Hash array with normalized entries, indexed by their DNs
+     */
+    public static function normalize_result($result)
+    {
+        if (!is_array($result)) {
+            return array();
+        }
+
+        $entries  = array();
+        for ($i = 0; $i < $result['count']; $i++) {
+            $key = $result[$i]['dn'] ? $result[$i]['dn'] : $i;
+            $entries[$key] = self::normalize_entry($result[$i]);
+        }
+
+        return $entries;
+    }
+
+    /**
+     * Turn an LDAP entry into a regular PHP array with attributes as keys.
+     *
+     * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
+     * @return array       Hash array with attributes as keys
+     */
+    public static function normalize_entry($entry)
+    {
+        $rec = array();
+        for ($i=0; $i < $entry['count']; $i++) {
+            $attr = $entry[$i];
+            if ($entry[$attr]['count'] == 1) {
+                switch ($attr) {
+                    case 'objectclass':
+                        $rec[$attr] = array(strtolower($entry[$attr][0]));
+                        break;
+                    default:
+                        $rec[$attr] = $entry[$attr][0];
+                        break;
+                }
+            }
+            else {
+                for ($j=0; $j < $entry[$attr]['count']; $j++) {
+                    $rec[$attr][$j] = $entry[$attr][$j];
+                }
+            }
+        }
+
+        return $rec;
+    }
+
+    /**
+     * Set server controls for Virtual List View (paginated listing)
+     */
+    private function _vlv_set_controls($sort, $list_page, $page_size, $search = null)
+    {
+        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => self::_sort_ber_encode((array)$sort));
+        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => self::_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
+
+        $this->_debug("C: Set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
+            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size; $search)");
+
+        if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
+            $this->_debug("S: ".ldap_error($this->conn));
+            $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns unified attribute name (resolving aliases)
+     */
+    private static function _attr_name($namev)
+    {
+        // list of known attribute aliases
+        static $aliases = array(
+            'gn' => 'givenname',
+            'rfc822mailbox' => 'email',
+            'userid' => 'uid',
+            'emailaddress' => 'email',
+            'pkcs9email' => 'email',
+        );
+
+        list($name, $limit) = explode(':', $namev, 2);
+        $suffix = $limit ? ':'.$limit : '';
+
+        return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
+    }
+
+    /**
+     * Quotes attribute value string
+     *
+     * @param string $str Attribute value
+     * @param bool   $dn  True if the attribute is a DN
+     *
+     * @return string Quoted string
+     */
+    public static function quote_string($str, $dn=false)
+    {
+        // take firt entry if array given
+        if (is_array($str))
+            $str = reset($str);
+
+        if ($dn)
+            $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
+                '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
+        else
+            $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
+                '/'=>'\2f');
+
+        return strtr($str, $replace);
+    }
+
+    /**
+     * Prints debug info to the log
+     */
+    private function _debug($str)
+    {
+        if ($this->debug && class_exists('rcube')) {
+            rcube::write_log('ldap', $str);
+        }
+    }
+
+
+    /*****************  Virtual List View (VLV) related utility functions  **************** */
+
+    /**
+     * Return the search string value to be used in VLV controls
+     */
+    private function _vlv_search($sort, $search)
+    {
+        foreach ($search as $attr => $value) {
+            if (!in_array(strtolower($attr), $sort)) {
+                $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")");
+                return null;
+            } else {
+                return $value;
+            }
+        }
+    }
+
+    /**
+     * Find a VLV index matching the given query attributes
+     *
+     * @return string Sort attribute or False if no match
+     */
+    private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null)
+    {
+        if (!$this->config['vlv'] || $scope == 'base') {
+            return false;
+        }
+
+        // get vlv config
+        $vlv_config = $this->_read_vlv_config();
+
+        if ($vlv = $vlv_config[$base_dn]) {
+            $this->_debug("D: Found a VLV for $base_dn");
+
+            if ($vlv['filter'] == strtolower($filter) || stripos($filter, '(&'.$vlv['filter'].'(') === 0) {
+                $this->_debug("D: Filter matches");
+                if ($vlv['scope'] == $scope) {
+                    // Not passing any sort attributes means you don't care
+                    if (empty($sort_attrs) || in_array($sort_attrs, $vlv['sort'])) {
+                        return $vlv['sort'][0];
+                    }
+                }
+                else {
+                    $this->_debug("D: Scope does not match");
+                }
+            }
+            else {
+                $this->_debug("D: Filter does not match");
+            }
+        }
+        else {
+            $this->_debug("D: No VLV for $base_dn");
+        }
+
+        return false;
+    }
+
+    /**
+     * Return VLV indexes and searches including necessary configuration
+     * details.
+     */
+    private function _read_vlv_config()
+    {
+        if (empty($this->config['vlv']) || empty($this->config['config_root_dn'])) {
+            return array();
+        }
+        // return hard-coded VLV config
+        else if (is_array($this->config['vlv'])) {
+            return $this->config['vlv'];
+        }
+
+        // return cached result
+        if (is_array($this->vlv_config)) {
+            return $this->vlv_config;
+        }
+
+        if ($this->cache && ($cached_config = $this->cache->get('vlvconfig'))) {
+            $this->vlv_config = $cached_config;
+            return $this->vlv_config;
+        }
+
+        $this->vlv_config = array();
+
+        $ldap_result = ldap_search($this->conn, $this->config['config_root_dn'], '(objectclass=vlvsearch)', array('*'), 0, 0, 0);
+        $vlv_searches = new rcube_ldap_result($this->conn, $ldap_result, $this->config['config_root_dn'], '(objectclass=vlvsearch)');
+
+        if ($vlv_searches->count() < 1) {
+            $this->_debug("D: Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'");
+            return array();
+        }
+
+        foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) {
+            // Multiple indexes may exist
+            $ldap_result = ldap_search($this->conn, $vlv_search_dn, '(objectclass=vlvindex)', array('*'), 0, 0, 0);
+            $vlv_indexes = new rcube_ldap_result($this->conn, $ldap_result, $vlv_search_dn, '(objectclass=vlvindex)');
+
+            // Reset this one for each VLV search.
+            $_vlv_sort = array();
+            foreach ($vlv_indexes->entries(true) as $vlv_index_dn => $vlv_index_attrs) {
+                $_vlv_sort[] = explode(' ', $vlv_index_attrs['vlvsort']);
+            }
+
+            $this->vlv_config[$vlv_search_attrs['vlvbase']] = array(
+                'scope'  => self::scopeint2str($vlv_search_attrs['vlvscope']),
+                'filter' => strtolower($vlv_search_attrs['vlvfilter']),
+                'sort'   => $_vlv_sort,
+            );
+        }
+
+        // cache this
+        if ($this->cache)
+            $this->cache->set('vlvconfig', $this->vlv_config);
+
+        $this->_debug("D: Refreshed VLV config: " . var_export($this->vlv_config, true));
+
+        return $this->vlv_config;
+    }
+
+    /**
+     * Generate BER encoded string for Virtual List View option
+     *
+     * @param integer List offset (first record)
+     * @param integer Records per page
+     *
+     * @return string BER encoded option value
+     */
+    private static function _vlv_ber_encode($offset, $rpp, $search = '')
+    {
+        /*
+            this string is ber-encoded, php will prefix this value with:
+            04 (octet string) and 10 (length of 16 bytes)
+            the code behind this string is broken down as follows:
+            30 = ber sequence with a length of 0e (14) bytes following
+            02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
+            02 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
+            a0 = type context-specific/constructed with a length of 06 (6) bytes following
+            02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
+            02 = type integer with 2 bytes following (contentCount):  01 00
+
+            with a search string present:
+            81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
+            81 indicates a user string is present where as a a0 indicates just a offset search
+            81 = type context-specific/constructed with a length of 06 (6) bytes following
+
+            The following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
+            encoding of integer values (note: these values are in
+            two-complement form so since offset will never be negative bit 8 of the
+            leftmost octet should never by set to 1):
+            8.3.2: If the contents octets of an integer value encoding consist
+            of more than one octet, then the bits of the first octet (rightmost)
+            and bit 8 of the second (to the left of first octet) octet:
+                a) shall not all be ones; and
+                b) shall not all be zero
+        */
+
+        if ($search) {
+            $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
+            $ber_val = self::_string2hex($search);
+            $str = self::_ber_addseq($ber_val, '81');
+        }
+        else {
+            // construct the string from right to left
+            $str = "020100"; # contentCount
+
+            $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
+
+            // calculate octet length of $ber_val
+            $str = self::_ber_addseq($ber_val, '02') . $str;
+
+            // now compute length over $str
+            $str = self::_ber_addseq($str, 'a0');
+        }
+
+        // now tack on records per page
+        $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
+
+        // now tack on sequence identifier and length
+        $str = self::_ber_addseq($str, '30');
+
+        return pack('H'.strlen($str), $str);
+    }
+
+    /**
+     * create ber encoding for sort control
+     *
+     * @param array List of cols to sort by
+     * @return string BER encoded option value
+     */
+    private static function _sort_ber_encode($sortcols)
+    {
+        $str = '';
+        foreach (array_reverse((array)$sortcols) as $col) {
+            $ber_val = self::_string2hex($col);
+
+            // 30 = ber sequence with a length of octet value
+            // 04 = octet string with a length of the ascii value
+            $oct = self::_ber_addseq($ber_val, '04');
+            $str = self::_ber_addseq($oct, '30') . $str;
+        }
+
+        // now tack on sequence identifier and length
+        $str = self::_ber_addseq($str, '30');
+
+        return pack('H'.strlen($str), $str);
+    }
+
+    /**
+     * Add BER sequence with correct length and the given identifier
+     */
+    private static function _ber_addseq($str, $identifier)
+    {
+        $len = dechex(strlen($str)/2);
+        if (strlen($len) % 2 != 0)
+            $len = '0'.$len;
+
+        return $identifier . $len . $str;
+    }
+
+    /**
+     * Returns BER encoded integer value in hex format
+     */
+    private static function _ber_encode_int($offset)
+    {
+        $val = dechex($offset);
+        $prefix = '';
+
+        // check if bit 8 of high byte is 1
+        if (preg_match('/^[89abcdef]/', $val))
+            $prefix = '00';
+
+        if (strlen($val)%2 != 0)
+            $prefix .= '0';
+
+        return $prefix . $val;
+    }
+
+    /**
+     * Returns ascii string encoded in hex
+     */
+    private static function _string2hex($str)
+    {
+        $hex = '';
+        for ($i=0; $i < strlen($str); $i++) {
+            $hex .= dechex(ord($str[$i]));
+        }
+        return $hex;
+    }
+
+}
diff --git a/lib/ext/Roundcube/rcube_ldap_result.php b/lib/ext/Roundcube/rcube_ldap_result.php
new file mode 100644
index 0000000..efc3331
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_ldap_result.php
@@ -0,0 +1,130 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | Roundcube/rcube_ldap_result.php                                       |
+ |                                                                       |
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
+ | Copyright (C) 2013, Kolab Systems AG                                  |
+ |                                                                       |
+ | 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:                                                              |
+ |   Model class that represents an LDAP search result                   |
+ |                                                                       |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube at gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Model class representing an LDAP search result
+ *
+ * @package    Framework
+ * @subpackage LDAP
+ */
+class rcube_ldap_result implements Iterator
+{
+    public $conn;
+    public $ldap;
+    public $base_dn;
+    public $filter;
+
+    private $count = null;
+    private $current = null;
+    private $iteratorkey = 0;
+
+    /**
+     * Default constructor
+     *
+     * @param resource $conn LDAP link identifier
+     * @param resource $ldap LDAP result entry identifier
+     * @param string   $base_dn   Base DN used to get this result
+     * @param string   $filter    Filter query used to get this result
+     * @param integer  $count     Record count value (pre-calculated)
+     */
+    function __construct($conn, $ldap, $base_dn, $filter, $count = null)
+    {
+        $this->conn = $conn;
+        $this->ldap = $ldap;
+        $this->base_dn = $base_dn;
+        $this->filter = $filter;
+        $this->count = $count;
+    }
+
+    /**
+     * Wrapper for ldap_sort()
+     */
+    public function sort($attr)
+    {
+        return ldap_sort($this->conn, $this->ldap, $attr);
+    }
+
+    /**
+     * Get entries count
+     */
+    public function count()
+    {
+        if (!isset($this->count))
+            $this->count = ldap_count_entries($this->conn, $this->ldap);
+
+        return $this->count;
+    }
+
+    /**
+     * Wrapper for ldap_get_entries()
+     *
+     * @param boolean $normalize Optionally normalize the entries to a list of hash arrays
+     * @return array  List of LDAP entries
+     */
+    public function entries($normalize = false)
+    {
+        $entries = ldap_get_entries($this->conn, $this->ldap);
+        return $normalize ? rcube_ldap_generic::normalize_result($entries) : $entries;
+    }
+
+    /**
+     * Wrapper for ldap_get_dn() using the current entry pointer
+     */
+    public function get_dn()
+    {
+        return $this->current ? ldap_get_dn($this->conn, $this->current) : null;
+    }
+
+
+    /***  Implements the PHP 5 Iterator interface to make foreach work  ***/
+
+    function current()
+    {
+        $attrib = ldap_get_attributes($this->conn, $this->current);
+        $attrib['dn'] = ldap_get_dn($this->conn, $this->current);
+        return $attrib;
+    }
+
+    function key()
+    {
+        return $this->iteratorkey;
+    }
+
+    function rewind()
+    {
+        $this->iteratorkey = 0;
+        $this->current = ldap_first_entry($this->conn, $this->ldap);
+    }
+
+    function next()
+    {
+        $this->iteratorkey++;
+        $this->current = ldap_next_entry($this->conn, $this->current);
+    }
+
+    function valid()
+    {
+        return (bool)$this->current;
+    }
+
+}
diff --git a/lib/ext/Roundcube/rcube_spellchecker.php b/lib/ext/Roundcube/rcube_spellchecker.php
index 60aec50..166de9b 100644
--- a/lib/ext/Roundcube/rcube_spellchecker.php
+++ b/lib/ext/Roundcube/rcube_spellchecker.php
@@ -377,7 +377,7 @@ class rcube_spellchecker
         if (!$store) {
             $this->error = "Empty result from spelling engine";
         }
-        else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m)) {
+        else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) {
             $this->error = "Error code $m[1] returned";
         }
 
diff --git a/lib/ext/Roundcube/rcube_storage.php b/lib/ext/Roundcube/rcube_storage.php
index b17291b..4b336f2 100644
--- a/lib/ext/Roundcube/rcube_storage.php
+++ b/lib/ext/Roundcube/rcube_storage.php
@@ -540,12 +540,13 @@ abstract class rcube_storage
     /**
      * Append a mail message (source) to a specific folder.
      *
-     * @param string  $folder  Target folder
-     * @param string  $message The message source string or filename
-     * @param string  $headers Headers string if $message contains only the body
-     * @param boolean $is_file True if $message is a filename
-     * @param array   $flags   Message flags
-     * @param mixed   $date    Message internal date
+     * @param string       $folder  Target folder
+     * @param string|array $message The message source string or filename
+     *                              or array (of strings and file pointers)
+     * @param string       $headers Headers string if $message contains only the body
+     * @param boolean      $is_file True if $message is a filename
+     * @param array        $flags   Message flags
+     * @param mixed        $date    Message internal date
      *
      * @return int|bool Appended message UID or True on success, False on error
      */
diff --git a/lib/ext/Roundcube/rcube_utils.php b/lib/ext/Roundcube/rcube_utils.php
index 29baa82..6c3bd21 100644
--- a/lib/ext/Roundcube/rcube_utils.php
+++ b/lib/ext/Roundcube/rcube_utils.php
@@ -360,12 +360,8 @@ class rcube_utils
             return $value;
         }
 
-        // strip single quotes if magic_quotes_sybase is enabled
-        if (ini_get('magic_quotes_sybase')) {
-            $value = str_replace("''", "'", $value);
-        }
         // strip slashes if magic_quotes enabled
-        else if (get_magic_quotes_gpc() || get_magic_quotes_runtime()) {
+        if (get_magic_quotes_gpc() || get_magic_quotes_runtime()) {
             $value = stripslashes($value);
         }
 
diff --git a/lib/ext/Roundcube/rcube_vcard.php b/lib/ext/Roundcube/rcube_vcard.php
index 90acb21..a71305c 100644
--- a/lib/ext/Roundcube/rcube_vcard.php
+++ b/lib/ext/Roundcube/rcube_vcard.php
@@ -714,9 +714,15 @@ class rcube_vcard
                             $value[] = $attrvalues;
                         }
                         else if (is_bool($attrvalues)) {
-                            // true means just tag, not tag=value, as in PHOTO;BASE64:...
+                            // true means just a tag, not tag=value, as in PHOTO;BASE64:...
                             if ($attrvalues) {
-                                $attr .= strtoupper(";$attrname");
+                                // vCard v3 uses ENCODING=B (#1489183)
+                                if ($attrname == 'base64') {
+                                    $attr .= ";ENCODING=B";
+                                }
+                                else {
+                                    $attr .= strtoupper(";$attrname");
+                                }
                             }
                         }
                         else {
diff --git a/lib/plugins/kolab_auth/config.inc.php.dist b/lib/plugins/kolab_auth/config.inc.php.dist
index 05c610b..3f77ea2 100644
--- a/lib/plugins/kolab_auth/config.inc.php.dist
+++ b/lib/plugins/kolab_auth/config.inc.php.dist
@@ -2,6 +2,17 @@
 
 // The id of the LDAP address book (which refers to the $rcmail_config['ldap_public'])
 // or complete addressbook definition array.
+// --------------------------------------------------------------------
+// Note: Multi-domain (hosted) installations can resolve domain aliases
+//   by adding following settings in kolab_auth_addressbook spec.:
+//
+//   'domain_base_dn'   => 'cn=kolab,cn=config',
+//   'domain_filter'    => '(&(objectclass=domainrelatedobject)(associateddomain=%s))',
+//   'domain_name_attr' => 'associateddomain',
+//
+//   With this %dc variable in base_dn and groups/base_dn will be
+//   replaced with DN string of resolved domain
+//---------------------------------------------------------------------
 $rcmail_config['kolab_auth_addressbook'] = '';
 
 // This will overwrite defined filter
diff --git a/lib/plugins/kolab_auth/kolab_auth.php b/lib/plugins/kolab_auth/kolab_auth.php
index 719df98..5579743 100644
--- a/lib/plugins/kolab_auth/kolab_auth.php
+++ b/lib/plugins/kolab_auth/kolab_auth.php
@@ -61,12 +61,12 @@ class kolab_auth extends rcube_plugin
             $rcmail->config->set('imap_debug', true);
             $rcmail->config->set('ldap_debug', true);
             $rcmail->config->set('smtp_debug', true);
-
         }
 
     }
 
-    public function startup($args) {
+    public function startup($args)
+    {
         // Arguments are task / action, not interested
         if (!empty($_SESSION['user_roledns'])) {
             $this->load_user_role_plugins_and_settings($_SESSION['user_roledns']);
@@ -75,7 +75,8 @@ class kolab_auth extends rcube_plugin
         return $args;
     }
 
-    public function load_user_role_plugins_and_settings($role_dns) {
+    public function load_user_role_plugins_and_settings($role_dns)
+    {
         $rcmail = rcube::get_instance();
         $this->load_config();
 
@@ -151,7 +152,8 @@ class kolab_auth extends rcube_plugin
         }
     }
 
-    public function write_log($args) {
+    public function write_log($args)
+    {
         $rcmail = rcube::get_instance();
 
         if (!$rcmail->config->get('kolab_auth_auditlog', false)) {
@@ -286,7 +288,7 @@ class kolab_auth extends rcube_plugin
         }
 
         // Find user record in LDAP
-        $record = $this->get_user_record($user, $host);
+        $record = $ldap->get_user_record($user, $host);
 
         if (empty($record)) {
             $args['abort'] = true;
@@ -309,8 +311,7 @@ class kolab_auth extends rcube_plugin
         // Login As...
         if (!empty($loginas) && $admin_login) {
             // Authenticate to LDAP
-            $dn     = rcube_ldap::dn_decode($record['ID']);
-            $result = $ldap->bind($dn, $pass);
+            $result = $ldap->bind($record['dn'], $pass);
 
             if (!$result) {
                 $args['abort'] = true;
@@ -318,32 +319,24 @@ class kolab_auth extends rcube_plugin
             }
 
             // check if the original user has/belongs to administrative role/group
-            $isadmin   = false;
-            $group     = $rcmail->config->get('kolab_auth_group');
-            $role_attr = $rcmail->config->get('kolab_auth_role');
-            $role_dn   = $rcmail->config->get('kolab_auth_role_value');
+            $isadmin = false;
+            $group   = $rcmail->config->get('kolab_auth_group');
+            $role_dn = $rcmail->config->get('kolab_auth_role_value');
 
             // check role attribute
             if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
-                $role_dn = $this->parse_vars($role_dn, $user, $host);
-                foreach ((array)$record[$role_attr] as $role) {
-                    if ($role == $role_dn) {
-                        $isadmin = true;
-                        break;
-                    }
+                $role_dn = $ldap->parse_vars($role_dn, $user, $host);
+                if (in_array($role_dn, (array)$record[$role_attr])) {
+                    $isadmin = true;
                 }
             }
 
             // check group
             if (!$isadmin && !empty($group)) {
-                $groups = $ldap->get_record_groups($record['ID']);
-                foreach ($groups as $g => $prop) {
-                    if ($group == rcube_ldap::dn_decode($g)) {
-                        $isadmin = true;
-                        break;
-                    }
+                $groups = $ldap->get_user_groups($record['dn'], $user, $host);
+                if (in_array($group, $groups)) {
+                    $isadmin = true;
                 }
-
             }
 
             // Save original user login for log (see below)
@@ -358,7 +351,7 @@ class kolab_auth extends rcube_plugin
 
             // user has the privilage, get "login as" user credentials
             if ($isadmin) {
-                $record = $this->get_user_record($loginas, $host);
+                $record = $ldap->get_user_record($loginas, $host);
             }
 
             if (empty($record)) {
@@ -376,7 +369,7 @@ class kolab_auth extends rcube_plugin
 
         // Store UID and DN of logged user in session for use by other plugins
         $_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid'];
-        $_SESSION['kolab_dn']  = $record['ID']; // encoded
+        $_SESSION['kolab_dn']  = $record['dn'];
 
         // Set user login
         if ($login_attr) {
@@ -485,80 +478,10 @@ class kolab_auth extends rcube_plugin
             return null;
         }
 
-        self::$ldap = new kolab_auth_ldap_backend(
-            $addressbook,
-            $rcmail->config->get('ldap_debug'),
-            $rcmail->config->mail_domain($_SESSION['imap_host'])
-        );
+        require_once __DIR__ . '/kolab_auth_ldap.php';
 
-        $rcmail->add_shutdown_function(array(self::$ldap, 'close'));
+        self::$ldap = new kolab_auth_ldap($addressbook);
 
         return self::$ldap;
     }
-
-    /**
-     * Fetches user data from LDAP addressbook
-     */
-    private function get_user_record($user, $host)
-    {
-        $rcmail = rcube::get_instance();
-        $filter = $rcmail->config->get('kolab_auth_filter');
-        $filter = $this->parse_vars($filter, $user, $host);
-        $ldap   = self::ldap();
-
-        // reset old result
-        $ldap->reset();
-
-        // get record
-        $ldap->set_filter($filter);
-        $results = $ldap->list_records();
-
-        if (count($results->records) == 1) {
-            return $results->records[0];
-        }
-    }
-
-    /**
-     * Prepares filter query for LDAP search
-     */
-    private function parse_vars($str, $user, $host)
-    {
-        $rcmail = rcube::get_instance();
-        $domain = $rcmail->config->get('username_domain');
-
-        if (!empty($domain) && strpos($user, '@') === false) {
-            if (is_array($domain) && isset($domain[$host])) {
-                $user .= '@'.rcube_utils::parse_host($domain[$host], $host);
-            }
-            else if (is_string($domain)) {
-                $user .= '@'.rcube_utils::parse_host($domain, $host);
-            }
-        }
-
-        // replace variables in filter
-        list($u, $d) = explode('@', $user);
-        $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
-        $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
-
-        return strtr($str, $replaces);
-    }
-}
-
-/**
- * Wrapper class for rcube_ldap addressbook
- */
-class kolab_auth_ldap_backend extends rcube_ldap
-{
-    function __construct($p, $debug=false, $mail_domain=null)
-    {
-        parent::__construct($p, $debug, $mail_domain);
-        $this->fieldmap['uid'] = 'uid';
-    }
-
-    function set_filter($filter)
-    {
-        if ($filter) {
-            $this->prop['filter'] = $filter;
-        }
-    }
 }
diff --git a/lib/plugins/kolab_auth/kolab_auth_ldap.php b/lib/plugins/kolab_auth/kolab_auth_ldap.php
new file mode 100644
index 0000000..c8ebcd1
--- /dev/null
+++ b/lib/plugins/kolab_auth/kolab_auth_ldap.php
@@ -0,0 +1,489 @@
+<?php
+
+/**
+ * Kolab Authentication
+ *
+ * @version @package_version@
+ * @author Aleksander Machniak <machniak at kolabsys.com>
+ *
+ * Copyright (C) 2011-2013, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Wrapper class for rcube_ldap_generic
+ */
+class kolab_auth_ldap extends rcube_ldap_generic
+{
+    private $icache = array();
+
+
+    function __construct($p)
+    {
+        $rcmail = rcube::get_instance();
+
+        $this->debug    = (bool) $rcmail->config->get('ldap_debug');
+        $this->fieldmap = $p['fieldmap'];
+        $this->fieldmap['uid'] = 'uid';
+
+        $p['attributes'] = array_values($this->fieldmap);
+
+        // Connect to the server (with bind)
+        parent::__construct($p);
+        $this->_connect();
+
+        $rcmail->add_shutdown_function(array($this, 'close'));
+    }
+
+    /**
+    * Establish a connection to the LDAP server
+    */
+    private function _connect()
+    {
+        $rcube = rcube::get_instance();
+
+        // try to connect + bind for every host configured
+        // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
+        // see http://www.php.net/manual/en/function.ldap-connect.php
+        foreach ((array)$this->config['hosts'] as $host) {
+            // skip host if connection failed
+            if (!$this->connect($host)) {
+                continue;
+            }
+
+            $bind_pass = $this->config['bind_pass'];
+            $bind_user = $this->config['bind_user'];
+            $bind_dn   = $this->config['bind_dn'];
+
+            if (empty($bind_pass)) {
+                $this->ready = true;
+            }
+            else {
+                if (!empty($bind_dn)) {
+                    $this->ready = $this->bind($bind_dn, $bind_pass);
+                }
+                else if (!empty($this->config['auth_cid'])) {
+                    $this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_user);
+                }
+                else {
+                    $this->ready = $this->sasl_bind($bind_user, $bind_pass);
+                }
+            }
+
+            // connection established, we're done here
+            if ($this->ready) {
+                break;
+            }
+
+        }  // end foreach hosts
+
+        if (!is_resource($this->conn)) {
+            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Could not connect to any LDAP server, last tried $host"), true);
+
+            $this->ready = false;
+        }
+
+        return $this->ready;
+    }
+
+    /**
+     * Fetches user data from LDAP addressbook
+     */
+    function get_user_record($user, $host)
+    {
+        $rcmail  = rcube::get_instance();
+        $filter  = $rcmail->config->get('kolab_auth_filter');
+        $filter  = $this->parse_vars($filter, $user, $host);
+        $base_dn = $this->parse_vars($this->config['base_dn'], $user, $host);
+        $scope   = $this->config['scope'];
+
+        // @TODO: print error if filter is empty
+
+        // get record
+        if ($result = parent::search($base_dn, $filter, $scope, $this->attributes)) {
+            if ($result->count() == 1) {
+                $entries = $result->entries(true);
+                $dn      = key($entries);
+                $entry   = array_pop($entries);
+                $entry   = $this->field_mapping($dn, $entry);
+
+                return $entry;
+            }
+        }
+    }
+
+    /**
+     * Fetches user data from LDAP addressbook
+     */
+    function get_user_groups($dn, $user, $host)
+    {
+        if (empty($dn) || empty($this->config['groups'])) {
+            return array();
+        }
+
+        $base_dn     = $this->parse_vars($this->config['groups']['base_dn'], $user, $host);
+        $name_attr   = $this->config['groups']['name_attr'] ? $this->config['groups']['name_attr'] : 'cn';
+        $member_attr = $this->get_group_member_attr();
+        $filter      = "(member=$dn)(uniqueMember=$dn)";
+
+        if ($member_attr != 'member' && $member_attr != 'uniqueMember')
+            $filter .= "($member_attr=$dn)";
+        $filter = strtr("(|$filter)", array("\\" => "\\\\"));
+
+        $result = parent::search($base_dn, $filter, 'sub', array('dn', $name_attr));
+
+        if (!$result) {
+            return array();
+        }
+
+        $groups = array();
+        foreach ($result as $entry) {
+            $entry = rcube_ldap_generic::normalize_entry($entry);
+            if (!$entry['dn']) {
+                $entry['dn'] = $result->get_dn();
+            }
+            $groups[$entry['dn']] = $entry[$name_attr];
+        }
+
+        return $groups;
+    }
+
+    /**
+     * Get a specific LDAP record
+     *
+     * @param string DN
+     *
+     * @return array Record data
+     */
+    function get_record($dn)
+    {
+        if (!$this->ready) {
+            return;
+        }
+
+        if ($rec = $this->get_entry($dn)) {
+            $rec = rcube_ldap_generic::normalize_entry($rec);
+            $rec = $this->field_mapping($dn, $rec);
+        }
+
+        return $rec;
+    }
+
+    /**
+     * Replace LDAP record data items
+     *
+     * @param string $dn    DN
+     * @param array  $entry LDAP entry
+     *
+     * return bool True on success, False on failure
+     */
+    function replace($dn, $entry)
+    {
+        // fields mapping
+        foreach ($this->fieldmap as $field => $attr) {
+            if (array_key_exists($field, $entry)) {
+                $entry[$attr] = $entry[$field];
+                unset($entry[$field]);
+            }
+        }
+
+        return $this->mod_replace($dn, $entry);
+    }
+
+    /**
+     * Search records (simplified version of rcube_ldap::search)
+     *
+     * @param mixed   $fields   The field name of array of field names to search in
+     * @param mixed   $value    Search value (or array of values when $fields is array)
+     * @param int     $mode     Matching mode:
+     *                          0 - partial (*abc*),
+     *                          1 - strict (=),
+     *                          2 - prefix (abc*)
+     * @param boolean $select   True if results are requested, False if count only
+     * @param array   $required List of fields that cannot be empty
+     * @param int     $limit    Number of records
+     *
+     * @return array List or false on error
+     */
+    function search($fields, $value, $mode=1, $required = array(), $limit = 0)
+    {
+        $mode = intval($mode);
+
+        // use AND operator for advanced searches
+        $filter = is_array($value) ? '(&' : '(|';
+
+        // set wildcards
+        $wp = $ws = '';
+        if (!empty($this->config['fuzzy_search']) && $mode != 1) {
+            $ws = '*';
+            if (!$mode) {
+                $wp = '*';
+            }
+        }
+
+        foreach ((array)$fields as $idx => $field) {
+            $val = is_array($value) ? $value[$idx] : $value;
+            if ($attrs = (array) $this->fieldmap[$field]) {
+                if (count($attrs) > 1)
+                    $filter .= '(|';
+                foreach ($attrs as $f)
+                    $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
+                if (count($attrs) > 1)
+                    $filter .= ')';
+            }
+        }
+        $filter .= ')';
+
+        // add required (non empty) fields filter
+        $req_filter = '';
+
+        foreach ((array)$required as $field) {
+            if (in_array($field, (array)$fields))  // required field is already in search filter
+                continue;
+            if ($attrs = (array) $this->fieldmap[$field]) {
+                if (count($attrs) > 1)
+                    $req_filter .= '(|';
+                foreach ($attrs as $f)
+                    $req_filter .= "($f=*)";
+                if (count($attrs) > 1)
+                    $req_filter .= ')';
+            }
+        }
+
+        if (!empty($req_filter)) {
+            $filter = '(&' . $req_filter . $filter . ')';
+        }
+
+        // avoid double-wildcard if $value is empty
+        $filter = preg_replace('/\*+/', '*', $filter);
+
+        // add general filter to query
+        if (!empty($this->config['filter'])) {
+            $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->config['filter']) . ')' . $filter . ')';
+        }
+
+        $base_dn = $this->parse_vars($this->config['base_dn']);
+        $scope   = $this->config['scope'];
+        $attrs   = array_values($this->fieldmap);
+        $list    = array();
+
+        if ($result = parent::search($base_dn, $filter, $scope, $attrs)) {
+            $i = 0;
+            foreach ($result as $entry) {
+                if ($limit && $limit <= $i) {
+                    break;
+                }
+                $dn = $result->get_dn();
+                $list[$dn] = $this->field_mapping($dn, $entry);
+                $i++;
+            }
+        }
+
+        return $list;
+    }
+
+    /**
+     * Set filter used in search()
+     */
+    function set_filter($filter)
+    {
+        $this->config['filter'] = $filter;
+    }
+
+    /**
+     * Maps LDAP attributes to defined fields
+     */
+    protected function field_mapping($dn, $entry)
+    {
+        $entry['dn'] = $dn;
+
+        // fields mapping
+        foreach ($this->fieldmap as $field => $attr) {
+            if (isset($entry[$attr])) {
+                $entry[$field] = $entry[$attr];
+            }
+        }
+
+        return $entry;
+    }
+
+    /**
+     * Detects group member attribute name
+     */
+    private function get_group_member_attr($object_classes = array())
+    {
+        if (empty($object_classes)) {
+            $object_classes = $this->config['groups']['object_classes'];
+        }
+        if (!empty($object_classes)) {
+            foreach ((array)$object_classes as $oc) {
+                switch (strtolower($oc)) {
+                    case 'group':
+                    case 'groupofnames':
+                    case 'kolabgroupofnames':
+                        $member_attr = 'member';
+                        break;
+
+                    case 'groupofuniquenames':
+                    case 'kolabgroupofuniquenames':
+                        $member_attr = 'uniqueMember';
+                        break;
+                }
+            }
+        }
+
+        if (!empty($member_attr)) {
+            return $member_attr;
+        }
+
+        if (!empty($this->config['groups']['member_attr'])) {
+            return $this->config['groups']['member_attr'];
+        }
+
+        return 'member';
+    }
+
+    /**
+     * Prepares filter query for LDAP search
+     */
+    function parse_vars($str, $user = null, $host = null)
+    {
+        // When authenticating user $user is always set
+        // if not set it means we use this LDAP object for other
+        // purposes, e.g. kolab_delegation, then username with
+        // correct domain is in a session
+        if (!$user) {
+            $user = $_SESSION['username'];
+        }
+        else if (isset($this->icache[$user])) {
+            list($user, $dc) = $this->icache[$user];
+        }
+        else {
+            $orig_user = $user;
+            $rcmail = rcube::get_instance();
+
+            // get default domain
+            if ($username_domain = $rcmail->config->get('username_domain')) {
+                if ($host && is_array($username_domain) && isset($username_domain[$host])) {
+                    $domain = rcube_utils::parse_host($username_domain[$host], $host);
+                }
+                else if (is_string($username_domain)) {
+                    $domain = rcube_utils::parse_host($username_domain, $host);
+                }
+            }
+
+            // realmed username (with domain)
+            if (strpos($user, '@')) {
+                list($usr, $dom) = explode('@', $user);
+
+                // unrealm domain, user login can contain a domain alias
+                if ($dom != $domain && ($r_domain = $this->find_domain($dom))) {
+                    // $dom is a domain DN string?
+                    if (strpos($r_domain, '=')) {
+                        $dc = $r_domain;
+                    }
+                    else {
+                        $user = $usr . '@' . $r_domain;
+                    }
+                }
+            }
+            else if ($domain && !strpos($user, '@')) {
+                $user .= '@' . $domain;
+            }
+
+            $this->icache[$orig_user] = array($user, $dc);
+        }
+
+        // replace variables in filter
+        list($u, $d) = explode('@', $user);
+
+        // hierarchal domain string
+        if (empty($dc)) {
+            $dc = 'dc=' . strtr($d, array('.' => ',dc='));
+        }
+
+        $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
+
+        return strtr($str, $replaces);
+    }
+
+    /**
+     * Find root domain for specified domain
+     *
+     * @param string $domain Domain name
+     *
+     * @return string Domain name or domain DN string
+     */
+    function find_domain($domain)
+    {
+        if (empty($domain) || empty($this->config['domain_base_dn']) || empty($this->config['domain_filter'])) {
+            return null;
+        }
+
+        $base_dn   = $this->config['domain_base_dn'];
+        $filter    = $this->config['domain_filter'];
+        $name_attr = $this->config['domain_name_attribute'];
+
+        if (empty($name_attr)) {
+            $name_attr = 'associateddomain';
+        }
+
+        $filter = str_replace('%s', rcube_ldap_generic::quote_string($domain), $filter);
+        $result = parent::search($base_dn, $filter, 'sub', array($name_attr, 'inetdomainbasedn'));
+
+        if (!$result) {
+            return null;
+        }
+
+        $entries  = $result->entries(true);
+        $entry_dn = key($entries);
+        $entry    = $entries[$entry_dn];
+
+        if (is_array($entry)) {
+            if (!empty($entry['inetdomainbasedn'])) {
+                return $entry['inetdomainbasedn'];
+            }
+
+            return is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+        }
+    }
+
+    /**
+     * HTML-safe DN string encoding
+     *
+     * @param string $str DN string
+     *
+     * @return string Encoded HTML identifier string
+     */
+    static function dn_encode($str)
+    {
+        return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
+    }
+
+    /**
+     * Decodes DN string encoded with _dn_encode()
+     *
+     * @param string $str Encoded HTML identifier string
+     *
+     * @return string DN string
+     */
+    static function dn_decode($str)
+    {
+        $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
+        return base64_decode($str);
+    }
+}
diff --git a/lib/plugins/kolab_auth/package.xml b/lib/plugins/kolab_auth/package.xml
index 2d75d83..00bc969 100644
--- a/lib/plugins/kolab_auth/package.xml
+++ b/lib/plugins/kolab_auth/package.xml
@@ -18,10 +18,10 @@
 		<email>machniak at kolabsys.com</email>
 		<active>yes</active>
 	</lead>
-	<date>2012-12-19</date>
+	<date>2013-06-25</date>
 	<version>
-		<release>0.6</release>
-		<api>0.1</api>
+		<release>0.7</release>
+		<api>0.2</api>
 	</version>
 	<stability>
 		<release>stable</release>
@@ -35,6 +35,10 @@
 				<tasks:replace from="@name@" to="name" type="package-info"/>
 				<tasks:replace from="@package_version@" to="version" type="package-info"/>
 			</file>
+			<file name="kolab_auth_ldap.php" role="php">
+				<tasks:replace from="@name@" to="name" type="package-info"/>
+				<tasks:replace from="@package_version@" to="version" type="package-info"/>
+			</file>
 			<file name="config.inc.php.dist" role="data"></file>
 			<file name="LICENSE" role="data"></file>
 
diff --git a/lib/plugins/libkolab/SQL/mysql.initial.sql b/lib/plugins/libkolab/SQL/mysql.initial.sql
index 8603bc8..764da2a 100644
--- a/lib/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/plugins/libkolab/SQL/mysql.initial.sql
@@ -21,7 +21,9 @@ CREATE TABLE `kolab_cache` (
   `dtend` DATETIME,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
-  PRIMARY KEY(`resource`,`type`,`msguid`)
+  `filename` varchar(255) DEFAULT NULL,
+  PRIMARY KEY(`resource`,`type`,`msguid`),
+  INDEX `resource_filename` (`resource`, `filename`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013011000');
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013041900');
diff --git a/lib/plugins/libkolab/SQL/mysql/2013041900.sql b/lib/plugins/libkolab/SQL/mysql/2013041900.sql
new file mode 100644
index 0000000..76577e6
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2013041900.sql
@@ -0,0 +1,3 @@
+DELETE FROM `kolab_cache` WHERE `type` = 'file';
+ALTER TABLE `kolab_cache` ADD `filename` varchar(255) DEFAULT NULL;
+ALTER TABLE `kolab_cache` ADD INDEX `resource_filename` (`resource`, `filename`);
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index d8b5162..0c571f2 100644
--- a/lib/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -255,6 +255,8 @@ class kolab_format_contact extends kolab_format
 
         // TODO: handle language, gpslocation, etc.
 
+        // set type property for proper caching
+        $object['_type'] = 'contact';
 
         // cache this data
         $this->data = $object;
diff --git a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
index 0110e4e..304cdc4 100644
--- a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -44,7 +44,6 @@ class kolab_format_distributionlist extends kolab_format
 
         $this->obj->setName($object['name']);
 
-        $seen = array();
         $members = new vectorcontactref;
         foreach ((array)$object['member'] as $member) {
             if ($member['uid'])
@@ -56,7 +55,6 @@ class kolab_format_distributionlist extends kolab_format
 
             $m->setName($member['name']);
             $members->push($m);
-            $seen[$member['email']]++;
         }
 
         $this->obj->setMembers($members);
@@ -100,11 +98,11 @@ class kolab_format_distributionlist extends kolab_format
         $members = $this->obj->members();
         for ($i=0; $i < $members->size(); $i++) {
             $member = $members->get($i);
-#            if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
+//            if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
                 $object['member'][] = array(
-                    'uid' => $member->uid(),
+                    'uid'   => $member->uid(),
                     'email' => $member->email(),
-                    'name' => $member->name(),
+                    'name'  => $member->name(),
                 );
         }
 
diff --git a/lib/plugins/libkolab/lib/kolab_format_file.php b/lib/plugins/libkolab/lib/kolab_format_file.php
index a71a794..f5b153b 100644
--- a/lib/plugins/libkolab/lib/kolab_format_file.php
+++ b/lib/plugins/libkolab/lib/kolab_format_file.php
@@ -66,12 +66,22 @@ class kolab_format_file extends kolab_format
 
             // make sure size is set, so object saved in cache contains this info
             if (!isset($attach_attr['size'])) {
-                if (isset($attach_attr['content'])) {
-                    $object['_attachments'][$cid]['size'] = strlen($attach_attr['content']);
+                $size = 0;
+
+                if (!empty($attach_attr['content'])) {
+                    if (is_resource($attach_attr['content'])) {
+                        $stat = fstat($attach_attr['content']);
+                        $size = $stat ? $stat['size'] : 0;
+                    }
+                    else {
+                        $size = strlen($attach_attr['content']);
+                    }
                 }
                 else if (isset($attach_attr['path'])) {
-                    $object['_attachments'][$cid]['size'] = @filesize($attach_attr['path']);
+                    $size = @filesize($attach_attr['path']);
                 }
+
+                $object['_attachments'][$cid]['size'] = $size;
             }
         }
 
@@ -81,7 +91,7 @@ class kolab_format_file extends kolab_format
     }
 
     /**
-     *
+     * Check if object's data validity
      */
     public function is_valid()
     {
diff --git a/lib/plugins/libkolab/lib/kolab_storage.php b/lib/plugins/libkolab/lib/kolab_storage.php
index 08e11b2..4205cf3 100644
--- a/lib/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/plugins/libkolab/lib/kolab_storage.php
@@ -29,8 +29,8 @@ class kolab_storage
     const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
     const COLOR_KEY_SHARED  = '/shared/vendor/kolab/color';
     const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
-    const NAME_KEY_SHARED   = '/shared/vendor/kolab/name';
-    const NAME_KEY_PRIVATE  = '/private/vendor/kolab/name';
+    const NAME_KEY_SHARED   = '/shared/vendor/kolab/displayname';
+    const NAME_KEY_PRIVATE  = '/private/vendor/kolab/displayname';
 
     public static $version = '3.0';
     public static $last_error;
@@ -102,6 +102,23 @@ class kolab_storage
         return $folders;
     }
 
+    /**
+     * Getter for the storage folder for the given type
+     *
+     * @param string Data type to list folders for (contact,distribution-list,event,task,note)
+     * @return object kolab_storage_folder  The folder object
+     */
+    public static function get_default_folder($type)
+    {
+        if (self::setup()) {
+            foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
+                return new kolab_storage_folder($foldername, $folderdata[$foldername]);
+            }
+        }
+
+        return null;
+    }
+
 
     /**
      * Getter for a specific storage folder
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index 6d8071b..a3b6626 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -37,7 +37,11 @@ class kolab_storage_cache
     private $ready = false;
     private $max_sql_packet;
     private $max_sync_lock_time = 600;
-    private $binary_cols = array('photo','pgppublickey','pkcs7publickey');
+    private $binary_items = array(
+        'photo'          => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
+        'pgppublickey'   => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></photo>|i',
+        'pkcs7publickey' => '|<key><uri>date:application/pkcs7-mime;base64,([^<]+)</uri></photo>|i',
+    );
 
 
     /**
@@ -524,7 +528,6 @@ class kolab_storage_cache
      */
     private function _serialize($object)
     {
-        $bincols  = array_flip($this->binary_cols);
         $sql_data = array('changed' => null, 'dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => '');
         $objtype  = $object['_type'] ? $object['_type'] : $this->folder->type;
 
@@ -566,12 +569,13 @@ class kolab_storage_cache
         // extract object data
         $data = array();
         foreach ($object as $key => $val) {
+            // skip empty properties
             if ($val === "" || $val === null) {
-                // skip empty properties
                 continue;
             }
-            if (isset($bincols[$key])) {
-                $data[$key] = base64_encode($val);
+            // mark binary data to be extracted from xml on unserialize()
+            if (isset($this->binary_items[$key])) {
+                $data[$key] = true;
             }
             else if ($key[0] != '_') {
                 $data[$key] = $val;
@@ -597,9 +601,10 @@ class kolab_storage_cache
         $object = unserialize($sql_arr['data']);
 
         // decode binary properties
-        foreach ($this->binary_cols as $key) {
-            if (!empty($object[$key]))
-                $object[$key] = base64_decode($object[$key]);
+        foreach ($this->binary_items as $key => $regexp) {
+            if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) {
+                $object[$key] = base64_decode($m[1]);
+            }
         }
 
         // add meta data
@@ -708,17 +713,15 @@ class kolab_storage_cache
         if (!$sql_arr) {
             $this->db->query(
                 "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml)".
-                " VALUES (?, ?, 1, ?, '', '', '')",
+                " VALUES (?, ?, 1, " . $this->db->now() . ", '', '', '')",
                 $this->resource_uri,
-                'lock',
-                date('Y-m-d H:i:s')
+                'lock'
             );
         }
         else {
             $this->db->query(
-                "UPDATE kolab_cache SET msguid=1, created=? ".
-                "WHERE resource=? AND type=?",
-                date('Y-m-d H:i:s'),
+                "UPDATE kolab_cache SET msguid = 1, created = " . $this->db->now() .
+                " WHERE resource = ? AND type = ?",
                 $this->resource_uri,
                 'lock'
             );
@@ -734,8 +737,7 @@ class kolab_storage_cache
             return;
 
         $this->db->query(
-            "UPDATE kolab_cache SET msguid=0 ".
-            "WHERE resource=? AND type=?",
+            "UPDATE kolab_cache SET msguid = 0 WHERE resource = ? AND type = ?",
             $this->resource_uri,
             'lock'
         );
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
index 702b621..84cb041 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -246,7 +246,7 @@ class kolab_storage_folder
         if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
             $subpath = substr($this->name, strlen($nsdata[0][0]));
             if ($ns == 'other') {
-                list($user, $suffix) = explode($nsdata[0][1], $subpath);
+                list($user, $suffix) = explode($nsdata[0][1], $subpath, 2);
                 $subpath = $suffix;
             }
         }
@@ -421,7 +421,7 @@ class kolab_storage_folder
         $this->cache->synchronize();
 
         $msguid = $this->cache->uid2msguid($uid);
-        if ($msguid && ($object = $this->cache->get($msguid)))
+        if ($msguid && ($object = $this->cache->get($msguid, '*')))
             return $object;
 
         return false;
@@ -648,7 +648,16 @@ class kolab_storage_folder
                 // make sure size is set, so object saved in cache contains this info
                 if (!isset($attachment['size'])) {
                     if (!empty($attachment['content'])) {
-                        $attachment['size'] = strlen($attachment['content']);
+                        if (is_resource($attachment['content'])) {
+                            // this need to be a seekable resource, otherwise
+                            // fstat() failes and we're unable to determine size
+                            // here nor in rcube_imap_generic before IMAP APPEND
+                            $stat = fstat($attachment['content']);
+                            $attachment['size'] = $stat ? $stat['size'] : 0;
+                        }
+                        else {
+                            $attachment['size'] = strlen($attachment['content']);
+                        }
                     }
                     else if (!empty($attachment['path'])) {
                         $attachment['size'] = filesize($attachment['path']);
@@ -681,20 +690,14 @@ class kolab_storage_folder
         $binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');
 
         // generate and save object message
-        if ($raw_msg = $this->build_message($object, $type, $binary)) {
+        if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) {
             // resolve old msguid before saving
             if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
                 $object['_msguid'] = $msguid;
                 $object['_mailbox'] = $this->name;
             }
 
-            if (is_array($raw_msg)) {
-                $result = $this->imap->save_message($this->name, $raw_msg[0], $raw_msg[1], true, null, null, $binary);
-                @unlink($raw_msg[0]);
-            }
-            else {
-                $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
-            }
+            $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
 
             // delete old message
             if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
@@ -706,6 +709,11 @@ class kolab_storage_folder
             if ($result) {
                 $object['_msguid'] = $result;
                 $this->cache->insert($result, $object);
+
+                // remove temp file
+                if ($body_file) {
+                    @unlink($body_file);
+                }
             }
         }
 
@@ -861,10 +869,15 @@ class kolab_storage_folder
     /**
      * Creates source of the configuration object message
      *
+     * @param array  $object    The array that holds the data of the object.
+     * @param string $type      The type of the kolab object.
+     * @param bool   $binary    Enables use of binary encoding of attachment(s)
+     * @param string $body_file Reference to filename of message body
+     *
      * @return mixed Message as string or array with two elements
      *               (one for message file path, second for message headers)
      */
-    private function build_message(&$object, $type, $binary)
+    private function build_message(&$object, $type, $binary, &$body_file)
     {
         // load old object to preserve data we don't understand/process
         if (is_object($object['_formatobj']))
@@ -891,6 +904,7 @@ class kolab_storage_folder
         $mime     = new Mail_mime("\r\n");
         $rcmail   = rcube::get_instance();
         $headers  = array();
+        $files    = array();
         $part_id  = 1;
         $encoding = $binary ? 'binary' : 'base64';
 
@@ -914,8 +928,9 @@ class kolab_storage_folder
                 $memory += $attachment['size'];
             }
 
-            // 1.33 is for base64, we need at least 2x more memory than the message size
-            if ($memory * ($binary ? 1 : 1.33) * 2 > $mem_limit) {
+            // 1.33 is for base64, we need at least 4x more memory than the message size
+            if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) {
+                $marker   = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%';
                 $is_file  = true;
                 $temp_dir = unslashify($rcmail->config->get('temp_dir'));
                 $mime->setParam('delay_file_io', true);
@@ -964,19 +979,58 @@ class kolab_storage_folder
             $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
             $name = !empty($att['name']) ? $att['name'] : $key;
 
+            // To store binary files we can use faster method
+            // without writting full message content to a temporary file but
+            // directly to IMAP, see rcube_imap_generic::append().
+            // I.e. use file handles where possible
             if (!empty($att['content'])) {
-                $mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
+                if (is_resource($att['content']) && $is_file && $binary) {
+                    $files[] = $att['content'];
+                    $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
+                }
+                else {
+                    if (is_resource($att['content'])) {
+                        @rewind($att['content']);
+                        $att['content'] = stream_get_contents($att['content']);
+                    }
+                    $mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
+                }
                 $part_id++;
             }
             else if (!empty($att['path'])) {
-                $mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
+                if ($is_file && $binary) {
+                    $files[] = fopen($att['path'], 'r');
+                    $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
+                }
+                else {
+                    $mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
+                }
                 $part_id++;
             }
 
             $object['_attachments'][$key]['id'] = $part_id;
         }
 
-        if ($is_file) {
+        if (!$is_file || !empty($files)) {
+            $message = $mime->getMessage();
+        }
+
+        // parse message and build message array with
+        // attachment file pointers in place of file markers
+        if (!empty($files)) {
+            $message = explode($marker, $message);
+            $tmp     = array();
+
+            foreach ($message as $msg_part) {
+                $tmp[] = $msg_part;
+                if ($file = array_shift($files)) {
+                    $tmp[] = $file;
+                }
+            }
+            $message = $tmp;
+        }
+        // write complete message body into temp file
+        else if ($is_file) {
             // use common temp dir
             $body_file = tempnam($temp_dir, 'rcmMsg');
 
@@ -988,11 +1042,10 @@ class kolab_storage_folder
                 return false;
             }
 
-            return array($body_file, $mime->txtHeaders());
-        }
-        else {
-            return $mime->getMessage();
+            $message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r'));
         }
+
+        return $message;
     }
 
 
diff --git a/lib/plugins/libkolab/package.xml b/lib/plugins/libkolab/package.xml
index 69b2b6f..cd3e3a0 100644
--- a/lib/plugins/libkolab/package.xml
+++ b/lib/plugins/libkolab/package.xml
@@ -19,10 +19,10 @@
 		<email>machniak at kolabsys.com</email>
 		<active>yes</active>
 	</developer>
-	<date>2012-11-21</date>
+	<date>2013-04-19</date>
 	<version>
-		<release>0.9-beta</release>
-		<api>0.9-beta</api>
+		<release>0.9</release>
+		<api>0.9</api>
 	</version>
 	<stability>
 		<release>stable</release>
@@ -83,6 +83,7 @@
 			<file name="config.inc.php.dist" role="data"></file>
 			<file name="LICENSE" role="data"></file>
 			<file name="README" role="data"></file>
+			<file name="UPGRADING" role="data"></file>
 		</dir>
 		<!-- / -->
 	</contents>






More information about the commits mailing list