Thomas Bruederli
2013-07-10 afe20eed79a224f03723123f7dc19a0eee32e6ea
Backported changes from the dev-advanced-ldap-groups branch to 0.9
2 files added
23 files modified
2566 ■■■■■ changed files
config/main.inc.php.dist 57 ●●●● patch | view | raw | blame | history
program/include/rcmail.php 2 ●●● patch | view | raw | blame | history
program/js/app.js 120 ●●●● patch | view | raw | blame | history
program/lib/Roundcube/rcube_config.php 2 ●●● patch | view | raw | blame | history
program/lib/Roundcube/rcube_ldap.php 1065 ●●●● patch | view | raw | blame | history
program/lib/Roundcube/rcube_ldap_generic.php 1059 ●●●●● patch | view | raw | blame | history
program/localization/en_US/labels.inc 1 ●●●● patch | view | raw | blame | history
program/steps/addressbook/copy.inc 4 ●●●● patch | view | raw | blame | history
program/steps/addressbook/func.inc 66 ●●●● patch | view | raw | blame | history
program/steps/addressbook/list.inc 5 ●●●●● patch | view | raw | blame | history
program/steps/addressbook/save.inc 4 ●●●● patch | view | raw | blame | history
program/steps/addressbook/show.inc 5 ●●●● patch | view | raw | blame | history
program/steps/mail/list_contacts.inc 29 ●●●● patch | view | raw | blame | history
skins/classic/addressbook.css 31 ●●●●● patch | view | raw | blame | history
skins/classic/mail.css 8 ●●●●● patch | view | raw | blame | history
skins/classic/templates/addressbook.html 2 ●●● patch | view | raw | blame | history
skins/classic/templates/contact.html 2 ●●● patch | view | raw | blame | history
skins/larry/addressbook.css 49 ●●●●● patch | view | raw | blame | history
skins/larry/ie7hacks.css 1 ●●●● patch | view | raw | blame | history
skins/larry/images/contactgroup.png patch | view | raw | blame | history
skins/larry/mail.css 13 ●●●●● patch | view | raw | blame | history
skins/larry/styles.css 4 ●●●● patch | view | raw | blame | history
skins/larry/templates/addressbook.html 2 ●●● patch | view | raw | blame | history
skins/larry/templates/contact.html 2 ●●● patch | view | raw | blame | history
skins/larry/ui.js 33 ●●●●● patch | view | raw | blame | history
config/main.inc.php.dist
@@ -638,6 +638,7 @@
    'phone:work'  => 'telephoneNumber',
    'phone:mobile' => 'mobile',
    'phone:pager' => 'pager',
    'phone:workfax' => 'facsimileTelephoneNumber',
    'street'      => 'street',
    'zipcode'     => 'postalCode',
    'region'      => 'st',
@@ -648,9 +649,8 @@
    'department'   => 'ou',
    'jobtitle'     => 'title',
    'notes'        => 'description',
    'photo'        => 'jpegPhoto',
    // these currently don't work:
    // 'phone:workfax' => 'facsimileTelephoneNumber',
    // 'photo'         => 'jpegPhoto',
    // 'manager'       => 'manager',
    // 'assistant'     => 'secretary',
  ),
@@ -661,27 +661,50 @@
  // 'uid'  => 'md5(microtime())',               // You may specify PHP code snippets which are then eval'ed 
  // 'mail' => '{givenname}.{sn}@mydomain.com',  // or composite strings with placeholders for existing attributes
  ),
  'sort'          => 'cn',    // The field to sort the listing by.
  'scope'         => 'sub',   // search mode: sub|base|list
  'filter'        => '(objectClass=inetOrgPerson)',      // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
  'fuzzy_search'  => true,    // server allows wildcard search
  'vlv'           => false,   // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
  '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.
  'referrals'     => true|false,  // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
  'sort'           => 'cn',         // The field to sort the listing by.
  'scope'          => 'sub',        // search mode: sub|base|list
  'filter'         => '(objectClass=inetOrgPerson)',      // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
  'fuzzy_search'   => true,         // server allows wildcard search
  'vlv'            => false,        // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
  'vlv_search'     => false,        // Use Virtual List View functions for autocompletion searches (if server supports it)
  '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
  'config_root_dn' => 'cn=config',  // Root DN to search config entries (e.g. vlv indexes)
  '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.
  'referrals'      => false,        // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
  // definition for contact groups (uncomment if no groups are supported)
  // for the groups base_dn, the user replacements %fu, %u, $d and %dc work as for base_dn (see above)
  // if the groups base_dn is empty, the contact base_dn is used for the groups as well
  // -> in this case, assure that groups and contacts are separated due to the concernig filters! 
  'groups'        => array(
    'base_dn'     => '',
    'scope'       => 'sub',   // search mode: sub|base|list
    'filter'      => '(objectClass=groupOfNames)',
  'groups'  => array(
    'base_dn'        => '',
    'scope'          => 'sub',       // Search mode: sub|base|list
    'filter'         => '(objectClass=groupOfNames)',
    'object_classes' => array("top", "groupOfNames"),
    'member_attr'  => 'member',   // name of the member attribute, e.g. uniqueMember
    'name_attr'    => 'cn',       // attribute to be used as group name
    'member_attr'    => 'member',   // Name of the member attribute, e.g. uniqueMember
    'name_attr'      => 'cn',       // Attribute to be used as group name
    'member_filter'  => '(objectclass=*)',  // Optional filter to use when querying for group members
    'vlv'            => false,      // Use VLV controls to list groups
  ),
  // this configuration replaces the regular groups listing in the directory tree with
  // a hard-coded list of groups, each listing entries with the configured base DN and filter.
  // if the 'groups' option from above is set, it'll be shown as the first entry with the name 'Groups'
  'group_filters' => array(
    'departments' => array(
      'name'    => 'Company Departments',
      'scope'   => 'list',
      'base_dn' => 'ou=Groups,dc=mydomain,dc=com',
      'filter'  => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls))',
      'name_attr' => 'cn',
    ),
    'customers' => array(
      'name'    => 'Customers',
      'scope'   => 'sub',
      'base_dn' => 'ou=Customers,dc=mydomain,dc=com',
      'filter'  => '(objectClass=inetOrgPerson)',
      'name_attr' => 'sn',
    ),
  ),
);
*/
program/include/rcmail.php
@@ -295,7 +295,7 @@
        $list[$id] = array(
          'id'       => $id,
          'name'     => html::quote($prop['name']),
          'groups'   => is_array($prop['groups']),
          'groups'   => !empty($prop['groups']) || !empty($prop['group_filters']),
          'readonly' => !$prop['writable'],
          'hidden'   => $prop['hidden'],
          'autocomplete' => in_array($id, $autocomplete)
program/js/app.js
@@ -251,7 +251,8 @@
          }
        }
        else if (this.env.action == 'compose') {
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin'];
          this.env.address_group_stack = [];
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin'];
          if (this.env.drafts_mailbox)
            this.env.compose_commands.push('savedraft')
@@ -318,11 +319,13 @@
        break;
      case 'addressbook':
        this.env.address_group_stack = [];
        if (this.gui_objects.folderlist)
          this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
        this.enable_command('add', 'import', this.env.writable_source);
        this.enable_command('list', 'listgroup', 'listsearch', 'advanced-search', true);
        this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'advanced-search', true);
        if (this.gui_objects.contactslist) {
          this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
@@ -769,7 +772,7 @@
      case 'moveto':
        if (this.task == 'mail')
          this.move_messages(props);
        else if (this.task == 'addressbook' && this.drag_active)
        else if (this.task == 'addressbook')
          this.copy_contact(null, props);
        break;
@@ -1080,9 +1083,23 @@
        }
        break;
      case 'pushgroup':
        // add group ID to stack
        this.env.address_group_stack.push(props.id);
        if (obj && event)
          rcube_event.cancel(event);
      case 'listgroup':
        this.reset_qsearch();
        this.list_contacts(props.source, props.id);
        break;
      case 'popgroup':
        if (this.env.address_group_stack.length > 1) {
          this.env.address_group_stack.pop();
          this.reset_qsearch();
          this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]);
        }
        break;
      case 'import':
@@ -3058,7 +3075,13 @@
  this.compose_recipient_select = function(list)
  {
    this.enable_command('add-recipient', list.selection.length > 0);
    var id, n, recipients = 0;
    for (n=0; n < list.selection.length; n++) {
      id = list.selection[n];
      if (this.env.contactdata[id])
        recipients++;
    }
    this.enable_command('add-recipient', recipients);
  };
  this.compose_add_recipient = function(field)
@@ -4052,7 +4075,7 @@
    if (this.preview_timer)
      clearTimeout(this.preview_timer);
    var n, id, sid, ref = this, writable = false,
    var n, id, sid, contact, ref = this, writable = false,
      source = this.env.source ? this.env.address_sources[this.env.source] : null;
    // we don't have dblclick handler here, so use 200 instead of this.dblclick_time
@@ -4062,33 +4085,39 @@
      this.show_contentframe(false);
    if (list.selection.length) {
      list.draggable = false;
      // no source = search result, we'll need to detect if any of
      // selected contacts are in writable addressbook to enable edit/delete
      // we'll also need to know sources used in selection for copy
      // and group-addmember operations (drag&drop)
      this.env.selection_sources = [];
      if (!source) {
        for (n in list.selection) {
      if (source)
        this.env.selection_sources.push(this.env.source);
      for (n in list.selection) {
        contact = list.data[list.selection[n]];
        if (!source) {
          sid = String(list.selection[n]).replace(/^[^-]+-/, '');
          if (sid && this.env.address_sources[sid]) {
            writable = writable || !this.env.address_sources[sid].readonly;
            writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);
            this.env.selection_sources.push(sid);
          }
        }
        this.env.selection_sources = $.unique(this.env.selection_sources);
        else
          writable = writable || (!source.readonly && !contact.readonly);
      }
      else {
        this.env.selection_sources.push(this.env.source);
        writable = !source.readonly;
      }
      this.env.selection_sources = $.unique(this.env.selection_sources);
    }
    // if a group is currently selected, and there is at least one contact selected
    // thend we can enable the group-remove-selected command
    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0);
    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable);
    this.enable_command('compose', this.env.group || list.selection.length > 0);
    this.enable_command('edit', id && writable);
    this.enable_command('delete', list.selection.length && writable);
    this.enable_command('delete', list.selection.length > 0 && writable);
    return false;
  };
@@ -4116,10 +4145,28 @@
    else if (!this.env.search_request)
      folder = group ? 'G'+src+group : src;
    this.select_folder(folder, '', true);
    this.env.source = src;
    this.env.group = group;
    // truncate groups listing stack
    var index = $.inArray(this.env.group, this.env.address_group_stack);
    if (index < 0)
      this.env.address_group_stack = [];
    else
      this.env.address_group_stack = this.env.address_group_stack.slice(0,index);
    // make sure the current group is on top of the stack
    if (this.env.group) {
      this.env.address_group_stack.push(this.env.group);
      // mark the first group on the stack as selected in the directory list
      folder = 'G'+src+this.env.address_group_stack[0];
    }
    else if (this.gui_objects.addresslist_title) {
      $(this.gui_objects.addresslist_title).html(this.get_label('contacts'));
    }
    this.select_folder(folder, '', true);
    // load contacts remotely
    if (this.gui_objects.contactslist) {
@@ -4175,16 +4222,38 @@
  this.list_contacts_clear = function()
  {
    this.contact_list.data = {};
    this.contact_list.clear(true);
    this.show_contentframe(false);
    this.enable_command('delete', false);
    this.enable_command('compose', this.env.group ? true : false);
  };
  this.set_group_prop = function(prop)
  {
    if (this.gui_objects.addresslist_title) {
      var boxtitle = $(this.gui_objects.addresslist_title).html('');  // clear contents
      // add link to pop back to parent group
      if (this.env.address_group_stack.length > 1) {
        $('<a href="#list">...</a>')
          .addClass('poplink')
          .appendTo(boxtitle)
          .click(function(e){ return ref.command('popgroup','',this); });
        boxtitle.append('&nbsp;&raquo;&nbsp;');
      }
      boxtitle.append($('<span>'+prop.name+'</span>'));
    }
    this.triggerEvent('groupupdate', prop);
  };
  // load contact record
  this.load_contact = function(cid, action, framed)
  {
    var win, url = {}, target = window;
    var win, url = {}, target = window,
      rec = this.contact_list ? this.contact_list.data[cid] : null;
    if (win = this.get_frame_window(this.env.contentframe)) {
      url._framed = 1;
@@ -4195,7 +4264,9 @@
      if (!cid) {
        // unselect selected row(s)
        this.contact_list.clear_selection();
        this.enable_command('delete', 'compose', false);
        this.enable_command('compose', rec && rec.email);
        this.enable_command('delete', rec && rec._type != 'group');
      }
    }
    else if (framed)
@@ -4306,7 +4377,7 @@
  };
  // update a contact record in the list
  this.update_contact_row = function(cid, cols_arr, newcid, source)
  this.update_contact_row = function(cid, cols_arr, newcid, source, data)
  {
    var c, row, list = this.contact_list;
@@ -4333,11 +4404,13 @@
        list.selection[0] = newcid;
        row.style.display = '';
      }
      list.data[cid] = data;
    }
  };
  // add row to contacts list
  this.add_contact_row = function(cid, cols, classes)
  this.add_contact_row = function(cid, cols, classes, data)
  {
    if (!this.gui_objects.contactslist)
      return false;
@@ -4360,6 +4433,8 @@
      row.appendChild(col);
    }
    // store data in list member
    list.data[cid] = data;
    list.insert_row(row);
    this.enable_command('export', list.rowcount > 0);
@@ -4769,6 +4844,9 @@
          if (++colprop.count == colprop.limit && colprop.limit)
            $(menu).children('option[value="'+col+'"]').prop('disabled', true);
        }
        if (contact._type != 'group')
          list.draggable = true;
      }
    }
  };
program/lib/Roundcube/rcube_config.php
@@ -192,7 +192,7 @@
     */
    public function get($name, $def = null)
    {
        if (isset($this->prop[$name])) {
        if (array_key_exists($name, $this->prop)) {
            $result = $this->prop[$name];
        }
        else {
program/lib/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.                |
@@ -36,7 +36,7 @@
    public $coltypes = array();
    /** private properties */
    protected $conn;
    protected $ldap;
    protected $prop = array();
    protected $fieldmap = array();
    protected $sub_filter;
@@ -51,9 +51,6 @@
    private $group_url = null;
    private $cache;
    private $vlv_active = false;
    private $vlv_count = 0;
    /**
    * Object constructor
@@ -65,6 +62,8 @@
    function __construct($p, $debug = false, $mail_domain = null)
    {
        $this->prop = $p;
        $fetch_attributes = array('objectClass');
        if (isset($p['searchonly']))
            $this->searchonly = $p['searchonly'];
@@ -82,6 +81,21 @@
                $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
@@ -186,7 +200,22 @@
        // initialize cache
        $rcube = rcube::get_instance();
        $this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
        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);
        }
        // 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, true);
        $this->ldap->set_cache($this->cache);
        $this->_connect();
    }
@@ -199,49 +228,18 @@
    {
        $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;
            }
@@ -256,7 +254,7 @@
            $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']) {
@@ -277,7 +275,8 @@
                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']);
                        if (!$this->ldap->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']))
                            continue;  // bind failed, try neyt host
                    }
                    // Search for the dn to use to authenticate
@@ -286,10 +285,11 @@
                    $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
                    $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
                    // TODO: use $this->ldap->search() here
                    $res = @ldap_search($this->ldap->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))
                        if (($entry = ldap_first_entry($this->ldap->conn, $res))
                            && ($bind_dn = ldap_get_dn($this->ldap->conn, $entry))
                        ) {
                            $this->_debug("S: search returned dn: $bind_dn");
                            $dn = ldap_explode_dn($bind_dn, 1);
@@ -297,7 +297,7 @@
                        }
                    }
                    else {
                        $this->_debug("S: ".ldap_error($this->conn));
                        $this->_debug("S: ".ldap_error($this->ldap->conn));
                    }
                    // DN not found
@@ -329,13 +329,13 @@
            }
            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);
                }
            }
@@ -346,10 +346,10 @@
        }  // 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;
        }
@@ -359,100 +359,12 @@
    /**
     * 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, true);
        }
        if (!empty($authz)) {
            $authz = 'u:' . $authz;
        }
        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
     *
     * @return boolean True on success, False on error
     */
    public function bind($dn, $pass)
    {
        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;
    }
    /**
     * Close connection to LDAP server
     */
    function close()
    {
        if ($this->conn)
        {
            $this->_debug("C: Close");
            ldap_unbind($this->conn);
            $this->conn = null;
        if ($this->ldap) {
            $this->ldap->close();
        }
    }
@@ -465,6 +377,29 @@
    function get_name()
    {
        return $this->prop['name'];
    }
    /**
     * Set internal list page
     *
     * @param  number  Page number to list
     */
    function set_page($page)
    {
        $this->list_page = (int)$page;
        $this->ldap->set_vlv_page($this->list_page, $this->page_size);
    }
    /**
     * Set internal page size
     *
     * @param  number  Number of records to display on one page
     */
    function set_pagesize($size)
    {
        $this->page_size = (int)$size;
        $this->ldap->set_vlv_page($this->list_page, $this->page_size);
    }
@@ -524,16 +459,14 @@
     */
    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
@@ -547,34 +480,34 @@
            $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;
@@ -599,25 +532,20 @@
        // 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));
            $this->_debug("C: Read Group [dn: $dn]");
            $entries = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
            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];
            if (empty($entry['objectclass']))
                continue;
            foreach ((array)$entry['objectclass'] as $objectclass)
            {
            foreach ((array)$entry['objectclass'] as $objectclass) {
                switch (strtolower($objectclass)) {
                    case "group":
                    case "groupofnames":
@@ -658,24 +586,19 @@
            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();
            }
@@ -701,34 +624,22 @@
    {
        $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];
                }
            }
        }
@@ -764,14 +675,11 @@
        $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++;
                }
@@ -783,34 +691,20 @@
        $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)) {
@@ -836,31 +730,27 @@
            }
        }
        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 .= ')';
                }
@@ -895,7 +785,6 @@
        // set filter string and execute search
        $this->set_search_set($filter);
        $this->_exec_search();
        if ($select)
            $this->list_records();
@@ -914,20 +803,21 @@
    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);
@@ -957,28 +847,16 @@
    {
        $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);
@@ -990,7 +868,7 @@
                // 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);
            }
        }
@@ -1104,7 +982,7 @@
        }
        // 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();
@@ -1117,19 +995,19 @@
            }
        }
        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);
@@ -1235,7 +1113,7 @@
        // 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;
            }
@@ -1245,17 +1123,17 @@
            // 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;
                }
@@ -1271,8 +1149,8 @@
        // 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;
                }
            }
@@ -1280,7 +1158,7 @@
        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;
            }
@@ -1288,7 +1166,7 @@
        // 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;
            }
@@ -1299,7 +1177,7 @@
            // 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);
@@ -1312,12 +1190,12 @@
        // 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);
            }
        }
@@ -1345,9 +1223,9 @@
            // 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;
                        }
@@ -1356,7 +1234,7 @@
            }
            // Delete the record.
            if (!$this->ldap_delete($dn)) {
            if (!$this->ldap->delete($dn)) {
                $this->set_error(self::ERROR_SAVING, 'errorsaving');
                return false;
            }
@@ -1365,7 +1243,7 @@
            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);
                }
            }
@@ -1380,8 +1258,8 @@
     */
    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) {
@@ -1418,120 +1296,26 @@
        }
    }
    /**
     * 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]))
@@ -1593,8 +1377,10 @@
            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;
                 }
               }
            }
        }
@@ -1645,6 +1431,16 @@
        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'),
            array_map('strtolower', (array)$entry['objectclass'])
        );
    }
    /**
     * Prints debug info to the log
@@ -1670,46 +1466,15 @@
    /**
     * 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);
    }
    /**
     * 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;
        }
@@ -1732,9 +1497,9 @@
            return array();
        // use cached list for searching
        $this->cache->expunge();
        if (!$search || ($group_cache = $this->cache->get('groups')) === null)
        if (!$this->cache || !empty($this->prop['group_filters']) || ($group_cache = $this->cache->get('groups')) === null) {
            $group_cache = $this->_fetch_groups();
        }
        $groups = array();
        if ($search) {
@@ -1755,6 +1520,23 @@
     */
    private function _fetch_groups($vlv_page = 0)
    {
        // special case: list groups from 'group_filters' config
        if (!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;
        }
        $base_dn = $this->groups_base_dn;
        $filter = $this->prop['groups']['filter'];
        $name_attr = $this->prop['groups']['name_attr'];
@@ -1762,46 +1544,46 @@
        $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
        $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'])
                $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'], $ns_function);
        $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();
        $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
@@ -1809,8 +1591,7 @@
            return $groups;
        // call recursively until we have fetched all groups
        while ($vlv_active && $group_count == $page_size)
        {
        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);
@@ -1821,9 +1602,46 @@
            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)
    {
        if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
            $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);
            $this->_debug("C: Read Group [dn: $dn]");
            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];
    }
    /**
@@ -1834,10 +1652,7 @@
     */
    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;
@@ -1851,9 +1666,8 @@
     */
    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';
@@ -1863,12 +1677,14 @@
            $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);
    }
@@ -1881,19 +1697,21 @@
     */
    function delete_group($group_id)
    {
        if (($group_cache = $this->cache->get('groups')) === null)
        if (!$this->cache || ($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";
        $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;
    }
@@ -1908,21 +1726,22 @@
     */
    function rename_group($group_id, $new_name, &$new_gid)
    {
        if (($group_cache = $this->cache->get('groups')) === null)
        if (!$this->cache || ($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);
        $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;
    }
@@ -1937,8 +1756,9 @@
     */
    function add_to_group($group_id, $contact_ids)
    {
        if (($group_cache = $this->cache->get('groups')) === null)
        if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
            $group_cache = $this->_fetch_groups();
        }
        if (!is_array($contact_ids))
            $contact_ids = explode(',', $contact_ids);
@@ -1946,7 +1766,7 @@
        $base_dn     = $this->groups_base_dn;
        $group_name  = $group_cache[$group_id]['name'];
        $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)
@@ -1957,7 +1777,9 @@
            return 0;
        }
        $this->cache->remove('groups');
        if ($this->cache) {
            $this->cache->remove('groups');
        }
        return count($new_attrs[$member_attr]);
    }
@@ -1972,8 +1794,9 @@
     */
    function remove_from_group($group_id, $contact_ids)
    {
        if (($group_cache = $this->cache->get('groups')) === null)
        if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
            $group_cache = $this->_fetch_groups();
        }
        if (!is_array($contact_ids))
            $contact_ids = explode(',', $contact_ids);
@@ -1981,7 +1804,7 @@
        $base_dn     = $this->groups_base_dn;
        $group_name  = $group_cache[$group_id]['name'];
        $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)
@@ -1992,7 +1815,9 @@
            return 0;
        }
        $this->cache->remove('groups');
        if ($this->cache) {
            $this->cache->remove('groups');
        }
        return count($del_attrs[$member_attr]);
    }
@@ -2019,23 +1844,18 @@
            $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;
    }
@@ -2078,135 +1898,6 @@
    /**
     * 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
@@ -2231,132 +1922,6 @@
    {
        $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
        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;
    }
}
program/lib/Roundcube/rcube_ldap_generic.php
New file
@@ -0,0 +1,1059 @@
<?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@gmail.com>                        |
 |         Aleksander Machniak <machniak@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
    * @param boolean $debug   Enables debug mode
    */
    function __construct($p, $debug = false)
    {
        $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']);
        $this->debug = $debug;
    }
    /**
     * 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 [$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: 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: $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: $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) {
            if (empty($filter))
                $filter = $filter = '(objectclass=*)';
            $this->_debug("C: Search [$filter][dn: $base_dn]");
            $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 ($this->config['numsub_filter'] && ($result_count = @$ns_function($this->conn, $base_dn, $this->config['numsub_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: $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: $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: $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: $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: $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: $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() + 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: $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: $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: " . $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 " . $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
        # 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 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;
    }
}
program/localization/en_US/labels.inc
@@ -348,6 +348,7 @@
$labels['group'] = 'Group';
$labels['groups'] = 'Groups';
$labels['listgroup'] = 'List group members';
$labels['personaladrbook'] = 'Personal Addresses';
$labels['searchsave'] = 'Save search';
program/steps/addressbook/copy.inc
@@ -57,6 +57,10 @@
    foreach ($cid as $cid) {
        $a_record = $CONTACTS->get_record($cid, true);
        // avoid copying groups
        if ($a_record['_type'] == 'group')
            continue;
        // Check if contact exists, if so, we'll need it's ID
        // Note: Some addressbooks allows empty email address field
        if (!empty($a_record['email']))
program/steps/addressbook/func.inc
@@ -310,7 +310,7 @@
    global $CONTACTS, $OUTPUT;
    // define list of cols to be displayed
    $a_show_cols = array('name');
    $a_show_cols = array('name','action');
    // add id to message list table if not specified
    if (!strlen($attrib['id']))
@@ -339,28 +339,70 @@
        return;
    // define list of cols to be displayed
    $a_show_cols = array('name');
    $a_show_cols = array('name','action');
    while ($row = $result->next()) {
        $row['CID'] = $row['ID'];
        $row['email'] = reset(rcube_addressbook::get_col_values('email', $row, true));
        $source_id = $OUTPUT->get_env('source');
        $a_row_cols = array();
        $classes = array('person');  // org records will follow some day
        $classes = array($row['_type'] ? $row['_type'] : 'person');
        // build contact ID with source ID
        if (isset($row['sourceid'])) {
            $row['ID'] = $row['ID'].'-'.$row['sourceid'];
            $source_id = $row['sourceid'];
        }
        // format each col
        foreach ($a_show_cols as $col) {
            $val = $col == 'name' ? rcube_addressbook::compose_list_name($row) : $row[$col];
            $a_row_cols[$col] = Q($val);
            $val = '';
            switch ($col) {
                case 'name':
                    $val = Q(rcube_addressbook::compose_list_name($row));
                    break;
                case 'action':
                    if ($row['_type'] == 'group') {
                        $val = html::a(array(
                            'href' => '#list',
                            'rel' => $row['ID'],
                            'title' => rcube_label('listgroup'),
                            'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", JS_OBJECT_NAME, $source_id, $row['CID']),
                        ), '&raquo;');
                    }
                    else
                        $val = '&nbsp;';
                    break;
                default:
                    $val = Q($row[$col]);
                    break;
            }
            $a_row_cols[$col] = $val;
        }
        if ($row['readonly'])
            $classes[] = 'readonly';
        $OUTPUT->command($prefix.'add_contact_row', $row['ID'], $a_row_cols, join(' ', $classes));
        $OUTPUT->command($prefix.'add_contact_row', $row['ID'], $a_row_cols, join(' ', $classes), array_intersect_key($row, array('ID'=>1,'readonly'=>1,'_type'=>1,'email'=>1,'name'=>1)));
    }
}
function rcmail_contacts_list_title($attrib)
{
    global $OUTPUT;
    $attrib += array('label' => 'contacts', 'id' => 'rcmabooklisttitle', 'tag' => 'span');
    unset($attrib['name']);
    $OUTPUT->add_gui_object('addresslist_title', $attrib['id']);
    $OUTPUT->add_label('contacts');
    return html::tag($attrib['tag'], $attrib, rcube_label($attrib['label']), html::$common_attrib);
}
@@ -432,7 +474,7 @@
function rcmail_contact_form($form, $record, $attrib = null)
{
    global $RCMAIL, $CONFIG;
    global $RCMAIL;
    // Allow plugins to modify contact form content
    $plugin = $RCMAIL->plugins->exec_hook('contact_form', array(
@@ -441,7 +483,7 @@
    $form = $plugin['form'];
    $record = $plugin['record'];
    $edit_mode = $RCMAIL->action != 'show';
    $del_button = $attrib['deleteicon'] ? html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete'))) : rcube_label('delete');
    $del_button = $attrib['deleteicon'] ? html::img(array('src' => $RCMAIL->output->get_skin_file($attrib['deleteicon']), 'alt' => rcube_label('delete'))) : rcube_label('delete');
    unset($attrib['deleteicon']);
    $out = '';
@@ -707,12 +749,15 @@
function rcmail_contact_photo($attrib)
{
    global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL, $CONFIG;
    global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL;
    if ($result = $CONTACTS->get_result())
        $record = $result->first();
    $photo_img = $attrib['placeholder'] ? $CONFIG['skin_path'] . $attrib['placeholder'] : 'program/resources/blank.gif';
    $photo_img = $attrib['placeholder'] ? $RCMAIL->output->get_skin_file($attrib['placeholder']) : 'program/resources/blank.gif';
    if ($record['_type'] == 'group' && $attrib['placeholdergroup'])
        $photo_img = $RCMAIL->output->get_skin_file($attrib['placeholdergroup']);
    $RCMAIL->output->set_env('photo_placeholder', $photo_img);
    unset($attrib['placeholder']);
@@ -803,6 +848,7 @@
    'directorylist' => 'rcmail_directory_list',
//  'groupslist' => 'rcmail_contact_groups',
    'addresslist' => 'rcmail_contacts_list',
    'addresslisttitle' => 'rcmail_contacts_list_title',
    'addressframe' => 'rcmail_contact_frame',
    'recordscountdisplay' => 'rcmail_rowcount_display',
    'searchform' => array($OUTPUT, 'search_form')
program/steps/addressbook/list.inc
@@ -81,6 +81,11 @@
        $OUTPUT->show_message('contactsearchonly', 'notice');
        $OUTPUT->command('command', 'advanced-search');
    }
    if ($CONTACTS->group_id) {
        $OUTPUT->command('set_group_prop', array('ID' => $CONTACTS->group_id)
            + array_intersect_key((array)$CONTACTS->get_group($CONTACTS->group_id), array('name'=>1,'email'=>1)));
    }
}
// update message count display
program/steps/addressbook/save.inc
@@ -134,11 +134,11 @@
    $record['email'] = reset($CONTACTS->get_col_values('email', $record, true));
    $record['name']  = rcube_addressbook::compose_list_name($record);
    foreach (array('name', 'email') as $col)
    foreach (array('name') as $col)
      $a_js_cols[] = Q((string)$record[$col]);
    // update the changed col in list
    $OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source);
    $OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source, $record);
    // show confirmation
    $OUTPUT->show_message('successfullysaved', 'confirmation', null, false);
program/steps/addressbook/show.inc
@@ -215,8 +215,11 @@
    $checkbox = new html_checkbox(array('name' => '_gid[]',
        'class' => 'groupmember', 'disabled' => $CONTACTS->readonly));
    foreach ($GROUPS as $group) {
    foreach (array_merge($GROUPS, $members) as $group) {
        $gid = $group['ID'];
        if ($seen[$gid]++)
            continue;
        $table->add(null, $checkbox->show($members[$gid] ? $gid : null,
            array('value' => $gid, 'id' => 'ff_gid' . $gid)));
        $table->add(null, html::label('ff_gid' . $gid, Q($group['name'])));
program/steps/mail/list_contacts.inc
@@ -73,8 +73,11 @@
        $CONTACTS->set_pagesize($page_size);
        $CONTACTS->set_page($list_page);
        if ($group_id = get_input_value('_gid', RCUBE_INPUT_GPC)) {
            $CONTACTS->set_group($group_id);
        }
        // list groups of this source (on page one)
        if ($CONTACTS->groups && $CONTACTS->list_page == 1) {
        else if ($CONTACTS->groups && $CONTACTS->list_page == 1) {
            foreach ($CONTACTS->list_groups() as $group) {
                $CONTACTS->reset();
                $CONTACTS->set_group($group['ID']);
@@ -89,6 +92,19 @@
                            'contactgroup' => html::span(array('title' => $email), Q($group['name']))), 'group');
                    }
                }
                // make virtual groups clickable to list their members
                else if ($group_prop['virtual']) {
                    $row_id = 'G'.$group['ID'];
                    $OUTPUT->command('add_contact_row', $row_id, array(
                        'contactgroup' => html::a(array(
                            'href' => '#list',
                            'rel' => $row['ID'],
                            'title' => rcube_label('listgroup'),
                            'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", JS_OBJECT_NAME, $source, $group['ID']),
                        ), Q($group['name']) . '&nbsp;' . html::span('action', '&raquo;'))),
                        'group',
                        array('ID' => $group['ID'], 'name' => $group['name'], 'virtual' => true));
                }
                // show group with count
                else if (($result = $CONTACTS->count()) && $result->count) {
                    $row_id = 'E'.$group['ID'];
@@ -97,10 +113,12 @@
                        'contactgroup' => Q($group['name'] . ' (' . intval($result->count) . ')')), 'group');
                }
            }
            $CONTACTS->reset();
            $CONTACTS->set_group(0);
        }
        // get contacts for this user
        $CONTACTS->set_group(0);
        $result = $CONTACTS->list_records($afields);
    }
}
@@ -118,10 +136,13 @@
        foreach ($emails as $i => $email) {
            $row_id = $row['ID'].$i;
            $jsresult[$row_id] = format_email_recipient($email, $name);
            $classname = $row['_type'] == 'group' ? 'group' : 'person';
            $keyname = $row['_type'] == 'group' ? 'contactgroup' : 'contact';
            $OUTPUT->command('add_contact_row', $row_id, array(
                'contact' => html::span(array('title' => $email), Q($name ? $name : $email) .
                $keyname => html::span(array('title' => $email), Q($name ? $name : $email) .
                    ($name && count($emails) > 1 ? '&nbsp;' . html::span('email', Q($email)) : '')
                )), 'person');
                )), $classname);
        }
    }
}
skins/classic/addressbook.css
@@ -216,6 +216,37 @@
  -o-text-overflow: ellipsis;
}
#contacts-table .contact.readonly td
{
  font-style: italic;
}
#contacts-table td.name
{
  width: 95%;
}
#contacts-table td.action
{
  width: 12px;
  padding: 0px 6px 0 4px;
  text-align: right;
}
#contacts-table td.action a
{
  font-size: 16px;
  font-weight: bold;
  font-style: normal;
  text-decoration: none;
  color: #333;
}
#contacts-table .selected td.action a
{
  color: #fff;
}
#contacts-box
{
  position: absolute;
skins/classic/mail.css
@@ -1666,6 +1666,14 @@
  -o-text-overflow: ellipsis;
}
#contacts-table td span.email
{
  display: inline;
  color: #ccc;
  font-style: italic;
  margin-left: 0.5em;
}
#abookcountbar
{
  margin-top: 4px;
skins/classic/templates/addressbook.html
@@ -65,7 +65,7 @@
<div id="addressscreen">
<div id="addresslist">
<div class="boxtitle"><roundcube:label name="contacts" /></div>
<roundcube:object name="addresslisttitle" label="contacts" tag="div" class="boxtitle" />
<div class="boxlistcontent">
<roundcube:object name="addresslist" id="contacts-table" class="records-table" cellspacing="0" summary="Contacts list" noheader="true" />
</div>
skins/classic/templates/contact.html
@@ -13,7 +13,7 @@
    <div id="sourcename"><roundcube:label name="addressbook" />: <roundcube:var name="env:sourcename" /></div>
  <roundcube:endif />
  <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div>
  <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>
  <roundcube:object name="contacthead" id="contacthead" />
  <div style="clear:both"></div>
  <div id="contacttabs">
skins/larry/addressbook.css
@@ -75,10 +75,6 @@
    text-overflow: ellipsis;
}
#contacts-table .contact.readonly td {
    font-style: italic;
}
#directorylist li.addressbook a {
    background-position: 6px -766px;
}
@@ -131,6 +127,28 @@
    left: 20px;
}
#contacts-table .contact.readonly td {
    font-style: italic;
}
#contacts-table td.name {
    width: 95%;
}
#contacts-table td.action {
    width: 24px;
    padding: 4px;
}
#contacts-table td.action a {
    display: block;
    width: 16px;
    height: 14px;
    text-indent: -5000px;
    overflow: hidden;
    background: url(images/listicons.png) -2px -1180px no-repeat;
}
#contacts-table .contact td.name {
    background-position: 6px -1603px;
}
@@ -141,6 +159,29 @@
    font-weight: bold;
}
#contacts-table .group td.name {
    background-position: 6px -1555px;
}
#contacts-table .group.selected td.name,
#contacts-table .group.unfocused td.name {
    background-position: 6px -1579px;
    font-weight: bold;
}
#addresslist .boxtitle {
    padding-right: 95px;
    overflow: hidden;
    text-overflow: ellipsis;
}
#addresslist .boxtitle a.poplink {
    color: #004458;
    font-size: 14px;
    line-height: 12px;
    text-decoration: none;
}
#contact-frame {
    position: absolute;
    top: 0;
skins/larry/ie7hacks.css
@@ -41,6 +41,7 @@
.boxfooter .listbutton .inner,
.attachmentslist li a.delete,
.attachmentslist li a.cancelupload,
#contacts-table td.action a,
.previewheader .iconlink,
.minimal #taskbar .button-inner {
    /* workaround for text-indent which also offsets the background image */
skins/larry/images/contactgroup.png
skins/larry/mail.css
@@ -1234,6 +1234,19 @@
    text-overflow: ellipsis;
}
#contacts-table td.contactgroup a {
    color: #376572;
    text-decoration: none;
}
#contacts-table td.contactgroup a span {
    display: inline-block;
    font-size: 16px;
    font-weight: bold;
    line-height: 11px;
    margin-left: 0.3em;
}
#contacts-table tr:first-child td {
    border-top: 0;
}
skins/larry/styles.css
@@ -995,6 +995,10 @@
    background-color: #e8e798;
}
.listbox table.listing {
    background-color: #d9ecf4;
}
table.listing,
table.layout {
    border: 0;
skins/larry/templates/addressbook.html
@@ -46,7 +46,7 @@
<!-- contacts list -->
<div id="addresslist" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="contacts" /></h2>
<roundcube:object name="addresslisttitle" label="contacts" tag="h2" class="boxtitle" />
<div class="scroller withfooter">
<roundcube:object name="addresslist" id="contacts-table" class="listing" noheader="true" />
</div>
skins/larry/templates/contact.html
@@ -13,7 +13,7 @@
        <div id="sourcename"><roundcube:label name="addressbook" />: <roundcube:var name="env:sourcename" /></div>
    <roundcube:endif />
    <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div>
    <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>
    <roundcube:object name="contacthead" id="contacthead" />
    <br style="clear:both" />
skins/larry/ui.js
@@ -176,6 +176,8 @@
    /***  addressbook task  ***/
    else if (rcmail.env.task == 'addressbook') {
      rcmail.addEventListener('afterupload-photo', show_uploadform);
      rcmail.addEventListener('beforepushgroup', push_contactgroup);
      rcmail.addEventListener('beforepopgroup', pop_contactgroup);
      if (rcmail.env.action == '') {
        new rcube_splitter({ id:'addressviewsplitterd', p1:'#addressview-left', p2:'#addressview-right',
@@ -748,6 +750,35 @@
    });
  }
  function push_contactgroup(p)
  {
    // lets the contacts list swipe to the left, nice!
    var table = $('#contacts-table'),
      scroller = table.parent().css('overflow', 'hidden');
    table.clone()
      .css({ position:'absolute', top:'0', left:'0', width:table.width()+'px', 'z-index':10 })
      .appendTo(scroller)
      .animate({ left: -(table.width()+5) + 'px' }, 300, 'swing', function(){
        $(this).remove();
        scroller.css('overflow', 'auto')
      });
  }
  function pop_contactgroup(p)
  {
    // lets the contacts list swipe to the left, nice!
    var table = $('#contacts-table'),
      scroller = table.parent().css('overflow', 'hidden'),
      clone = table.clone().appendTo(scroller);
      table.css({ position:'absolute', top:'0', left:-(table.width()+5) + 'px', width:table.width()+'px', height:table.height()+'px', 'z-index':10 })
        .animate({ left:'0' }, 300, 'linear', function(){
        clone.remove();
        $(this).css({ position:'relative', left:'0', width:'100%', height:'auto', 'z-index':1 });
        scroller.css('overflow', 'auto')
      });
  }
  function show_uploadform()
  {
@@ -758,7 +789,7 @@
      $dialog.dialog('close');
      return;
    }
    // add icons to clone file input field
    if (rcmail.env.action == 'compose' && !$dialog.data('extended')) {
      $('<a>')